import { Signal } from "@char/aftercare"; import type { InboxItem } from "@earendil-works/pi-durable"; import type { UserMessage } from "@earendil-works/pi-ai"; import { imageSummary } from "@berry/api/images.ts"; import { storedDraft } from "./draft-storage.ts"; import { keyed, type Scope } from "./ui.ts"; export type StoppedInput = Extract; class StoppedInputs { readonly items = new Signal([]); readonly ready: Promise; #saving = Promise.resolve(); constructor(readonly tid: string) { this.ready = typeof indexedDB === "undefined" ? Promise.resolve() : storedDraft(`stopped:${tid}`).then((items) => this.items.set(items)); } save(items: readonly StoppedInput[]): Promise { this.items.set(items); if (typeof indexedDB === "undefined") return Promise.resolve(); const saved = this.#saving.then(() => storedDraft(`stopped:${this.tid}`, items)).then(() => {}); this.#saving = saved.catch(() => {}); return saved; } } const drafts = new Map(); /** Aborted inputs are drafts, never members of the conversation's live queue. */ export function stoppedInputs( scope: Scope, tid: string, restore: (content: UserMessage["content"]) => Promise, notify: (message: string, error?: boolean) => void, ) { if (!drafts.has(tid)) drafts.set(tid, new StoppedInputs(tid)); const draft = drafts.get(tid)!; let disposed = false; let pending = false; const element =
as HTMLElement; const restoreInput = async (item: StoppedInput) => { if (pending || disposed) return; pending = true; try { if (await restore(item.content)) { await draft.save(draft.items.get().filter(({ id }) => id !== item.id)); } else { notify("Send or clear your current draft before restoring a stopped message."); } } finally { pending = false; } }; const report = (error: unknown) => notify(error instanceof Error ? error.message : String(error), true); const rows = keyed(element, (item: StoppedInput) => item.id, (item) => ({ node: (
stopped{" "} {typeof item.content === "string" ? item.content : item.content.flatMap((block) => block.type === "text" ? [block.text] : []).join( "\n", )} {" "} {imageSummary(item.content)}
), })); scope.add( draft.items.subscribeImmediate((items) => { element.hidden = items.length === 0; rows.sync(items); }).unsubscribe, ); scope.add(rows.dispose); scope.add(() => disposed = true); void draft.ready.catch(report); return { element, async recover(inputs: readonly StoppedInput[]) { if (inputs.length === 0) return; await draft.ready.catch(() => {}); const ordered = [...inputs].sort((a, b) => Number(a.mode === "followUp") - Number(b.mode === "followUp") ); const ids = new Set(draft.items.get().map(({ id }) => id)); await draft.save([...draft.items.get(), ...ordered.filter(({ id }) => !ids.has(id))]); await restoreInput(ordered[0]); }, }; }