320 lines
9.0 KiB
TypeScript
320 lines
9.0 KiB
TypeScript
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<Selection>;
|
|
recent: State<ConversationSummaryResponse[]>;
|
|
projects: State<ProjectResponse[]>;
|
|
projectConversations: State<Record<string, ConversationSummaryResponse[]>>;
|
|
active: State<ConversationResponse | null>;
|
|
interactions: State<InteractionResponse[]>;
|
|
loading: State<boolean>;
|
|
sending: State<boolean>;
|
|
assistantDraft: State<string>;
|
|
error: State<string>;
|
|
initialize(): Promise<void>;
|
|
startNewTask(): void;
|
|
startProjectCreation(): void;
|
|
createProject(name: string, workspaceRoot: string): Promise<void>;
|
|
openProject(id: string): Promise<void>;
|
|
startProjectTask(id: string): void;
|
|
startProjectRename(id: string): void;
|
|
renameProject(id: string, name: string): Promise<void>;
|
|
startProjectDelete(id: string): void;
|
|
deleteProject(id: string): Promise<void>;
|
|
openConversation(id: string): Promise<void>;
|
|
send(content: string): Promise<void>;
|
|
answerInteraction(
|
|
id: string,
|
|
answer: InteractionAnswerRequest,
|
|
): Promise<void>;
|
|
cancelInteraction(runId: string): Promise<void>;
|
|
}>;
|
|
|
|
export function createAppController(): AppController {
|
|
const selection = van.state<Selection>({ kind: "none" });
|
|
const recent = van.state<ConversationSummaryResponse[]>([]);
|
|
const projects = van.state<ProjectResponse[]>([]);
|
|
const projectConversations = van.state<
|
|
Record<string, ConversationSummaryResponse[]>
|
|
>({});
|
|
const active = van.state<ConversationResponse | null>(null);
|
|
const interactions = van.state<InteractionResponse[]>([]);
|
|
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<string, unknown>;
|
|
}) {
|
|
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<void>) {
|
|
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 : "操作失败,请重试";
|
|
}
|