haiku.rag/app/frontend/lib/sessionStorage.ts
2026-07-24 15:26:17 +03:00

165 lines
4.3 KiB
TypeScript

export interface Citation {
index: number;
document_id: string;
chunk_id: string;
chunk_ids?: string[];
document_uri: string;
document_title: string | null;
page_numbers: number[];
headings: string[] | null;
content: string;
doc_item_refs?: string[];
}
// Matches RAGState from the backend capability.
export interface RAGState {
citation_index: Record<string, Citation>;
citations: string[];
document_filter: string | null;
searches: Record<string, unknown[]>;
}
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>): 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);
}