From 7cc7f5fd26226e68fe7e91f27da81e40bbc5612b Mon Sep 17 00:00:00 2001 From: Blizzard Date: Fri, 26 Jun 2026 16:56:52 +0800 Subject: [PATCH] =?UTF-8?q?refactor(desktop):=20=E8=A7=82=E6=B5=8B?= =?UTF-8?q?=E6=94=B6=E6=95=9B=E8=BF=9B=E3=80=8C=E8=BF=90=E8=A1=8C=E3=80=8D?= =?UTF-8?q?=E9=A1=B5=EF=BC=88tab=20=E5=88=87=E6=8D=A2=EF=BC=89=EF=BC=8C?= =?UTF-8?q?=E5=88=A0=E5=85=A8=E5=B1=80=E5=BA=95=E9=83=A8=E6=8A=BD=E5=B1=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 底部抽屉与新版运行页重复,且默认展开常驻占 ~176px、引用/评测还是空壳。按高内聚收敛: - 运行·观测 详情区改 tab 切换:执行轨迹 / 模型输出 / 工具调用 / 评测(去掉没接的「引用」空标签); OutputView(含 chart 渲染) + ToolCalls 从抽屉并入运行页;评测标签接 taskEval 全量展示。 - 删除全局 BottomDrawer,释放底部空间。 - HITL 审批条抽成独立 shell/ApprovalBar,全局常驻于 TopBar 下(审批中断必须随处可见可操作)。 - 发起运行自动跳「运行」页 + 新运行自动切回「当前运行」,实时观测不丢。 tsc+vite 构建通过;wails HMR 热加载生效。 Co-Authored-By: Claude Opus 4.8 (1M context) --- sundynix-desktop/frontend/src/App.tsx | 5 +- .../frontend/src/shell/ApprovalBar.tsx | 63 +++++++ .../frontend/src/shell/BottomDrawer.tsx | 175 ------------------ .../frontend/src/views/RunsView.tsx | 144 ++++++++++---- 4 files changed, 176 insertions(+), 211 deletions(-) create mode 100644 sundynix-desktop/frontend/src/shell/ApprovalBar.tsx delete mode 100644 sundynix-desktop/frontend/src/shell/BottomDrawer.tsx diff --git a/sundynix-desktop/frontend/src/App.tsx b/sundynix-desktop/frontend/src/App.tsx index 0c550de..91d84b3 100644 --- a/sundynix-desktop/frontend/src/App.tsx +++ b/sundynix-desktop/frontend/src/App.tsx @@ -3,7 +3,7 @@ import { LayoutDashboard, Workflow, Database, FileText, Activity, Bookmark, Boxe import { TopBar } from "./shell/TopBar"; import { LeftNav, type ViewKey } from "./shell/LeftNav"; -import { BottomDrawer } from "./shell/BottomDrawer"; +import { ApprovalBar } from "./shell/ApprovalBar"; import { StudioView } from "./studio/StudioView"; import { MemoryView } from "./views/MemoryView"; import { KbView } from "./views/KbView"; @@ -108,6 +108,7 @@ export default function App() { stopPoll(); const t0 = Date.now(); setRun({ phase: "submitting", output: "", events: [{ t: 0, label: "提交任务" }], exec: [] }); + setView("runs"); // 发起即跳「运行 · 观测」,实时看轨迹/输出(观测统一收敛在此页) try { const taskId = await submitTask(dsl, identity); let first = true; @@ -180,6 +181,7 @@ export default function App() { /> setCmdOpen(true)} /> +
@@ -200,7 +202,6 @@ export default function App() { )}
- setCmdOpen(false)} commands={commands} /> diff --git a/sundynix-desktop/frontend/src/shell/ApprovalBar.tsx b/sundynix-desktop/frontend/src/shell/ApprovalBar.tsx new file mode 100644 index 0000000..7d62eb0 --- /dev/null +++ b/sundynix-desktop/frontend/src/shell/ApprovalBar.tsx @@ -0,0 +1,63 @@ +import { useState } from "react"; +import { ShieldCheck, Check, X } from "lucide-react"; +import { pendingApproval, type RunState } from "../lib/run"; +import { approveTask } from "../lib/api"; +import { Badge } from "../ui"; + +// ApprovalBar:HITL 人工审批中断条 —— 全局常驻(任务停在审批节点时,不管在哪个页面都要能看见+操作)。 +// 触发以「后端状态 waiting」为准(落 PG,可靠);exec 的 await 事件仅用于丰富摘要(可能抢跑丢失)。 +// 决定经 approveTask 发回;dispatcher 续跑后状态轮询/SSE 更新,本条随 waiting 解除自动消失。 +export function ApprovalBar({ run }: { run: RunState }) { + const approval = + run.taskId && run.lifecycle === "waiting" + ? pendingApproval(run.exec) ?? { node: "", title: run.detail || "人工审批", summary: "" } + : null; + if (!approval || !run.taskId) return null; + return ; +} + +function Bar({ taskId, node, title, summary }: { taskId: string; node: string; title: string; summary: string }) { + const [note, setNote] = useState(""); + const [busy, setBusy] = useState<"approve" | "reject" | null>(null); + const [err, setErr] = useState(""); + + const decide = async (approved: boolean) => { + setBusy(approved ? "approve" : "reject"); + setErr(""); + try { + await approveTask(taskId, approved, { node, note }); + } catch (e) { + setErr((e as Error).message); + setBusy(null); + } + }; + + return ( +
+
+ + 人工审批 + {title} + 等待决定 +
+ {summary &&
{summary}
} +
+ setNote(e.target.value)} + placeholder="备注(可选,拒绝原因等)" + className="min-w-0 flex-1 rounded border border-line bg-ink-950/60 px-2 py-1 text-[11px] text-slate-200 placeholder:text-slate-600 focus:border-amber-500/50 focus:outline-none" + /> + + +
+ {err &&

提交失败:{err}

} +
+ ); +} diff --git a/sundynix-desktop/frontend/src/shell/BottomDrawer.tsx b/sundynix-desktop/frontend/src/shell/BottomDrawer.tsx deleted file mode 100644 index be91343..0000000 --- a/sundynix-desktop/frontend/src/shell/BottomDrawer.tsx +++ /dev/null @@ -1,175 +0,0 @@ -import { useState } from "react"; -import { ChevronDown, ChevronUp, Wrench, ShieldCheck, Check, X } from "lucide-react"; -import { deriveNodes, pendingApproval, type RunState } from "../lib/run"; -import { ExecTrace } from "../components/ExecTrace"; -import { ChartView } from "../components/ChartView"; -import { extractChartBlocks, hasChart } from "../lib/chartspec"; -import { approveTask } from "../lib/api"; -import { Tabs, Badge, cn, type TabDef } from "../ui"; - -type Tab = "output" | "trace" | "tools" | "cite" | "eval"; - -// 底部抽屉:运行输出 / 轨迹 / 工具调用 / 引用 / 评测(深色,全局常驻)。 -export function BottomDrawer({ run }: { run: RunState }) { - const [open, setOpen] = useState(true); - const [tab, setTab] = useState("output"); - - const nodes = deriveNodes(run.exec); - const toolCount = nodes.filter((n) => n.kind === "tool").length; - const tabs: TabDef[] = [ - { key: "output", label: "输出" }, - { key: "trace", label: "轨迹", count: nodes.length }, - { key: "tools", label: "工具调用", count: toolCount }, - { key: "cite", label: "引用" }, - { key: "eval", label: "评测" }, - ]; - - const statusCls = - run.phase === "streaming" ? "text-accent-400" : run.phase === "done" ? "text-success" : run.phase === "error" ? "text-danger" : "text-slate-500"; - const statusText = - run.phase === "streaming" ? "流式中…" : run.phase === "done" ? "完成 ✓" : run.phase === "error" ? `✗ ${run.error ?? "出错"}` : run.phase === "submitting" ? "提交中…" : "就绪"; - - // 审批条触发以「后端状态 waiting」为准(可靠,落 PG);exec 的 await 事件仅用于丰富摘要(可能抢跑丢失)。 - const approval = - run.taskId && run.lifecycle === "waiting" - ? pendingApproval(run.exec) ?? { node: "", title: run.detail || "人工审批", summary: "" } - : null; - - return ( -
- {approval && run.taskId && } -
- { - setTab(t); - setOpen(true); - }} - /> - {statusText} - -
- {open && ( -
- {tab === "output" && } - {tab === "trace" && } - {tab === "tools" && } - {tab === "cite" &&

引用列表:RAG 答案的来源块(源文档 + 分数 + 来源徽标)。

} - {tab === "eval" &&

评测:忠实度 / 完整度质量门结果(需 harness eval)。

} -
- )} -
- ); -} - -// ApprovalBar:HITL 人工审批中断条。任务停在审批节点时常驻顶部,展示待审摘要 + 批准/拒绝。 -// 决定经 approveTask 发回;dispatcher 续跑后 SSE 会推来 end/error 事件,本条随 pendingApproval 归 null 自动消失。 -function ApprovalBar({ taskId, node, title, summary }: { taskId: string; node: string; title: string; summary: string }) { - const [note, setNote] = useState(""); - const [busy, setBusy] = useState<"approve" | "reject" | null>(null); - const [err, setErr] = useState(""); - - const decide = async (approved: boolean) => { - setBusy(approved ? "approve" : "reject"); - setErr(""); - try { - await approveTask(taskId, approved, { node, note }); - } catch (e) { - setErr((e as Error).message); - setBusy(null); - } - }; - - return ( -
-
- - 人工审批 - {title} - 等待决定 -
- {summary &&
{summary}
} -
- setNote(e.target.value)} - placeholder="备注(可选,拒绝原因等)" - className="min-w-0 flex-1 rounded border border-line bg-ink-950/60 px-2 py-1 text-[11px] text-slate-200 placeholder:text-slate-600 focus:border-amber-500/50 focus:outline-none" - /> - - -
- {err &&

提交失败:{err}

} -
- ); -} - -// OutputView:渲染模型输出。含 ```chart 块时分段渲染(文本 + SVG 图表),否则纯文本。 -function OutputView({ output }: { output: string }) { - if (!output) { - return ( -
-        在编排页搭图 → 运行,模型注入画像与历史后流式作答,token 在此呈现。
-      
- ); - } - if (!hasChart(output)) { - return
{output}
; - } - return ( -
- {extractChartBlocks(output).map((seg, i) => - seg.kind === "chart" ? ( - - ) : ( - seg.text.trim() && ( -
-              {seg.text}
-            
- ) - ), - )} -
- ); -} - -// ToolCalls:从执行事件里筛出工具调用节点,逐条展示入参 → 产出 + 耗时/状态。 -function ToolCalls({ run }: { run: RunState }) { - const tools = deriveNodes(run.exec).filter((n) => n.kind === "tool"); - if (tools.length === 0) { - return

