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:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user