feat(desktop): 深色 AI 控制台视觉改造 + 工作台仪表盘

桌面端从扁平 dev 工具风改为深色高端 AI 控制台:分层表面 + 紫青强调 + 微光 +
首页仪表盘,提升第一印象与吸引力。

- 设计 tokens: tailwind ink 分层调色板 + glow/card 阴影; index.css 深色基底 +
  品牌渐变 + 深色滚动条
- shell: TopBar(品牌渐变+毛玻璃+发光健康灯) / LeftNav(激活态紫色高亮+左光条) /
  BottomDrawer(深色+状态色)
- 新 views/Home 工作台仪表盘: 渐变 hero + 4 状态卡 + 3 能力卡 + 快速开始(默认首页)
- 画布: TypedNode 深色节点卡; StudioView ReactFlow colorMode=dark + 深色工具栏/面板/
  MiniMap; Inspector 深色表单
- KbView/MemoryView/MemoryPanel/Placeholder 全深色化; 进度条改紫青渐变
- 纯前端改造, npm build✓; 浏览器验证: 仪表盘 + 编排画布深色呈现

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Blizzard
2026-06-11 16:56:04 +08:00
parent d623b8590e
commit 2291f60380
18 changed files with 327 additions and 207 deletions
@@ -1,7 +1,10 @@
import type { Node } from "@xyflow/react";
import { NODE_KINDS } from "./nodeCatalog";
// 右检查器:按选中节点的类型渲染配置表单;空选时显示图级提示。
const inputCls =
"mt-1 w-full rounded-md border border-line bg-ink-800 px-2 py-1 text-sm text-slate-200 focus:border-violet-500/60 focus:outline-none";
// 右检查器:按选中节点的类型渲染配置表单;空选时显示图级提示(深色)。
export function Inspector({
node,
onChange,
@@ -13,7 +16,7 @@ export function Inspector({
}) {
if (!node) {
return (
<div className="p-4 text-xs text-gray-400">
<div className="p-4 text-xs leading-relaxed text-slate-500">
/
<br />
线
@@ -23,66 +26,49 @@ export function Inspector({
const data = node.data as { kind: string; label?: string; config?: Record<string, unknown> };
const k = NODE_KINDS[data.kind] ?? NODE_KINDS.output;
const config = data.config ?? {};
const setConfig = (key: string, value: unknown) =>
onChange(node.id, { config: { ...config, [key]: value } });
const setConfig = (key: string, value: unknown) => onChange(node.id, { config: { ...config, [key]: value } });
return (
<div className="flex h-full flex-col">
<div className="flex items-center justify-between border-b p-3">
<div className="flex items-center justify-between border-b border-line p-3">
<span className={`rounded px-1.5 py-0.5 text-[11px] font-medium ${k.badge}`}>{k.label}</span>
<button onClick={() => onDelete(node.id)} className="text-xs text-rose-500 hover:underline">
<button onClick={() => onDelete(node.id)} className="text-xs text-rose-400 hover:underline">
</button>
</div>
<div className="flex flex-col gap-3 overflow-auto p-3">
<label className="text-xs text-gray-500">
<label className="text-xs text-slate-500">
<input
className="mt-1 w-full rounded border px-2 py-1 text-sm text-gray-900"
value={data.label ?? ""}
onChange={(e) => onChange(node.id, { label: e.target.value })}
/>
<input className={inputCls} value={data.label ?? ""} onChange={(e) => onChange(node.id, { label: e.target.value })} />
</label>
{k.fields.map((f) => {
const v = config[f.key];
return (
<label key={f.key} className="text-xs text-gray-500">
<label key={f.key} className="text-xs text-slate-500">
{f.label}
{f.required && <span className="text-rose-500"> *</span>}
{f.required && <span className="text-rose-400"> *</span>}
{f.type === "select" ? (
<select
className="mt-1 w-full rounded border px-2 py-1 text-sm text-gray-900"
value={String(v ?? "")}
onChange={(e) => setConfig(f.key, e.target.value)}
>
<select className={inputCls} value={String(v ?? "")} onChange={(e) => setConfig(f.key, e.target.value)}>
{f.options?.map((o) => (
<option key={o}>{o}</option>
))}
</select>
) : f.type === "textarea" ? (
<textarea
className="mt-1 h-16 w-full resize-none rounded border px-2 py-1 text-sm text-gray-900"
className={`${inputCls} h-16 resize-none`}
value={String(v ?? "")}
placeholder={f.placeholder}
onChange={(e) => setConfig(f.key, e.target.value)}
/>
) : f.type === "checkbox" ? (
<input
type="checkbox"
className="ml-2 align-middle"
checked={Boolean(v)}
onChange={(e) => setConfig(f.key, e.target.checked)}
/>
<input type="checkbox" className="ml-2 align-middle accent-violet-500" checked={Boolean(v)} onChange={(e) => setConfig(f.key, e.target.checked)} />
) : (
<input
type={f.type === "number" ? "number" : "text"}
className="mt-1 w-full rounded border px-2 py-1 text-sm text-gray-900"
className={inputCls}
value={String(v ?? "")}
placeholder={f.placeholder}
onChange={(e) =>
setConfig(f.key, f.type === "number" ? Number(e.target.value) : e.target.value)
}
onChange={(e) => setConfig(f.key, f.type === "number" ? Number(e.target.value) : e.target.value)}
/>
)}
</label>
@@ -93,15 +93,15 @@ export function StudioView({
return (
<div className="flex h-full">
{/* 左节点面板 */}
<div className="w-40 shrink-0 overflow-auto border-r bg-gray-50 p-2">
<div className="mb-1 px-1 text-[11px] font-semibold text-gray-500"></div>
<div className="w-40 shrink-0 overflow-auto border-r border-line bg-ink-900 p-2">
<div className="mb-1 px-1 text-[11px] font-semibold text-slate-500"></div>
{NODE_ORDER.map((kind) => {
const k = NODE_KINDS[kind];
return (
<button
key={kind}
onClick={() => addNode(kind)}
className={`mb-1 flex w-full items-center gap-2 rounded border border-l-4 bg-white px-2 py-1.5 text-left text-xs hover:bg-gray-50 ${k.accent}`}
className={`mb-1 flex w-full items-center gap-2 rounded-md border border-l-[3px] border-line bg-ink-800 px-2 py-1.5 text-left text-xs text-slate-300 hover:bg-ink-700 ${k.accent}`}
title={k.desc}
>
{k.label}
@@ -111,36 +111,37 @@ export function StudioView({
</div>
{/* 中画布 */}
<div className="relative flex-1">
<div className="absolute left-0 right-0 top-0 z-10 flex items-center gap-2 border-b bg-white/90 px-2 py-1.5">
<div className="relative flex-1 bg-ink-950">
<div className="absolute left-0 right-0 top-0 z-10 flex items-center gap-2 border-b border-line bg-ink-900/90 px-2 py-1.5 backdrop-blur">
<button
onClick={run}
disabled={running || nodes.length === 0}
className="rounded bg-violet-600 px-3 py-1 text-xs font-medium text-white disabled:opacity-40"
className="rounded-md bg-violet-600 px-3 py-1 text-xs font-medium text-white hover:bg-violet-500 disabled:opacity-40"
>
{running ? "运行中…" : "▶ 运行"}
</button>
<button
onClick={() => setIssues(validate(nodes, edges))}
className="rounded border px-3 py-1 text-xs hover:bg-gray-50"
className="rounded-md border border-line px-3 py-1 text-xs text-slate-300 hover:bg-ink-800"
>
</button>
<span className="ml-1 text-[11px] text-gray-400">
<span className="ml-1 text-[11px] text-slate-500">
{nodes.length} · {edges.length} 线
</span>
{issues && (
<span className="ml-auto text-[11px]">
{issues.length === 0 ? (
<span className="text-emerald-600"> </span>
<span className="text-emerald-400"> </span>
) : (
<span className="text-amber-600">{issues.length} </span>
<span className="text-amber-400">{issues.length} </span>
)}
</span>
)}
</div>
<ReactFlow
colorMode="dark"
nodes={nodes}
edges={edges}
nodeTypes={nodeTypes}
@@ -151,15 +152,15 @@ export function StudioView({
onPaneClick={() => setSelId(null)}
fitView
>
<Background />
<Background color="#242b3c" gap={18} />
<Controls />
<MiniMap zoomable pannable className="!bg-white" />
<MiniMap zoomable pannable className="!bg-ink-850" />
</ReactFlow>
{issues && issues.length > 0 && (
<div className="absolute bottom-2 left-2 max-w-md rounded border bg-white p-2 text-[11px] shadow">
<div className="absolute bottom-2 left-2 max-w-md rounded-lg border border-line bg-ink-850 p-2 text-[11px] shadow-card">
{issues.map((i, idx) => (
<div key={idx} className={i.level === "error" ? "text-rose-600" : "text-amber-600"}>
<div key={idx} className={i.level === "error" ? "text-rose-400" : "text-amber-400"}>
{i.level === "error" ? "✗" : "⚠"} {i.msg}
</div>
))}
@@ -168,7 +169,7 @@ export function StudioView({
</div>
{/* 右检查器 */}
<div className="w-72 shrink-0 border-l bg-white">
<div className="w-72 shrink-0 border-l border-line bg-ink-900">
<Inspector node={selected} onChange={patchNode} onDelete={deleteNode} />
</div>
</div>
@@ -19,20 +19,20 @@ export function TypedNode({ data, selected }: NodeProps) {
return (
<div
className={`min-w-[160px] rounded-md border border-l-4 bg-white shadow-sm ${k.accent} ${
selected ? "ring-2 ring-violet-400" : ""
className={`min-w-[170px] rounded-lg border border-l-[3px] bg-ink-800 shadow-card ${k.accent} ${
selected ? "ring-2 ring-violet-500/70" : "border-line"
}`}
>
<Handle type="target" position={Position.Left} className="!h-2 !w-2 !bg-gray-400" />
<Handle type="target" position={Position.Left} className="!h-2 !w-2 !border-0 !bg-slate-500" />
<div className="flex items-center justify-between gap-2 px-3 py-2">
<span className={`rounded px-1.5 py-0.5 text-[10px] font-medium ${k.badge}`}>{k.label}</span>
<span className={`h-2 w-2 rounded-full ${dot}`} />
</div>
<div className="px-3 pb-2">
<div className="text-xs font-medium text-gray-800">{d.label || k.desc}</div>
{d.summary && <div className="mt-0.5 truncate text-[10px] text-gray-400">{d.summary}</div>}
<div className="text-xs font-medium text-slate-200">{d.label || k.desc}</div>
{d.summary && <div className="mt-0.5 truncate text-[10px] text-slate-500">{d.summary}</div>}
</div>
<Handle type="source" position={Position.Right} className="!h-2 !w-2 !bg-gray-400" />
<Handle type="source" position={Position.Right} className="!h-2 !w-2 !border-0 !bg-slate-500" />
</div>
);
}