172 lines
4.4 KiB
TypeScript
172 lines
4.4 KiB
TypeScript
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;
|
|
}
|
|
|
|
export interface QAResponse {
|
|
question: string;
|
|
answer: string;
|
|
confidence: number;
|
|
citations: Citation[];
|
|
}
|
|
|
|
export interface SessionContext {
|
|
summary: string;
|
|
last_updated: string | null;
|
|
}
|
|
|
|
export interface ChatSessionState {
|
|
initial_context: string | null;
|
|
citations: Citation[];
|
|
qa_history: QAResponse[];
|
|
session_context: SessionContext | null;
|
|
document_filter: string[];
|
|
citation_registry: Record<string, number>;
|
|
}
|
|
|
|
export interface StoredMessage {
|
|
id: string;
|
|
role?: string;
|
|
content?: string;
|
|
[key: string]: unknown;
|
|
}
|
|
|
|
export interface StoredSession {
|
|
id: string;
|
|
title: string;
|
|
messages: StoredMessage[];
|
|
chatState: ChatSessionState;
|
|
createdAt: string;
|
|
updatedAt: string;
|
|
}
|
|
|
|
const SESSIONS_KEY = "haiku.rag.sessions";
|
|
const ACTIVE_SESSION_KEY = "haiku.rag.activeSession";
|
|
|
|
export function normalizeChatState(state?: ChatSessionState): ChatSessionState {
|
|
return {
|
|
initial_context: state?.initial_context ?? null,
|
|
citations: state?.citations ?? [],
|
|
qa_history: state?.qa_history ?? [],
|
|
session_context: state?.session_context ?? null,
|
|
document_filter: state?.document_filter ?? [],
|
|
citation_registry: state?.citation_registry ?? {},
|
|
};
|
|
}
|
|
|
|
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: [],
|
|
chatState: normalizeChatState(),
|
|
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[],
|
|
chatState: ChatSessionState,
|
|
): void {
|
|
const sessions = getAllSessions();
|
|
const idx = sessions.findIndex((s) => s.id === id);
|
|
if (idx < 0) return;
|
|
|
|
const session = sessions[idx];
|
|
session.messages = messages;
|
|
session.chatState = chatState;
|
|
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);
|
|
}
|