refactor(desktop): 去黑话 + 收基建灯(商业化语言铺开)

把首屏定的"去黑话+精致克制"语言铺到其余页面:
- 顶栏 5 盏基建灯(Gateway/DB/NATS/Milvus/Neo4j)→ 一枚「系统正常」状态点
  (客户不看数据库名;异常时 tooltip 提示项数,顺带缓解窄屏拥挤)
- 知识库:入库阶段标签去实现名(写入向量库/Milvus/Bleve/Neo4j → 建立语义索引/
  全文索引/构建知识关系);检索台「混合召回+rerank」→「在库中搜索」,命中分数
  0.83→83%,"RAG 未配置"→"该库可能为空";图谱去「Neo4j·力导向」
- 运行:流式中→生成中;"token 在此流式呈现"→"回复逐字呈现";忠实→可信度/引用
- 编排节点「检索(RAG)」→「知识检索」,描述去"混合检索"

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Blizzard
2026-07-13 16:57:24 +08:00
parent 2eb38d3eaf
commit 8f1aefe13e
4 changed files with 34 additions and 30 deletions
+15 -11
View File
@@ -10,12 +10,20 @@ import { cn } from "../ui";
const DRAG = { "--wails-draggable": "drag" } as CSSProperties; const DRAG = { "--wails-draggable": "drag" } as CSSProperties;
const NODRAG = { "--wails-draggable": "no-drag" } as CSSProperties; const NODRAG = { "--wails-draggable": "no-drag" } as CSSProperties;
function Light({ on, label }: { on: boolean; label: string }) { // 系统状态:把 5 项基建健康收成一个整体状态点(客户不看数据库名,异常时 tooltip 提示项数)。
const dot = on ? "bg-success shadow-[0_0_8px_rgba(52,211,153,0.7)]" : "bg-danger"; function SystemStatus({ h }: { h: ReturnType<typeof useHealth> }) {
const down = [h.gateway, h.db, h.nats, h.milvus, h.neo4j].filter((v) => !v).length;
const ok = down === 0;
return ( return (
<span className="flex items-center gap-1.5 text-[11px] text-slate-500" title={`${label} ${on ? "在线" : "离线"}`}> <span
<span className={cn("h-1.5 w-1.5 rounded-full", dot)} /> title={ok ? "所有服务运行正常" : `${down} 项服务暂不可用`}
{label} className={cn(
"flex items-center gap-1.5 rounded-full border px-2.5 py-1 text-[11px]",
ok ? "border-line text-slate-500" : "border-warn/40 text-warn",
)}
>
<span className={cn("h-1.5 w-1.5 rounded-full", ok ? "bg-success" : "bg-warn")} />
{ok ? "系统正常" : "部分异常"}
</span> </span>
); );
} }
@@ -140,12 +148,8 @@ export function TopBar({ user, tenant, tenants = [], onSwitchTenant, space, spac
<kbd className="rounded border border-line px-1 font-mono text-[10px] text-slate-500">K</kbd> <kbd className="rounded border border-line px-1 font-mono text-[10px] text-slate-500">K</kbd>
</button> </button>
<div className="ml-2 flex items-center gap-3"> <div className="ml-2">
<Light on={h.gateway} label="Gateway" /> <SystemStatus h={h} />
<Light on={h.db} label="DB" />
<Light on={h.nats} label="NATS" />
<Light on={h.milvus} label="Milvus" />
<Light on={h.neo4j} label="Neo4j" />
</div> </div>
<div className="ml-auto flex items-center gap-2" style={NODRAG}> <div className="ml-auto flex items-center gap-2" style={NODRAG}>
<TenantSwitcher tenants={tenants} activeId={tenant?.tenant?.id} onSwitch={onSwitchTenant} /> <TenantSwitcher tenants={tenants} activeId={tenant?.tenant?.id} onSwitch={onSwitchTenant} />
@@ -45,10 +45,10 @@ export const NODE_KINDS: Record<string, NodeKind> = {
}, },
retriever: { retriever: {
kind: "retriever", kind: "retriever",
label: "检索 (RAG)", label: "知识检索",
accent: "border-l-sky-500", accent: "border-l-sky-500",
badge: "bg-sky-100 text-sky-700", badge: "bg-sky-100 text-sky-700",
desc: "绑定知识库做混合检索", desc: "知识库检索相关内容",
fields: [ fields: [
{ key: "kb", label: "知识库", type: "select", options: ["(未建库)", "合同库", "判例库"], required: true }, { key: "kb", label: "知识库", type: "select", options: ["(未建库)", "合同库", "判例库"], required: true },
{ key: "topK", label: "TopK", type: "number" }, { key: "topK", label: "TopK", type: "number" },
+14 -14
View File
@@ -77,15 +77,16 @@ interface FileJob {
} }
type KbTab = "ingest" | "vault" | "search" | "graph"; type KbTab = "ingest" | "vault" | "search" | "graph";
// key 对齐后端事件阶段(勿改);label 用「结果语言」,不暴露 Milvus/Bleve/Neo4j 等实现名。
const STAGE: Record<string, { icon: LucideIcon; label: string }> = { const STAGE: Record<string, { icon: LucideIcon; label: string }> = {
: { icon: Upload, label: "解析文件" }, : { icon: Upload, label: "解析文件" },
: { icon: FileText, label: "解析完成" }, : { icon: FileText, label: "解析完成" },
: { icon: Scissors, label: "切块" }, : { icon: Scissors, label: "拆分段落" },
: { icon: Sparkles, label: "向量化" }, : { icon: Sparkles, label: "理解内容" },
Milvus: { icon: Database, label: "写入向量库" }, Milvus: { icon: Database, label: "建立语义索引" },
Bleve: { icon: Search, label: "写入全文索引" }, Bleve: { icon: Search, label: "建立全文索引" },
: { icon: Network, label: "取知识" }, : { icon: Network, label: "取知识" },
Neo4j: { icon: Share2, label: "写入图谱" }, Neo4j: { icon: Share2, label: "构建知识关系" },
: { icon: CheckCircle2, label: "完成" }, : { icon: CheckCircle2, label: "完成" },
: { icon: XCircle, label: "失败" }, : { icon: XCircle, label: "失败" },
}; };
@@ -395,23 +396,22 @@ export function KbView({ identity, spaceId = "", spaceReadOnly = false }: { iden
{tab === "search" && ( {tab === "search" && (
<div className="h-full overflow-y-auto p-4"> <div className="h-full overflow-y-auto p-4">
<h3 className="mb-2 text-xs font-semibold text-slate-400"> · {kb} + rerank</h3> <h3 className="mb-2 text-xs font-semibold text-slate-400">{kb}</h3>
<div className="flex max-w-3xl gap-2"> <div className="flex max-w-3xl gap-2">
<Input className="flex-1" value={q} onChange={(e) => setQ(e.target.value)} onKeyDown={(e) => e.key === "Enter" && onSearch()} placeholder="输入查询,语义召回相关片段…" /> <Input className="flex-1" value={q} onChange={(e) => setQ(e.target.value)} onKeyDown={(e) => e.key === "Enter" && onSearch()} placeholder="输入问题或关键词,查找相关内容…" />
<Input type="number" className="w-16" value={topK} min={1} max={20} onChange={(e) => setTopK(Number(e.target.value))} title="TopK" /> <Input type="number" className="w-16" value={topK} min={1} max={20} onChange={(e) => setTopK(Number(e.target.value))} title="TopK" />
<Button variant="primary" size="md" icon={Search} onClick={onSearch} disabled={searching || !q.trim()}> <Button variant="primary" size="md" icon={Search} onClick={onSearch} disabled={searching || !q.trim()}>
{searching ? "检索中…" : "检索"} {searching ? "检索中…" : "检索"}
</Button> </Button>
</div> </div>
<ul className="mt-3 max-w-3xl space-y-2"> <ul className="mt-3 max-w-3xl space-y-2">
{hits === null && <li className="text-xs text-slate-600"></li>} {hits === null && <li className="text-xs text-slate-600"></li>}
{hits !== null && hits.length === 0 && <li className="text-xs text-slate-600"> RAG </li>} {hits !== null && hits.length === 0 && <li className="text-xs text-slate-600"></li>}
{hits?.map((h, i) => ( {hits?.map((h, i) => (
<li key={i} className="rounded-lg border border-line bg-ink-850 p-2"> <li key={i} className="rounded-lg border border-line bg-ink-850 p-2">
<div className="mb-1 flex items-center gap-2 text-[10px]"> <div className="mb-1 flex items-center gap-2 text-[10px]">
<Badge tone="accent"></Badge>
<span className="text-slate-600">#{i + 1}</span> <span className="text-slate-600">#{i + 1}</span>
<span className="ml-auto font-mono text-brand-400">{h.score.toFixed(3)}</span> <span className="ml-auto font-mono text-slate-500" title="相关度">{(h.score * 100).toFixed(0)}%</span>
</div> </div>
<div className="text-xs text-slate-300">{h.text}</div> <div className="text-xs text-slate-300">{h.text}</div>
</li> </li>
@@ -423,7 +423,7 @@ export function KbView({ identity, spaceId = "", spaceReadOnly = false }: { iden
{tab === "graph" && ( {tab === "graph" && (
<div className="h-full overflow-y-auto p-4"> <div className="h-full overflow-y-auto p-4">
<div className="mb-2 flex max-w-3xl items-center justify-between"> <div className="mb-2 flex max-w-3xl items-center justify-between">
<h3 className="text-xs font-semibold text-slate-400"> · {kb}Neo4j · </h3> <h3 className="text-xs font-semibold text-slate-400">{kb}</h3>
<Button size="sm" icon={Network} onClick={onGraph}> <Button size="sm" icon={Network} onClick={onGraph}>
</Button> </Button>
@@ -613,7 +613,7 @@ function VaultPanel({ identity, kb, spaceReadOnly = false }: { identity: Identit
</Button> </Button>
</> </>
)} )}
<span className="ml-auto text-[10px] text-slate-600"> [[]]</span> <span className="ml-auto text-[10px] text-slate-600"> [[]] </span>
</div> </div>
{mode === "graph" ? ( {mode === "graph" ? (
@@ -79,7 +79,7 @@ export function RunsView({ run }: { run: RunState }) {
const statusCls = const statusCls =
cur.phase === "streaming" ? "text-accent-400" : cur.phase === "done" ? "text-success" : cur.phase === "error" ? "text-danger" : "text-slate-500"; cur.phase === "streaming" ? "text-accent-400" : cur.phase === "done" ? "text-success" : cur.phase === "error" ? "text-danger" : "text-slate-500";
const statusText = const statusText =
cur.phase === "streaming" ? "流式中…" : cur.phase === "done" ? "完成 ✓" : cur.phase === "error" ? `${cur.error ?? "出错"}` : cur.phase === "submitting" ? "提交中…" : "就绪"; cur.phase === "streaming" ? "生成中…" : cur.phase === "done" ? "完成 ✓" : cur.phase === "error" ? `${cur.error ?? "出错"}` : cur.phase === "submitting" ? "提交中…" : "就绪";
return ( return (
<div className="flex h-full min-h-0 flex-col gap-3 overflow-hidden p-6"> <div className="flex h-full min-h-0 flex-col gap-3 overflow-hidden p-6">
@@ -145,7 +145,7 @@ export function RunsView({ run }: { run: RunState }) {
{cur.output ? ( {cur.output ? (
<OutputView output={cur.output} /> <OutputView output={cur.output} />
) : ( ) : (
<EmptyState icon={Activity} title="尚无输出" desc="选择一次运行查看其模型输出;或发起新运行,token 在此流式呈现。" /> <EmptyState icon={Activity} title="尚无输出" desc="选择一次运行查看结果;或发起新运行,回复会在这里逐字呈现。" />
)} )}
</Panel> </Panel>
</div> </div>
@@ -208,7 +208,7 @@ function EvalView({ ev }: { ev: EvalResult | null }) {
<Badge tone={LEVEL_TONE[ev.level] ?? "neutral"}>{ev.level} · {ev.overall.toFixed(2)}</Badge> <Badge tone={LEVEL_TONE[ev.level] ?? "neutral"}>{ev.level} · {ev.overall.toFixed(2)}</Badge>
{ev.corrected && <Badge tone="accent"></Badge>} {ev.corrected && <Badge tone="accent"></Badge>}
<span className="text-slate-500"> {ev.rule.toFixed(2)} · {ev.llm.toFixed(2)}</span> <span className="text-slate-500"> {ev.rule.toFixed(2)} · {ev.llm.toFixed(2)}</span>
{ev.sources > 0 && <span className="text-slate-500"> {ev.faithful.toFixed(2)} · {ev.sources} </span>} {ev.sources > 0 && <span className="text-slate-500"> {ev.faithful.toFixed(2)} · {ev.sources} </span>}
</div> </div>
{ev.reason && <p className="leading-relaxed text-slate-400">{ev.reason}</p>} {ev.reason && <p className="leading-relaxed text-slate-400">{ev.reason}</p>}
{ev.flags?.length > 0 && <p className="text-warn">{ev.flags.join("")}</p>} {ev.flags?.length > 0 && <p className="text-warn">{ev.flags.join("")}</p>}