From 4dc774d03f8905d2eb38b0f7446b1c30ed41415c Mon Sep 17 00:00:00 2001 From: Dayuan Jiang <34411969+DayuanJiang@users.noreply.github.com> Date: Sun, 4 Jan 2026 10:25:19 +0900 Subject: [PATCH] feat: add chat session history with IndexedDB persistence (#500) * feat(session): add chat session history with IndexedDB storage - Add session-storage.ts with IndexedDB wrapper using idb library - Add use-session-manager.ts hook for session state management - Add session-history-dropdown.tsx for session selection UI - Integrate session system into chat-panel.tsx - Auto-generate session titles from first user message - Auto-save sessions on message completion - Support session switching, deletion, and creation - Migrate existing localStorage data to IndexedDB - Add i18n translations for session history UI * feat(session): improve history dropdown and persist diagram history - Add time-based grouping (Today, Yesterday, This Week, Earlier) - Add thumbnail previews using Next.js Image component - Add staggered entrance animations with fade-in effects - Improve active session indicator with left border accent - Fix scrolling by using native overflow instead of ScrollArea - Persist diagram version history to IndexedDB sessions - Remove redundant diagram XML from localStorage - Add i18n strings for time group labels (en, ja, zh) * fix(session): prevent data loss on theme change and tab close - Add isDrawioReady effect to restore diagram after DrawIO remount - Add visibilitychange handler to save session when page becomes hidden - Fix missing currentSessionId in saveCurrentSession dependency array - Remove unused sanitizeMessages import from use-session-manager * fix(session): fix diagram save and migration data loss bugs - Add diagramHistory to save effect dependency array so diagram-only edits trigger saves (previously only message changes did) - Destructure stable sessionManager values to prevent unnecessary effect re-runs on every render - Add try-catch wrapper around debounced async save operation - Make saveSession() return boolean to indicate success/failure - Verify IndexedDB write succeeded before deleting localStorage data during migration (prevents data loss if write silently fails) - Keep localStorage data for retry if migration fails instead of marking as complete anyway * refactor(session): extract helpers to reduce code duplication - Add syncUIWithSession helper to consolidate 4 duplicate UI sync blocks - Add buildSessionData helper to consolidate 4 duplicate save logic blocks - Remove unused saveTimeoutRef and its cleanup effect - Net reduction of ~80 lines of duplicate code * style(ui): improve history dropdown and delete dialog styling - Change destructive color from coral to muted rose for refined look - Make session history panel taller (400px fixed height) - Fix popover alignment to prevent truncation - Style delete button with soft red outline instead of solid fill - Make delete dialog more compact (max-w-sm) * fix(session): reset refs on new chat and show recent sessions - Fix cached example diagrams not displaying after creating new session - Reset previousXML, lastProcessedXmlRef and processedToolCalls when messages become empty (new chat or session switch) - Add recent chats section in empty chat state with collapsible examples - Pass sessions and onSelectSession to ChatMessageDisplay - Add loadedMessageIdsRef to skip animations on session restore - Add debug console.log for diagram processing flow * feat(session): add search bar and improve history UI - Remove session history dropdown, use main panel instead - Add search bar to filter history chats by title - Show minutes (Xm ago) instead of "Just now" for recent sessions - Scroll to top when switching to new/empty chat - Remove title truncation limit for better searchability - Remove debug console.log statements * refactor: remove redundant code and fix nested button hydration error - Remove unused 'sessions' from deleteSession dependency array - Remove unused 'switchedTo' variable and simplify return type - Remove unused 'restoredMessageIdsRef' (always empty) - Fix nested button hydration error by using div with role=button - Simplify handleDeleteSession callback * fix(session): fix migration bug, improve metadata perf, truncate titles - Fix migration retry loop when localStorage has empty array - Use cursor-based iteration for getAllSessionMetadata - Truncate session titles to 100 chars with ellipsis * refactor: remove dead code and extract diagram length constant - Remove unused exports: getAllSessions, createNewSession, updateSessionTitle - Remove write-only CURRENT_SESSION_KEY and all localStorage calls - Remove dead messagesEndRef and unused scroll effect - Extract magic number 300 to MIN_REAL_DIAGRAM_LENGTH constant - Add isRealDiagram() helper function for semantic clarity --- app/[lang]/page.tsx | 42 +-- app/globals.css | 6 +- components/chat-example-panel.tsx | 82 ++--- components/chat-input.tsx | 161 ++++------ components/chat-message-display.tsx | 336 +++++++++++++++++--- components/chat-panel.tsx | 473 +++++++++++++++++++--------- contexts/diagram-context.tsx | 91 +++--- hooks/use-session-manager.ts | 322 +++++++++++++++++++ lib/i18n/dictionaries/en.json | 16 + lib/i18n/dictionaries/ja.json | 16 + lib/i18n/dictionaries/zh.json | 16 + lib/session-storage.ts | 338 ++++++++++++++++++++ lib/storage.ts | 10 +- lib/utils.ts | 19 ++ package-lock.json | 53 ++-- package.json | 5 +- 16 files changed, 1547 insertions(+), 439 deletions(-) create mode 100644 hooks/use-session-manager.ts create mode 100644 lib/session-storage.ts diff --git a/app/[lang]/page.tsx b/app/[lang]/page.tsx index 6b7ee1d..ffbf4a8 100644 --- a/app/[lang]/page.tsx +++ b/app/[lang]/page.tsx @@ -1,6 +1,6 @@ "use client" import { usePathname, useRouter } from "next/navigation" -import { useCallback, useEffect, useRef, useState } from "react" +import { Suspense, useCallback, useEffect, useRef, useState } from "react" import { DrawIoEmbed } from "react-drawio" import type { ImperativePanelHandle } from "react-resizable-panels" import ChatPanel from "@/components/chat-panel" @@ -22,7 +22,6 @@ export default function Home() { handleDiagramExport, onDrawioLoad, resetDrawioReady, - saveDiagramToStorage, showSaveDialog, setShowSaveDialog, } = useDiagram() @@ -110,8 +109,7 @@ export default function Home() { onDrawioLoad() }, [onDrawioLoad]) - const handleDarkModeChange = async () => { - await saveDiagramToStorage() + const handleDarkModeChange = () => { const newValue = !darkMode setDarkMode(newValue) localStorage.setItem("next-ai-draw-io-dark-mode", String(newValue)) @@ -120,8 +118,7 @@ export default function Home() { resetDrawioReady() } - const handleDrawioUiChange = async () => { - await saveDiagramToStorage() + const handleDrawioUiChange = () => { const newUi = drawioUi === "min" ? "sketch" : "min" localStorage.setItem("drawio-theme", newUi) setDrawioUi(newUi) @@ -129,7 +126,7 @@ export default function Home() { resetDrawioReady() } - // Check mobile - save diagram and reset draw.io before crossing breakpoint + // Check mobile - reset draw.io before crossing breakpoint const isInitialRenderRef = useRef(true) useEffect(() => { const checkMobile = () => { @@ -138,7 +135,6 @@ export default function Home() { !isInitialRenderRef.current && newIsMobile !== isMobileRef.current ) { - saveDiagramToStorage().catch(() => {}) setIsDrawioReady(false) resetDrawioReady() } @@ -150,7 +146,7 @@ export default function Home() { checkMobile() window.addEventListener("resize", checkMobile) return () => window.removeEventListener("resize", checkMobile) - }, [saveDiagramToStorage, resetDrawioReady]) + }, [resetDrawioReady]) const toggleChatPanel = () => { const panel = chatPanelRef.current @@ -266,16 +262,24 @@ export default function Home() { onExpand={() => setIsChatVisible(true)} >
+ {dict.examples.mcpDescription} +
+- {dict.examples.mcpDescription} +
+ + {/* Welcome section */} ++ {dict.examples.subtitle}
- {dict.examples.subtitle} -
-- {dict.examples.quickExamples} -
+ {!minimal && ( ++ {dict.examples.quickExamples} +
+ )}