diff --git a/frontend/src/modules/dashboard/BotDashboardModule.tsx b/frontend/src/modules/dashboard/BotDashboardModule.tsx index 8d282bc..f96249a 100644 --- a/frontend/src/modules/dashboard/BotDashboardModule.tsx +++ b/frontend/src/modules/dashboard/BotDashboardModule.tsx @@ -452,6 +452,63 @@ function normalizeDashboardAttachmentPath(path: string): string { return v.replace(/^\/+/, ''); } +const COMPOSER_DRAFT_STORAGE_PREFIX = 'nanobot-dashboard-composer-draft:v1:'; + +interface ComposerDraftStorage { + command: string; + attachments: string[]; + updated_at_ms: number; +} + +function getComposerDraftStorageKey(botId: string): string { + return `${COMPOSER_DRAFT_STORAGE_PREFIX}${String(botId || '').trim()}`; +} + +function loadComposerDraft(botId: string): ComposerDraftStorage | null { + const id = String(botId || '').trim(); + if (!id || typeof window === 'undefined') return null; + try { + const raw = window.localStorage.getItem(getComposerDraftStorageKey(id)); + if (!raw) return null; + const parsed = JSON.parse(raw) as Partial | null; + const command = String(parsed?.command || ''); + const attachments = normalizeAttachmentPaths(parsed?.attachments) + .map(normalizeDashboardAttachmentPath) + .filter(Boolean); + return { + command, + attachments, + updated_at_ms: Number(parsed?.updated_at_ms || Date.now()), + }; + } catch { + return null; + } +} + +function persistComposerDraft(botId: string, commandRaw: string, attachmentsRaw: string[]): void { + const id = String(botId || '').trim(); + if (!id || typeof window === 'undefined') return; + const command = String(commandRaw || ''); + const attachments = normalizeAttachmentPaths(attachmentsRaw) + .map(normalizeDashboardAttachmentPath) + .filter(Boolean); + const key = getComposerDraftStorageKey(id); + try { + if (!command.trim() && attachments.length === 0) { + window.localStorage.removeItem(key); + return; + } + const payload: ComposerDraftStorage = { + command, + attachments, + updated_at_ms: Date.now(), + }; + window.localStorage.setItem(key, JSON.stringify(payload)); + } catch { + // ignore localStorage write failures + } +} + function isExternalHttpLink(href: string): boolean { return /^https?:\/\//i.test(String(href || '').trim()); } @@ -631,6 +688,7 @@ export function BotDashboardModule({ const [workspaceAutoRefresh, setWorkspaceAutoRefresh] = useState(false); const [workspaceQuery, setWorkspaceQuery] = useState(''); const [pendingAttachments, setPendingAttachments] = useState([]); + const [composerDraftHydrated, setComposerDraftHydrated] = useState(false); const [quotedReply, setQuotedReply] = useState(null); const [isUploadingAttachments, setIsUploadingAttachments] = useState(false); const [attachmentUploadPercent, setAttachmentUploadPercent] = useState(null); @@ -700,7 +758,6 @@ export function BotDashboardModule({ memory_mb: String(clampMemoryMb(bot.memory_mb ?? 1024)), storage_gb: String(clampStorageGb(bot.storage_gb ?? 10)), }); - setPendingAttachments([]); }, []); const buildWorkspaceDownloadHref = (filePath: string, forceDownload: boolean = true) => { const query = [`path=${encodeURIComponent(filePath)}`]; @@ -1383,6 +1440,36 @@ export function BotDashboardModule({ if (selectedBotId && !activeBots[selectedBotId] && bots.length > 0) setSelectedBotId(bots[0].id); }, [bots, selectedBotId, activeBots, forcedBotId]); + useEffect(() => { + setComposerDraftHydrated(false); + if (!selectedBotId) { + setCommand(''); + setPendingAttachments([]); + setComposerDraftHydrated(true); + return; + } + const draft = loadComposerDraft(selectedBotId); + setCommand(draft?.command || ''); + setPendingAttachments(draft?.attachments || []); + setComposerDraftHydrated(true); + }, [selectedBotId]); + + useEffect(() => { + if (!selectedBotId || !composerDraftHydrated) return; + persistComposerDraft(selectedBotId, command, pendingAttachments); + }, [selectedBotId, composerDraftHydrated, command, pendingAttachments]); + + useEffect(() => { + const hasDraft = Boolean(String(command || '').trim()) || pendingAttachments.length > 0 || Boolean(quotedReply); + if (!hasDraft && !isUploadingAttachments) return; + const onBeforeUnload = (event: BeforeUnloadEvent) => { + event.preventDefault(); + event.returnValue = ''; + }; + window.addEventListener('beforeunload', onBeforeUnload); + return () => window.removeEventListener('beforeunload', onBeforeUnload); + }, [command, pendingAttachments.length, quotedReply, isUploadingAttachments]); + useEffect(() => { chatBottomRef.current?.scrollIntoView({ behavior: 'smooth', block: 'end' }); }, [selectedBotId, conversation.length]);