Skip to content

feat: Persist AI chat history across page reloads using IndexedDB #284

@d-oit

Description

@d-oit

Problem

useChat.ts initializes its messages state fresh on every component mount. All conversation history is lost on page reload, making the AI harness effectively stateless across sessions. There is no session management UI to view, switch, or clear chat sessions.

Impact

  • Users lose all context when refreshing the page
  • No ability to continue a previous conversation
  • No audit trail of AI interactions
  • Rebuilding context manually on every session wastes tokens

Proposed Implementation

1. Chat session data model

// src/lib/chat/types.ts
export interface ChatSession {
  id: string;
  title: string;          // auto-generated from first message
  messages: Message[];
  createdAt: number;
  updatedAt: number;
  model: string;
  provider: string;
}

2. IndexedDB persistence layer

// src/lib/chat/chat-persistence.ts
import { openDB } from 'idb';

const DB_NAME = 'do-knowledge-studio';
const STORE = 'chat-sessions';

export async function saveChatSession(session: ChatSession): Promise<void> {
  const db = await openDB(DB_NAME, 1, {
    upgrade(db) {
      if (!db.objectStoreNames.contains(STORE)) {
        const store = db.createObjectStore(STORE, { keyPath: 'id' });
        store.createIndex('updatedAt', 'updatedAt');
      }
    },
  });
  await db.put(STORE, session);
}

export async function loadChatSessions(): Promise<ChatSession[]> {
  const db = await openDB(DB_NAME, 1);
  return db.getAllFromIndex(STORE, 'updatedAt');
}

export async function deleteChatSession(id: string): Promise<void> {
  const db = await openDB(DB_NAME, 1);
  await db.delete(STORE, id);
}

3. Update useChat.ts

// Initialize from persisted session
const [sessionId] = useState(() => currentSessionId ?? crypto.randomUUID());

// Load messages from IndexedDB on mount
useEffect(() => {
  loadChatSession(sessionId).then(session => {
    if (session) setMessages(session.messages);
  });
}, [sessionId]);

// Auto-save on every message change (debounced 500ms)
useEffect(() => {
  const timeout = setTimeout(() => {
    saveChatSession({
      id: sessionId,
      title: messages[0]?.content.slice(0, 60) ?? 'New Chat',
      messages,
      createdAt: Date.now(),
      updatedAt: Date.now(),
      model: currentModel,
      provider: currentProvider,
    });
  }, 500);
  return () => clearTimeout(timeout);
}, [messages]);

4. Session management sidebar

Add a ChatSessionList component in AIHarness.tsx:

  • Lists recent sessions (sorted by updatedAt desc)
  • Click to switch to session and load its messages
  • "New Chat" button to create a fresh session
  • Delete button per session with confirmation
  • Session title auto-generated from first user message (first 60 chars)
  • Optional: export session as Markdown

5. Session title auto-generation

function generateSessionTitle(messages: Message[]): string {
  const firstUserMsg = messages.find(m => m.role === 'user');
  if (!firstUserMsg) return 'New Chat';
  return firstUserMsg.content.slice(0, 60) + (firstUserMsg.content.length > 60 ? '...' : '');
}

Acceptance Criteria

  • ChatSession type defined with id, title, messages, timestamps, model/provider
  • chat-persistence.ts with IndexedDB save/load/delete using idb library
  • useChat.ts loads from and saves to IndexedDB
  • Auto-save debounced at 500ms after message changes
  • ChatSessionList sidebar component implemented
  • New Chat / switch / delete session UI working
  • Session titles auto-generated from first user message
  • Unit tests for persistence layer with mock IndexedDB

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions