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:
1 parent
c08c6c7436
commit
388ca96419
31 files changed
+1518
-82
No files matched your search
@@ -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;
|
||||
}
|
||||
Reference in new issue
Block a user