feat(desktop): MVP 驾驶舱外壳 + 类型化节点 Studio + 运行抽屉

按 desktop-ui-plan.md 落 MVP:五区外壳 + 编排 Studio + 底部抽屉 + 健康灯。

- shell: TopBar(垂直切换/健康灯[Gateway/DB 实时,余规划]/身份会话) +
  LeftNav(BUILD/RUN/MANAGE 分组,未就绪模块灰显) + BottomDrawer(输出/轨迹/工具调用/引用/评测)
- studio: 类型化节点目录(输入/检索RAG/Agent/工具/记忆/分支/并行/汇聚/渲染/输出,
  按类配色) + 自定义 TypedNode(状态徽标) + Inspector(按类型渲染配置表单) +
  校验(孤立节点/必填项) + 运行
- views: MemoryView(复用偏好面板) + Placeholder(规划中模块,露出 IA 与依赖)
- lib: run(运行状态机) + health(轮询 billing) + dsl(导出类型化 DSL + validate)
- 删旧 AgentCanvas(被 StudioView 取代)

验证: npm run build(tsc+vite)✓; 真实浏览器跑通——加类型化节点→校验(标出孤立)→运行
→SSE 注入画像(老王)+历史 流入抽屉, 健康灯 Gateway/DB 实时绿

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Blizzard
2026-06-10 15:00:32 +08:00
parent a8e210cd96
commit 61c1177eba
14 changed files with 850 additions and 146 deletions
+56 -60
View File
@@ -1,87 +1,83 @@
import { useCallback, useRef, useState } from "react";
import { AgentCanvas } from "./canvas/AgentCanvas";
import { MemoryPanel } from "./panels/MemoryPanel";
import { TopBar } from "./shell/TopBar";
import { LeftNav, type ViewKey } from "./shell/LeftNav";
import { BottomDrawer } from "./shell/BottomDrawer";
import { StudioView } from "./studio/StudioView";
import { MemoryView } from "./views/MemoryView";
import { Placeholder } from "./views/Placeholder";
import { submitTask, streamTokens, type Identity } from "./lib/api";
import type { TaskDsl } from "./lib/dsl";
import { emptyRun, type RunState } from "./lib/run";
const PLACEHOLDERS: Partial<Record<ViewKey, { title: string; desc: string }>> = {
home: { title: "工作台", desc: "概览:知识库 / 文档 / 近期运行 / 待办报告 / 配额计费 + 快捷入口。" },
kb: { title: "知识库 (RAG)", desc: "入库流水线监控 · 检索调试台(带来源徽标) · 文档/块浏览 · 知识图谱 · 检索评测。依赖 embedding + 入库 worker + 真实混合检索。" },
report: { title: "报告生成", desc: "模板库 · 大纲编辑 · 章节并行生成进度 · 实时预览(含引用) · 导出 docx/pdf。依赖 RAG 核心链 + UniOffice。" },
runs: { title: "运行 · 观测", desc: "实时执行 · 节点轨迹 · 工具调用 · 运行历史复盘。当前运行结果见底部抽屉。" },
market: { title: "市场 · Packs", desc: "垂直包(法律/医疗/金融) · Agent 模板 · 开通向导(建租户→入库→注册模板→应用配置)。依赖多租户 + Pack 格式。" },
admin: { title: "管理", desc: "租户/工作区 · 用户计费 · 护栏 · 模型与连接 · 设置。" },
};
// 顶层布局:左侧 React Flow 编排画布 + 右侧 身份 / 偏好记忆 / 运行输出。
export default function App() {
const [view, setView] = useState<ViewKey>("studio");
const [identity, setIdentity] = useState<Identity>({ userId: "wt", sessionId: "sess-ui" });
const [output, setOutput] = useState("");
const [status, setStatus] = useState("就绪");
const [running, setRunning] = useState(false);
const [run, setRun] = useState<RunState>(emptyRun);
const closeRef = useRef<(() => void) | null>(null);
const onRun = useCallback(
async (dsl: TaskDsl) => {
closeRef.current?.();
setOutput("");
setRunning(true);
setStatus("提交任务…");
const t0 = Date.now();
setRun({ phase: "submitting", output: "", events: [{ t: 0, label: "提交任务" }] });
try {
const taskId = await submitTask(dsl, identity);
setStatus(`流式中 · ${taskId}`);
let first = true;
setRun((r) => ({
...r,
phase: "streaming",
taskId,
events: [...r.events, { t: Date.now() - t0, label: `已发布 ${taskId}` }],
}));
closeRef.current = streamTokens(
taskId,
(t) => setOutput((o) => o + t),
() => {
setStatus("完成 ✓");
setRunning(false);
},
() => {
setStatus("连接中断");
setRunning(false);
},
(tok) =>
setRun((r) => {
const ev = first ? [...r.events, { t: Date.now() - t0, label: "首 token" }] : r.events;
first = false;
return { ...r, output: r.output + tok, events: ev };
}),
() =>
setRun((r) => ({
...r,
phase: "done",
events: [...r.events, { t: Date.now() - t0, label: "完成" }],
})),
() => setRun((r) => ({ ...r, phase: "error", error: "连接中断" })),
);
} catch (e) {
setStatus(`${(e as Error).message}`);
setRunning(false);
setRun((r) => ({ ...r, phase: "error", error: (e as Error).message }));
}
},
[identity],
);
return (
<div className="flex h-screen w-screen text-gray-900">
<main className="flex-1 border-r">
<AgentCanvas onRun={onRun} running={running} />
</main>
<aside className="flex w-[26rem] flex-col overflow-auto">
<section className="border-b p-4">
<h2 className="mb-2 text-sm font-semibold text-gray-700"> / </h2>
<div className="flex gap-2">
<label className="flex-1 text-xs text-gray-500">
<input
className="mt-1 w-full rounded border px-2 py-1 text-sm text-gray-900"
value={identity.userId}
onChange={(e) => setIdentity((id) => ({ ...id, userId: e.target.value }))}
/>
</label>
<label className="flex-1 text-xs text-gray-500">
<input
className="mt-1 w-full rounded border px-2 py-1 text-sm text-gray-900"
value={identity.sessionId}
onChange={(e) => setIdentity((id) => ({ ...id, sessionId: e.target.value }))}
/>
</label>
</div>
</section>
<MemoryPanel identity={identity} />
<section className="flex flex-1 flex-col p-4">
<div className="mb-2 flex items-center justify-between">
<h2 className="text-sm font-semibold text-gray-700">SSE Token </h2>
<span className="text-xs text-gray-400">{status}</span>
</div>
<pre className="flex-1 whitespace-pre-wrap rounded bg-gray-900 p-3 text-xs leading-relaxed text-emerald-300">
{output || "在左侧加节点 → 运行,模型会注入你的偏好与历史后流式作答。"}
</pre>
</section>
</aside>
<div className="flex h-screen w-screen flex-col text-gray-900">
<TopBar identity={identity} setIdentity={setIdentity} />
<div className="flex min-h-0 flex-1">
<LeftNav active={view} onSelect={setView} />
<main className="min-w-0 flex-1 overflow-hidden">
{view === "studio" ? (
<StudioView onRun={onRun} phase={run.phase} />
) : view === "memory" ? (
<MemoryView identity={identity} />
) : (
<Placeholder {...(PLACEHOLDERS[view] ?? { title: "模块", desc: "规划中。" })} />
)}
</main>
</div>
<BottomDrawer run={run} />
</div>
);
}