import { resource, tapState, tapMemo } from "@assistant-ui/tap"; import { type ClientOutput, tapClientLookup, Derived, attachTransformScopes, tapClientResource, } from "@assistant-ui/store"; import { withKey } from "@assistant-ui/tap"; import type { ResourceElement } from "@assistant-ui/tap"; import { ModelContext, Suggestions } from "@assistant-ui/core/store"; import { Tools, DataRenderers } from "@assistant-ui/core/react"; const RESOLVED_PROMISE = Promise.resolve(); export type InMemoryThreadListProps = { thread: (threadId: string) => ResourceElement>; onSwitchToThread?: (threadId: string) => void; onSwitchToNewThread?: () => void; }; type ThreadData = { id: string; title?: string; status: "regular" | "archived"; }; // ThreadListItem Client const ThreadListItemClient = resource( (props: { data: ThreadData; onSwitchTo: () => void; onArchive: () => void; onUnarchive: () => void; onDelete: () => void; }): ClientOutput<"threadListItem"> => { const { data, onSwitchTo, onArchive, onUnarchive, onDelete } = props; const state = tapMemo( () => ({ id: data.id, remoteId: undefined, externalId: undefined, title: data.title, status: data.status, }), [data.id, data.title, data.status], ); return { getState: () => state, switchTo: onSwitchTo, rename: () => {}, archive: onArchive, unarchive: onUnarchive, delete: onDelete, generateTitle: () => {}, initialize: async () => ({ remoteId: data.id, externalId: undefined }), detach: () => {}, }; }, ); // InMemoryThreadList Client export const InMemoryThreadList = resource( (props: InMemoryThreadListProps): ClientOutput<"threads"> => { const { thread: threadFactory, onSwitchToThread, onSwitchToNewThread, } = props; const [mainThreadId, setMainThreadId] = tapState("main"); const [threads, setThreads] = tapState(() => [ { id: "main", title: "Main Thread", status: "regular" }, ]); const handleSwitchToThread = (threadId: string) => { setMainThreadId(threadId); onSwitchToThread?.(threadId); }; const handleArchive = (threadId: string) => { setThreads((prev) => prev.map((t) => t.id === threadId ? { ...t, status: "archived" as const } : t, ), ); }; const handleUnarchive = (threadId: string) => { setThreads((prev) => prev.map((t) => t.id === threadId ? { ...t, status: "regular" as const } : t, ), ); }; const handleDelete = (threadId: string) => { setThreads((prev) => prev.filter((t) => t.id !== threadId)); if (mainThreadId === threadId) { const remaining = threads.filter((t) => t.id !== threadId); setMainThreadId(remaining[0]?.id || "main"); } }; const handleSwitchToNewThread = () => { const newId = `thread-${Date.now()}`; setThreads((prev) => [ ...prev, { id: newId, title: "New Thread", status: "regular" }, ]); setMainThreadId(newId); onSwitchToNewThread?.(); }; const threadListItems = tapClientLookup( () => threads.map((t) => withKey( t.id, ThreadListItemClient({ data: t, onSwitchTo: () => handleSwitchToThread(t.id), onArchive: () => handleArchive(t.id), onUnarchive: () => handleUnarchive(t.id), onDelete: () => handleDelete(t.id), }), ), ), [threads], ); // Create the main thread const mainThreadClient = tapClientResource(threadFactory(mainThreadId)); const state = tapMemo(() => { const regularThreads = threads.filter((t) => t.status === "regular"); const archivedThreads = threads.filter((t) => t.status === "archived"); return { mainThreadId, newThreadId: null, isLoading: false, isLoadingMore: false, hasMore: false, threadIds: regularThreads.map((t) => t.id), archivedThreadIds: archivedThreads.map((t) => t.id), threadItems: threadListItems.state, main: mainThreadClient.state, }; }, [mainThreadId, threads, threadListItems.state, mainThreadClient.state]); return { getState: () => state, switchToThread: handleSwitchToThread, switchToNewThread: handleSwitchToNewThread, getLoadThreadsPromise: () => RESOLVED_PROMISE, reload: () => RESOLVED_PROMISE, loadMore: () => RESOLVED_PROMISE, item: (selector) => { if (selector === "main") { const index = threads.findIndex((t) => t.id === mainThreadId); return threadListItems.get({ index: index === -1 ? 0 : index }); } if ("id" in selector) { const index = threads.findIndex((t) => t.id === selector.id); return threadListItems.get({ index }); } return threadListItems.get(selector); }, thread: () => mainThreadClient.methods, }; }, ); attachTransformScopes(InMemoryThreadList, (scopes, parent) => { scopes.thread ??= Derived({ source: "threads", query: { type: "main" }, get: (aui) => aui.threads().thread("main"), }); scopes.threadListItem ??= Derived({ source: "threads", query: { type: "main" }, get: (aui) => aui.threads().item("main"), }); scopes.composer ??= Derived({ source: "thread", query: {}, get: (aui) => aui.threads().thread("main").composer(), }); if (!scopes.modelContext && parent.modelContext.source === null) { scopes.modelContext = ModelContext(); } if (!scopes.tools && parent.tools.source === null) { scopes.tools = Tools({}); } if (!scopes.dataRenderers && parent.dataRenderers.source === null) { scopes.dataRenderers = DataRenderers(); } if (!scopes.suggestions && parent.suggestions.source === null) { scopes.suggestions = Suggestions(); } });