great-agent/apps/web/src/features/conversations/conversation-pane.ts
2026-08-14 14:34:27 +08:00

146 lines
4.4 KiB
TypeScript

import van, { type State } from "vanjs-core";
import type { AppController } from "../../app/app-controller";
import "./conversation-pane.css";
import { ProjectPanel } from "../projects/project-panel";
const { article, button, div, h1, header, main, p, span, textarea } = van.tags;
export function ConversationPane(controller: AppController): HTMLElement {
const draft = van.state("");
async function submit() {
const content = draft.val;
if (!content.trim()) return;
await controller.send(content);
draft.val = "";
}
return main(
{ class: "main-pane" },
header({ class: "mobile-header" }, "Great Agent 2"),
div({ class: "pane-content" }, () =>
controller.selection.val.kind.startsWith("project-")
? ProjectPanel(controller)
: controller.selection.val.kind === "conversation" &&
controller.active.val
? div(
{ class: "conversation-view" },
h1({ class: "conversation-title" }, controller.active.val.title),
div(
{ class: "message-list" },
controller.active.val.messages.map((message) =>
article(
{ class: `message ${message.role}` },
span(
{ class: `message-role ${message.role}-role` },
message.role === "user" ? "你" : "G",
),
p(message.content),
),
),
() =>
controller.assistantDraft.val
? article(
{ class: "message assistant streaming" },
span({ class: "message-role assistant-role" }, "G"),
p(controller.assistantDraft),
)
: null,
),
)
: div(
{ class: "onboarding" },
h1(span({ class: "spark" }, "✳"), " 今天想完成什么?"),
p("从一个问题、想法或具体任务开始。"),
),
),
div(
{ class: "composer-slot", hidden: () => !canCompose(controller) },
Composer(controller, draft, submit),
),
);
}
function Composer(
controller: AppController,
draft: State<string>,
submit: () => Promise<void>,
): HTMLElement {
return div(
{ class: "composer-wrap" },
div(
{
class: "error-banner",
role: "alert",
hidden: () => !controller.error.val,
},
() => controller.error.val,
),
div(
{ class: "composer" },
textarea({
"aria-label": "输入消息",
placeholder: "我能帮你做些什么?",
value: draft,
disabled: controller.sending,
oninput: (event: Event) => {
draft.val = (event.target as HTMLTextAreaElement).value;
},
onkeydown: (event: KeyboardEvent) => {
if (event.key === "Enter" && !event.shiftKey) {
event.preventDefault();
void submit();
}
},
}),
div(
{ class: "composer-actions" },
button(
{ class: "attach", disabled: true, "aria-label": "添加附件" },
"+",
),
button(
{
class: "send",
disabled: () => controller.sending.val || !draft.val.trim(),
onclick: () => void submit(),
"aria-label": "发送消息",
},
() => (controller.sending.val ? "…" : "↑"),
),
),
),
div(
{ class: "composer-meta" },
button({ disabled: true }, () =>
isProjectConversation(controller) ? "项目对话" : "普通对话",
),
span("DeepSeek"),
),
);
}
function isProjectConversation(controller: AppController): boolean {
const selection = controller.selection.val;
return (
selection.kind === "project-draft" ||
(selection.kind === "conversation" &&
controller.active.val?.projectId !== null)
);
}
function canCompose(controller: AppController): boolean {
const selection = controller.selection.val;
if (selection.kind === "conversation") return true;
if (selection.kind === "ordinary-draft") return true;
if (selection.kind === "project-draft") {
return (
controller.projects.val.find(
(project) => project.id === selection.projectId,
)?.workspaceAvailable ?? false
);
}
return selection.kind === "none";
}