本次运行暂无工具调用。图里挂了检索/工具节点,或报告挂了知识库时,每次 sundynix.tools.* 调用会在此列出入参与产出。

; - } - return ( -
    - {tools.map((t) => ( -
  • -
    - - {t.node.replace(/^tool:/, "")} - - {t.status === "error" ? "失败" : t.status === "running" ? "调用中" : "成功"} - - {t.ms != null && t.ms > 0 && {t.ms} ms} -
    - {t.detail &&

    {t.detail}

    } -
  • - ))} -
- ); -} diff --git a/sundynix-desktop/frontend/src/views/RunsView.tsx b/sundynix-desktop/frontend/src/views/RunsView.tsx index c32026f..f7ce34b 100644 --- a/sundynix-desktop/frontend/src/views/RunsView.tsx +++ b/sundynix-desktop/frontend/src/views/RunsView.tsx @@ -1,10 +1,13 @@ import { useCallback, useEffect, useRef, useState } from "react"; -import { Activity, FileText, History } from "lucide-react"; +import { Activity, History, Wrench } from "lucide-react"; import { ExecTrace } from "../components/ExecTrace"; -import { Markdown } from "../components/Markdown"; +import { ChartView } from "../components/ChartView"; +import { extractChartBlocks, hasChart } from "../lib/chartspec"; import { deriveNodes, emptyRun, type RunState } from "../lib/run"; import { listRuns, taskEval, streamExec, streamTokens, type RunSummary, type EvalResult } from "../lib/api"; -import { Panel, Dot, Badge, EmptyState, cn } from "../ui"; +import { Tabs, Dot, Badge, EmptyState, cn, type TabDef } from "../ui"; + +type DetailTab = "trace" | "output" | "tools" | "eval"; const STATUS_DOT: Record = { done: "success", failed: "danger", timeout: "danger", rejected: "danger", @@ -20,16 +23,16 @@ function relTime(iso: string): string { return `${Math.floor(d / 86_400_000)} 天前`; } -// 运行 · 观测:左侧运行历史,选中后从 Redis 回放该次执行轨迹 + 输出 + 评测(复盘)。 -// 「当前运行」固定在顶部,沿用实时订阅;历史项点击即复现(exec/token 流对完成任务回放)。 +// 运行 · 观测:左侧运行历史,右侧 tab 切换查看选中运行的 轨迹/输出/工具调用/评测(复盘)。 +// 选中历史运行后从 Redis 回放(exec/token 流对完成任务即回放);「当前运行」置顶沿用实时订阅。 export function RunsView({ run }: { run: RunState }) { const [runs, setRuns] = useState([]); const [sel, setSel] = useState(null); // null = 当前实时运行 const [replay, setReplay] = useState(emptyRun); const [evalRes, setEvalRes] = useState(null); + const [tab, setTab] = useState("trace"); const closeRef = useRef<(() => void) | null>(null); - // 拉运行历史(轮询刷新,捕获新完成的运行)。 useEffect(() => { let alive = true; const load = () => listRuns(40).then((r) => alive && setRuns(r)).catch(() => {}); @@ -38,10 +41,9 @@ export function RunsView({ run }: { run: RunState }) { return () => { alive = false; clearInterval(id); closeRef.current?.(); }; }, []); - // 选中历史运行:从 Redis 回放轨迹 + token + 取评测。 const selectRun = useCallback((taskId: string) => { closeRef.current?.(); - if (taskId === run.taskId) { setSel(null); setEvalRes(null); return; } // 即当前实时运行 + if (taskId === run.taskId) { setSel(null); setEvalRes(null); return; } setSel(taskId); setReplay({ phase: "streaming", taskId, output: "", events: [], exec: [] }); setEvalRes(null); @@ -51,22 +53,39 @@ export function RunsView({ run }: { run: RunState }) { taskEval(taskId).then(setEvalRes).catch(() => {}); }, [run.taskId]); + // 新的实时运行开始 → 自动切回「当前运行」(若此前选着历史项),并回到轨迹标签。 + useEffect(() => { + if (run.taskId) { closeRef.current?.(); setSel(null); setEvalRes(null); setTab("trace"); } + }, [run.taskId]); + + // 当前实时运行的评测:完成后拉一次。 + useEffect(() => { + if (!sel && run.taskId && run.phase === "done") taskEval(run.taskId).then(setEvalRes).catch(() => {}); + }, [sel, run.taskId, run.phase]); + const liveActive = run.taskId && run.phase !== "idle"; const cur = sel ? replay : run; const nodes = deriveNodes(cur.exec); const tools = nodes.filter((n) => n.kind === "tool"); + const tabs: TabDef[] = [ + { key: "trace", label: "执行轨迹", count: nodes.length }, + { key: "output", label: "模型输出" }, + { key: "tools", label: "工具调用", count: tools.length }, + { key: "eval", label: "评测" }, + ]; + const empty = !cur.taskId && cur.exec.length === 0 && !cur.output; return (

运行 · 观测

-

运行历史复盘:选一次运行,从持久化流回放节点轨迹、模型输出与评测。

+

运行历史复盘:选一次运行,切换标签查看节点轨迹、模型输出、工具调用与评测。

{nodes.length} 节点 · {tools.length} 次工具调用
-
+
{/* 运行历史列表 */}
@@ -96,34 +115,91 @@ export function RunsView({ run }: { run: RunState }) {
- {/* 执行轨迹 */} - - - - - {/* 模型输出 + 评测 */} -
- - {cur.output ? ( - - ) : ( + {/* 详情:tab 切换 轨迹/输出/工具/评测 */} +
+
+ +
+
+ {empty ? ( + ) : tab === "trace" ? ( + + ) : tab === "output" ? ( + + ) : tab === "tools" ? ( + + ) : ( + )} - - {evalRes && ( -
-
- 评测 - {evalRes.level} · {evalRes.overall.toFixed(2)} - {evalRes.corrected && 已纠偏} - {evalRes.sources > 0 && 忠实 {evalRes.faithful.toFixed(2)} · {evalRes.sources} 来源} -
- {evalRes.reason &&

{evalRes.reason}

} - {evalRes.flags?.length > 0 &&

{evalRes.flags.join(";")}

} -
- )} +
); } + +// OutputView:渲染模型输出。含 ```chart 块时分段渲染(文本 + SVG 图表),否则纯文本。 +function OutputView({ output }: { output: string }) { + if (!output) { + return

本次运行暂无输出(或尚未产出 token)。

; + } + if (!hasChart(output)) { + return
{output}
; + } + return ( +
+ {extractChartBlocks(output).map((seg, i) => + seg.kind === "chart" ? ( + + ) : ( + seg.text.trim() && ( +
{seg.text}
+ ) + ), + )} +
+ ); +} + +// ToolCalls:从执行事件筛出工具调用节点,逐条展示入参 → 产出 + 耗时/状态。 +function ToolCalls({ run }: { run: RunState }) { + const tools = deriveNodes(run.exec).filter((n) => n.kind === "tool"); + if (tools.length === 0) { + return

本次运行暂无工具调用。图里挂了检索/工具节点,或报告挂了知识库时,每次工具调用会在此列出入参与产出。

; + } + return ( +
    + {tools.map((t) => ( +
  • +
    + + {t.node.replace(/^tool:/, "")} + + {t.status === "error" ? "失败" : t.status === "running" ? "调用中" : "成功"} + + {t.ms != null && t.ms > 0 && {t.ms} ms} +
    + {t.detail &&

    {t.detail}

    } +
  • + ))} +
+ ); +} + +// EvalView:本次运行的自动化评测(分级/综合/忠实度/纠偏/评语/flags)。 +function EvalView({ ev }: { ev: EvalResult | null }) { + if (!ev) return

暂无评测结果(任务未完成或评测进行中)。

; + return ( +
+
+ {ev.level} · {ev.overall.toFixed(2)} + {ev.corrected && 已纠偏} + 规则 {ev.rule.toFixed(2)} · 质量 {ev.llm.toFixed(2)} + {ev.sources > 0 && 忠实 {ev.faithful.toFixed(2)} · {ev.sources} 来源} +
+ {ev.reason &&

{ev.reason}

} + {ev.flags?.length > 0 &&

{ev.flags.join(";")}

} +
+ ); +}