146 lines
4.4 KiB
TypeScript
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";
|
|
}
|