Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
87 lines
2.5 KiB
TypeScript
87 lines
2.5 KiB
TypeScript
import type { Info, Message, Part, Session } from "./types";
|
||
|
||
/**
|
||
* Read-only copy of the app's last state for the offline case (train, tunnel): the chat
|
||
* list and the most recently opened chats. Kept small: attachments are dropped and long
|
||
* tool output is cut. Everything is best-effort – storage may be full or blocked.
|
||
*/
|
||
const KEY = "codecraft.offline.v1";
|
||
const MAX_CHATS = 6;
|
||
const MAX_MESSAGES = 80;
|
||
const MAX_BYTES = 2_500_000;
|
||
|
||
interface Cache {
|
||
info?: Info;
|
||
sessions?: Session[];
|
||
chats: Record<string, { at: number; session: Session; messages: Message[] }>;
|
||
}
|
||
|
||
function read(): Cache {
|
||
try {
|
||
const c = JSON.parse(localStorage.getItem(KEY) ?? "null");
|
||
if (c && typeof c === "object") return { chats: {}, ...c };
|
||
} catch {
|
||
/* empty or corrupt */
|
||
}
|
||
return { chats: {} };
|
||
}
|
||
|
||
function write(c: Cache) {
|
||
try {
|
||
// Oldest chats go first when the copy gets too big.
|
||
let text = JSON.stringify(c);
|
||
while (text.length > MAX_BYTES && Object.keys(c.chats).length > 0) {
|
||
const oldest = Object.entries(c.chats).sort((a, b) => a[1].at - b[1].at)[0][0];
|
||
delete c.chats[oldest];
|
||
text = JSON.stringify(c);
|
||
}
|
||
localStorage.setItem(KEY, text);
|
||
} catch {
|
||
/* storage full or unavailable */
|
||
}
|
||
}
|
||
|
||
function slim(part: Part): Part {
|
||
if (part.type === "image") return { type: "text", text: "[image]" } as Part;
|
||
if (part.type === "tool_result") return { ...part, content: part.content.slice(0, 1500), images: [], meta: undefined } as Part;
|
||
return part;
|
||
}
|
||
|
||
export function cacheInfo(info: Info) {
|
||
if (!info.user) return;
|
||
write({ ...read(), info });
|
||
}
|
||
|
||
export function cachedInfo(): Info | undefined {
|
||
return read().info;
|
||
}
|
||
|
||
export function cacheSessions(sessions: Session[]) {
|
||
write({ ...read(), sessions });
|
||
}
|
||
|
||
export function cachedSessions(): Session[] {
|
||
return read().sessions ?? [];
|
||
}
|
||
|
||
export function cacheChat(session: Session, messages: Message[]) {
|
||
const c = read();
|
||
c.chats[session.id] = {
|
||
at: Date.now(),
|
||
session,
|
||
messages: messages.slice(-MAX_MESSAGES).map((m) => ({ ...m, parts: m.parts.map(slim) })),
|
||
};
|
||
const ids = Object.entries(c.chats).sort((a, b) => b[1].at - a[1].at);
|
||
for (const [id] of ids.slice(MAX_CHATS)) delete c.chats[id];
|
||
write(c);
|
||
}
|
||
|
||
export function cachedChat(id: string) {
|
||
return read().chats[id];
|
||
}
|
||
|
||
/** A request failed because there is no connection (as opposed to an answer from the server). */
|
||
export function isOffline(e: unknown): boolean {
|
||
return e instanceof TypeError || !navigator.onLine;
|
||
}
|