import van, { type State } from "vanjs-core"; import type { ConversationResponse, ConversationSummaryResponse, ProjectResponse, InteractionAnswerRequest, InteractionResponse, } from "@great-agent/web-contracts"; import { getConversation, listConversations, listProjectConversations, } from "../api/conversations"; import { createProject, deleteProject, listProjects, renameProject, } from "../api/projects"; import { startRun, streamRun } from "../api/runs"; import { answerInteraction as submitInteractionAnswer, cancelWaitingRun, listInteractions, } from "../api/interactions"; export type Selection = | { kind: "none" } | { kind: "ordinary-draft" } | { kind: "project-create" } | { kind: "project-draft"; projectId: string } | { kind: "project-rename"; projectId: string } | { kind: "project-delete"; projectId: string } | { kind: "conversation"; id: string }; export type AppController = Readonly<{ selection: State; recent: State; projects: State; projectConversations: State>; active: State; interactions: State; loading: State; sending: State; assistantDraft: State; error: State; initialize(): Promise; startNewTask(): void; startProjectCreation(): void; createProject(name: string, workspaceRoot: string): Promise; openProject(id: string): Promise; startProjectTask(id: string): void; startProjectRename(id: string): void; renameProject(id: string, name: string): Promise; startProjectDelete(id: string): void; deleteProject(id: string): Promise; openConversation(id: string): Promise; send(content: string): Promise; answerInteraction( id: string, answer: InteractionAnswerRequest, ): Promise; cancelInteraction(runId: string): Promise; }>; export function createAppController(): AppController { const selection = van.state({ kind: "none" }); const recent = van.state([]); const projects = van.state([]); const projectConversations = van.state< Record >({}); const active = van.state(null); const interactions = van.state([]); const loading = van.state(false); const sending = van.state(false); const assistantDraft = van.state(""); const error = van.state(""); async function initialize() { loading.val = true; error.val = ""; try { [recent.val, projects.val] = await Promise.all([ listConversations(), listProjects(), ]); } catch (cause) { error.val = readMessage(cause); } finally { loading.val = false; } } function select(next: Selection) { selection.val = next; active.val = null; interactions.val = []; error.val = ""; } function startNewTask() { select({ kind: "ordinary-draft" }); } function startProjectCreation() { select({ kind: "project-create" }); } function startProjectTask(projectId: string) { select({ kind: "project-draft", projectId }); } function startProjectRename(projectId: string) { select({ kind: "project-rename", projectId }); } function startProjectDelete(projectId: string) { select({ kind: "project-delete", projectId }); } async function createNewProject(name: string, workspaceRoot: string) { await perform(async () => { const created = await createProject(name, workspaceRoot); projects.val = [created, ...projects.val]; projectConversations.val = { ...projectConversations.val, [created.id]: [], }; select({ kind: "project-draft", projectId: created.id }); }); } async function openProject(id: string) { await perform(async () => { const conversations = await listProjectConversations(id); projectConversations.val = { ...projectConversations.val, [id]: conversations, }; if (conversations[0]) await openConversation(conversations[0].id); else select({ kind: "project-draft", projectId: id }); }); } async function renameExistingProject(id: string, name: string) { await perform(async () => { const updated = await renameProject(id, name); projects.val = projects.val.map((project) => project.id === id ? updated : project, ); select({ kind: "project-draft", projectId: id }); }); } async function deleteExistingProject(id: string) { await perform(async () => { await deleteProject(id); projects.val = projects.val.filter((project) => project.id !== id); const next = { ...projectConversations.val }; delete next[id]; projectConversations.val = next; select({ kind: "none" }); }); } async function openConversation(id: string) { await perform(async () => { active.val = await getConversation(id); interactions.val = await listInteractions(id); selection.val = { kind: "conversation", id }; }); } async function send(content: string) { if (!content.trim() || sending.val) return; sending.val = true; assistantDraft.val = ""; error.val = ""; try { const current = selection.val; const input = current.kind === "conversation" ? { kind: "existing" as const, conversationId: current.id, message: content, } : current.kind === "project-draft" ? { kind: "project" as const, projectId: current.projectId, message: content, } : { kind: "ordinary" as const, message: content }; const started = await startRun(input); active.val = await getConversation(started.conversationId); selection.val = { kind: "conversation", id: started.conversationId }; await refreshLists(active.val.projectId); await streamRun(started.runId, collectDelta); await refreshActive(started.conversationId); } catch (cause) { error.val = readMessage(cause); } finally { sending.val = false; } } async function answerInteraction( id: string, answer: InteractionAnswerRequest, ) { if (sending.val) return; sending.val = true; assistantDraft.val = ""; error.val = ""; try { const resolved = await submitInteractionAnswer(id, answer); interactions.val = interactions.val.map((item) => item.id === id ? resolved.interaction : item, ); if (resolved.resumed) { await streamRun( resolved.interaction.runId, collectDelta, resolved.resumeFromSequence, ); } await refreshActive(resolved.interaction.conversationId); } catch (cause) { error.val = readMessage(cause); } finally { sending.val = false; } } async function cancelInteraction(runId: string) { if (sending.val) return; sending.val = true; error.val = ""; try { await cancelWaitingRun(runId); if (active.val) interactions.val = await listInteractions(active.val.id); } catch (cause) { error.val = readMessage(cause); } finally { sending.val = false; } } function collectDelta(event: { type: string; payload: Record; }) { if ( event.type === "message.delta" && typeof event.payload.delta === "string" ) assistantDraft.val += event.payload.delta; } async function refreshActive(conversationId: string) { [active.val, interactions.val] = await Promise.all([ getConversation(conversationId), listInteractions(conversationId), ]); assistantDraft.val = ""; } async function refreshLists(projectId: string | null) { if (!projectId) recent.val = await listConversations(); else { const conversations = await listProjectConversations(projectId); projectConversations.val = { ...projectConversations.val, [projectId]: conversations, }; projects.val = await listProjects(); } } async function perform(action: () => Promise) { loading.val = true; error.val = ""; try { await action(); } catch (cause) { error.val = readMessage(cause); } finally { loading.val = false; } } return { selection, recent, projects, projectConversations, active, interactions, loading, sending, assistantDraft, error, initialize, startNewTask, startProjectCreation, createProject: createNewProject, openProject, startProjectTask, startProjectRename, renameProject: renameExistingProject, startProjectDelete, deleteProject: deleteExistingProject, openConversation, send, answerInteraction, cancelInteraction, }; } function readMessage(cause: unknown): string { return cause instanceof Error ? cause.message : "操作失败,请重试"; }