import type { ApiClient } from "$lib/api/client"; import { LiveStream } from "$lib/api/live.svelte"; import type { BusEvent, ConversationSummary } from "$lib/api/types"; const RELOAD_DEBOUNCE_MS = 1500; const PAGE = 500; // The conversation index kept fresh by ``/api/events``: rows land as // ``conversation.*`` events arrive, turn markers flip ``running_turn`` // without a refetch, and anything that moves the queue reloads once. // Shared by the admin's rail and the panel's picker. export class ConversationIndex { conversations = $state([]); loaded = $state(false); readonly live: LiveStream; protected readonly client: () => ApiClient | null; private reloadTimer: ReturnType | null = null; constructor(client: () => ApiClient | null) { this.client = client; this.live = new LiveStream(client, "/api/events", { onEvent: (event) => this.apply(event), prepare: () => this.load(), }); } get running(): ConversationSummary[] { return this.conversations.filter((row) => row.running_turn); } get open(): ConversationSummary[] { return this.conversations.filter((row) => row.status === "open"); } byId(id: string): ConversationSummary | undefined { return this.conversations.find((row) => row.id === id); } async load(): Promise { const client = this.client(); if (!client) { return; } const list = await client.conversations({ limit: PAGE }); this.conversations = list.conversations; this.loaded = true; } apply(event: BusEvent): void { switch (event.type) { case "conversation.created": case "conversation.updated": { if (typeof event.id === "string") { this.upsert(event as unknown as ConversationSummary); } return; } case "turn.start": case "turn.end": { const row = event.conversation_id ? this.byId(event.conversation_id) : undefined; if (row) { row.running_turn = event.type === "turn.start" ? (event.turn_id ?? null) : null; row.last_activity_at = event.ts; } return; } case "message.queued": case "inject.queued": case "reply": { this.reloadSoon(); return; } default: } } start(): void { this.live.start(); } stop(): void { this.live.stop(); } private upsert(row: ConversationSummary): void { const index = this.conversations.findIndex((c) => c.id === row.id); if (index >= 0) { this.conversations[index] = { ...this.conversations[index], ...row }; } else { this.conversations.unshift(row); } } private reloadSoon(): void { if (this.reloadTimer) { return; } this.reloadTimer = setTimeout(() => { this.reloadTimer = null; this.load().catch(() => undefined); }, RELOAD_DEBOUNCE_MS); } }