Stats history with year filter, folding and XLSX/CSV export; snippets, read-only links, commit suggestions, weekly review, offline copies, server speech-to-text, planning model, job reports; fal.ai health fix

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard ZinkandClaude Sonnet 5.5 committed 2026-11-15 08:20:52 +01:00
1 parent c08c6c7436
commit 388ca96419
31 files changed
+1518 -82

No files matched your search

+86
View File
@@ -0,0 +1,86 @@
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;
}