export interface Citation { index: number; document_id: string; chunk_id: string; document_uri: string; document_title: string | null; page_numbers: number[]; headings: string[] | null; content: string; } // Matches RAGState from the backend skill export interface RAGState { citation_index: Record; citations: string[]; document_filter: string | null; searches: Record; } export interface StoredMessage { id: string; role?: string; content?: string; [key: string]: unknown; } export interface StoredSession { id: string; title: string; messages: StoredMessage[]; ragState: RAGState; createdAt: string; updatedAt: string; } const SESSIONS_KEY = "haiku.rag.sessions"; const ACTIVE_SESSION_KEY = "haiku.rag.activeSession"; export function normalizeRAGState(state?: Partial): RAGState { return { citation_index: state?.citation_index ?? {}, citations: state?.citations ?? [], document_filter: state?.document_filter ?? null, searches: state?.searches ?? {}, }; } export function getLatestCitations(state: RAGState): Citation[] { return state.citations .map((id) => state.citation_index[id]) .filter((c): c is Citation => c !== undefined); } export function getAllSessions(): StoredSession[] { const raw = localStorage.getItem(SESSIONS_KEY); if (!raw) return []; try { return JSON.parse(raw) as StoredSession[]; } catch { return []; } } export function getSession(id: string): StoredSession | null { return getAllSessions().find((s) => s.id === id) ?? null; } export function getActiveSessionId(): string | null { return localStorage.getItem(ACTIVE_SESSION_KEY); } export function setActiveSessionId(id: string): void { localStorage.setItem(ACTIVE_SESSION_KEY, id); } export function createSession(): StoredSession { const now = new Date().toISOString(); const session: StoredSession = { id: crypto.randomUUID(), title: "New Session", messages: [], ragState: normalizeRAGState(), createdAt: now, updatedAt: now, }; const sessions = getAllSessions(); sessions.unshift(session); localStorage.setItem(SESSIONS_KEY, JSON.stringify(sessions)); setActiveSessionId(session.id); return session; } export function saveSession(session: StoredSession): void { const sessions = getAllSessions(); const idx = sessions.findIndex((s) => s.id === session.id); if (idx >= 0) { sessions[idx] = session; } else { sessions.unshift(session); } localStorage.setItem(SESSIONS_KEY, JSON.stringify(sessions)); } export function updateSessionMessages( id: string, messages: StoredMessage[], ragState: RAGState, ): void { const sessions = getAllSessions(); const idx = sessions.findIndex((s) => s.id === id); if (idx < 0) return; const session = sessions[idx]; session.messages = messages; session.ragState = ragState; session.updatedAt = new Date().toISOString(); // Derive title from first user message if (session.title === "New Session") { const firstUserMsg = messages.find( (m) => m.content && typeof m.role === "string" && m.role.toLowerCase() === "user", ); if (firstUserMsg?.content) { session.title = firstUserMsg.content.length > 60 ? `${firstUserMsg.content.slice(0, 57)}...` : firstUserMsg.content; } } sessions[idx] = session; localStorage.setItem(SESSIONS_KEY, JSON.stringify(sessions)); } export function deleteSession(id: string): void { const sessions = getAllSessions().filter((s) => s.id !== id); localStorage.setItem(SESSIONS_KEY, JSON.stringify(sessions)); if (getActiveSessionId() === id) { localStorage.removeItem(ACTIVE_SESSION_KEY); } } export function exportSessionToMarkdown(session: StoredSession): void { const lines: string[] = [`# ${session.title}`, ""]; for (const msg of session.messages) { const role = typeof msg.role === "string" ? msg.role.toLowerCase() : ""; if (role === "user" && msg.content) { lines.push(`**User:** ${msg.content}`, ""); } else if (role === "assistant" && msg.content) { lines.push(`**Assistant:** ${msg.content}`, ""); } } const blob = new Blob([lines.join("\n")], { type: "text/markdown" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `${session.title.replace(/[^a-zA-Z0-9]/g, "_")}.md`; a.click(); URL.revokeObjectURL(url); }