Spaces:
Runtime error
Runtime error
| /** | |
| * Typed localStorage read/write helpers for the health-marketing-compliance-rag frontend. | |
| * | |
| * Keys: | |
| * hmc-conversations — array of Conversation objects (max 20, sorted by last_active_at desc) | |
| * hmc-saved-responses — array of SavedResponse objects (max 50, sorted by saved_at desc) | |
| */ | |
| import type { Conversation, Message, SavedResponse } from "../types"; | |
| const CONVERSATIONS_KEY = "hmc-conversations"; | |
| const SAVED_RESPONSES_KEY = "hmc-saved-responses"; | |
| const CONVERSATIONS_CAP = 20; | |
| const SAVED_RESPONSES_CAP = 50; | |
| const TITLE_MAX_LENGTH = 80; | |
| // --------------------------------------------------------------------------- | |
| // Conversations | |
| // --------------------------------------------------------------------------- | |
| /** | |
| * Reads the stored conversations array from localStorage. | |
| * Returns an empty array on missing key or JSON parse error. | |
| */ | |
| export function readConversations(): Conversation[] { | |
| try { | |
| const raw = localStorage.getItem(CONVERSATIONS_KEY); | |
| if (raw === null) return []; | |
| return JSON.parse(raw) as Conversation[]; | |
| } catch { | |
| return []; | |
| } | |
| } | |
| /** | |
| * Writes conversations to localStorage, enforcing a cap of 20 entries. | |
| * Entries are sorted by `last_active_at` descending before the cap is applied, | |
| * so the oldest entry is always the one evicted. | |
| * | |
| * On `QuotaExceededError`: evicts the oldest conversation and retries once. | |
| * If the retry also fails, emits a console warning and swallows the error. | |
| */ | |
| export function writeConversations(conversations: Conversation[]): void { | |
| // Sort descending by last_active_at, then cap at 20. | |
| const sorted = [...conversations].sort( | |
| (a, b) => | |
| new Date(b.last_active_at).getTime() - | |
| new Date(a.last_active_at).getTime() | |
| ); | |
| const capped = sorted.slice(0, CONVERSATIONS_CAP); | |
| try { | |
| localStorage.setItem(CONVERSATIONS_KEY, JSON.stringify(capped)); | |
| } catch (err) { | |
| if (isQuotaError(err)) { | |
| // Evict the oldest entry (last in the sorted array) and retry once. | |
| const evicted = capped.slice(0, capped.length - 1); | |
| try { | |
| localStorage.setItem(CONVERSATIONS_KEY, JSON.stringify(evicted)); | |
| } catch (retryErr) { | |
| console.warn( | |
| "[storage] Failed to write conversations after quota eviction:", | |
| retryErr | |
| ); | |
| } | |
| } | |
| } | |
| } | |
| // --------------------------------------------------------------------------- | |
| // Saved responses | |
| // --------------------------------------------------------------------------- | |
| /** | |
| * Reads the stored saved-responses array from localStorage. | |
| * Returns an empty array on missing key or JSON parse error. | |
| */ | |
| export function readSavedResponses(): SavedResponse[] { | |
| try { | |
| const raw = localStorage.getItem(SAVED_RESPONSES_KEY); | |
| if (raw === null) return []; | |
| return JSON.parse(raw) as SavedResponse[]; | |
| } catch { | |
| return []; | |
| } | |
| } | |
| /** | |
| * Writes saved responses to localStorage, enforcing a cap of 50 entries. | |
| * Entries are sorted by `saved_at` descending before the cap is applied, | |
| * so the oldest entry is always the one evicted. | |
| * | |
| * On `QuotaExceededError`: evicts the oldest response and retries once. | |
| * If the retry also fails, emits a console warning and swallows the error. | |
| */ | |
| export function writeSavedResponses(responses: SavedResponse[]): void { | |
| // Sort descending by saved_at, then cap at 50. | |
| const sorted = [...responses].sort( | |
| (a, b) => | |
| new Date(b.saved_at).getTime() - new Date(a.saved_at).getTime() | |
| ); | |
| const capped = sorted.slice(0, SAVED_RESPONSES_CAP); | |
| try { | |
| localStorage.setItem(SAVED_RESPONSES_KEY, JSON.stringify(capped)); | |
| } catch (err) { | |
| if (isQuotaError(err)) { | |
| // Evict the oldest entry (last in the sorted array) and retry once. | |
| const evicted = capped.slice(0, capped.length - 1); | |
| try { | |
| localStorage.setItem(SAVED_RESPONSES_KEY, JSON.stringify(evicted)); | |
| } catch (retryErr) { | |
| console.warn( | |
| "[storage] Failed to write saved responses after quota eviction:", | |
| retryErr | |
| ); | |
| } | |
| } | |
| } | |
| } | |
| // --------------------------------------------------------------------------- | |
| // Utility helpers | |
| // --------------------------------------------------------------------------- | |
| /** | |
| * Derives a conversation title from a message array. | |
| * Finds the first message with role "user" and truncates its content to 80 | |
| * characters, appending the Unicode ellipsis character (U+2026) if truncated. | |
| * Returns "New conversation" when no user message is present. | |
| */ | |
| export function titleFromMessages(messages: Message[]): string { | |
| const userMessage = messages.find((m) => m.role === "user"); | |
| if (!userMessage) return "New conversation"; | |
| const { content } = userMessage; | |
| if (content.length > TITLE_MAX_LENGTH) { | |
| return content.slice(0, TITLE_MAX_LENGTH) + "\u2026"; | |
| } | |
| return content; | |
| } | |
| /** | |
| * Returns the stable ID for a saved response: `"{conversationId}:{msgIndex}"`. | |
| */ | |
| export function savedResponseId( | |
| conversationId: string, | |
| msgIndex: number | |
| ): string { | |
| return `${conversationId}:${msgIndex}`; | |
| } | |
| // --------------------------------------------------------------------------- | |
| // Internal helpers | |
| // --------------------------------------------------------------------------- | |
| function isQuotaError(err: unknown): boolean { | |
| if (err instanceof DOMException) { | |
| // Standard name across modern browsers. | |
| return ( | |
| err.name === "QuotaExceededError" || | |
| // Legacy Firefox name. | |
| err.name === "NS_ERROR_DOM_QUOTA_REACHED" | |
| ); | |
| } | |
| return false; | |
| } | |