|
| 1 | +/** |
| 2 | + * KnowledgeSampleView — 摄影示例体系浏览 + 一键复制(v0.13.7 具象化)。 |
| 3 | + * |
| 4 | + * @ai-context: 示例≠预填(纪律裁决 2026-08-24)——浏览是被动参照;复制是 |
| 5 | + * 主动获得骨架("待改造"非"已完成")。无全局体系时阻塞等待 |
| 6 | + * 用户先建全局(onNeedGlobal 由父级开全局向导)。 |
| 7 | + * @ai-context: 复制落库顺序 golden——create_knowledge_system → 逐条 |
| 8 | + * add_knowledge_node(parentId 由"示例内数组下标"映射为真实 id, |
| 9 | + * 串行保证父先于子)→ 概念 ×3 → 模型 ×1。顺序在测试有断言,勿改。 |
| 10 | + */ |
| 11 | +import { useEffect, useMemo, useState } from "react"; |
| 12 | +import type { ReactNode } from "react"; |
| 13 | +import { invoke } from "@tauri-apps/api/core"; |
| 14 | +import { SAMPLE_SYSTEM } from "../utils/knowledgeSample"; |
| 15 | +import type { KnowledgeSystem } from "../types/knowledge"; |
| 16 | + |
| 17 | +interface Props { |
| 18 | + /** 复制完成回调(父级刷新体系列表并选中新体系) */ |
| 19 | + onCopied: (system: KnowledgeSystem) => void; |
| 20 | + /** 无全局体系时触发(父级打开全局创建向导;完成后父级调用 refreshGlobal 重试) */ |
| 21 | + onNeedGlobal: () => void; |
| 22 | + /** 外部刷新信号(全局创建完成后父级触发) */ |
| 23 | + refreshGlobal?: number; |
| 24 | +} |
| 25 | + |
| 26 | +export default function KnowledgeSampleView({ onCopied, onNeedGlobal, refreshGlobal = 0 }: Props) { |
| 27 | + const [globalSystem, setGlobalSystem] = useState<KnowledgeSystem | null>(null); |
| 28 | + const [busy, setBusy] = useState(false); |
| 29 | + const [err, setErr] = useState(""); |
| 30 | + |
| 31 | + const checkGlobal = async () => { |
| 32 | + try { |
| 33 | + const list = await invoke<KnowledgeSystem[]>("list_knowledge_systems"); |
| 34 | + setGlobalSystem(list.find((s) => s.kind === "global") ?? null); |
| 35 | + } catch (e) { |
| 36 | + setErr(`体系检查失败: ${e}`); |
| 37 | + } |
| 38 | + }; |
| 39 | + useEffect(() => { void checkGlobal(); }, [refreshGlobal]); |
| 40 | + |
| 41 | + const tree = useMemo(() => { |
| 42 | + const byParent = new Map<number | null, { type: string; text: string; index: number }[]>(); |
| 43 | + SAMPLE_SYSTEM.nodes.forEach((n, i) => { |
| 44 | + const list = byParent.get(n.parentId) ?? []; |
| 45 | + list.push({ type: n.type, text: n.text, index: i }); |
| 46 | + byParent.set(n.parentId, list); |
| 47 | + }); |
| 48 | + return byParent; |
| 49 | + }, []); |
| 50 | + |
| 51 | + const renderNodes = (parentId: number | null, depth: number): ReactNode => |
| 52 | + (tree.get(parentId) ?? []).map((n) => ( |
| 53 | + <div key={n.index} style={{ paddingLeft: depth * 14, fontSize: 12, marginTop: 2 }}> |
| 54 | + {n.type === "question" ? "❓" : n.type === "scenario" ? "🎯" : "📂"} {n.text} |
| 55 | + {renderNodes(n.index, depth + 1)} |
| 56 | + </div> |
| 57 | + )); |
| 58 | + |
| 59 | + const runCopy = async () => { |
| 60 | + if (busy) return; |
| 61 | + if (!globalSystem) { onNeedGlobal(); return; } |
| 62 | + setBusy(true); setErr(""); |
| 63 | + try { |
| 64 | + const sys = await invoke<KnowledgeSystem>("create_knowledge_system", { |
| 65 | + name: SAMPLE_SYSTEM.name, |
| 66 | + kind: SAMPLE_SYSTEM.kind, |
| 67 | + parentSystemId: globalSystem.id, |
| 68 | + coreQuestion: SAMPLE_SYSTEM.coreQuestion, |
| 69 | + }); |
| 70 | + // 示例内 parentId 是 nodes 数组下标索引 → 落库时维护 index → 真实 node id 映射。 |
| 71 | + // 子节点 parentId 恒指向更小的下标(父先于子),串行创建即可满足映射。 |
| 72 | + const idMap = new Map<number, number>(); |
| 73 | + for (let index = 0; index < SAMPLE_SYSTEM.nodes.length; index++) { |
| 74 | + const n = SAMPLE_SYSTEM.nodes[index]; |
| 75 | + const realParentId = n.parentId == null ? undefined : idMap.get(n.parentId); |
| 76 | + const created = await invoke<{ id: number }>("add_knowledge_node", { |
| 77 | + systemId: sys.id, |
| 78 | + parentId: realParentId, |
| 79 | + nodeType: n.type, |
| 80 | + text: n.text, |
| 81 | + }); |
| 82 | + idMap.set(index, created.id); |
| 83 | + } |
| 84 | + for (const c of SAMPLE_SYSTEM.concepts) { |
| 85 | + await invoke("add_knowledge_concept", { |
| 86 | + systemId: sys.id, name: c.name, essence: c.essence, boundary: c.boundary, relation: c.relation, |
| 87 | + }); |
| 88 | + } |
| 89 | + for (const m of SAMPLE_SYSTEM.models) { |
| 90 | + await invoke("add_knowledge_model", { |
| 91 | + systemId: sys.id, name: m.name, disciplines: m.disciplines, claim: m.claim, |
| 92 | + validWhen: m.validWhen, invalidWhen: m.invalidWhen, |
| 93 | + }); |
| 94 | + } |
| 95 | + onCopied(sys); |
| 96 | + } catch (e) { |
| 97 | + setErr(`复制失败: ${e}`); |
| 98 | + } finally { |
| 99 | + setBusy(false); |
| 100 | + } |
| 101 | + }; |
| 102 | + |
| 103 | + return ( |
| 104 | + <div data-testid="sample-view" style={{ width: "100%", maxWidth: 560, textAlign: "left", background: "#fff", border: "1px solid #e5e7eb", borderRadius: 10, padding: 16 }}> |
| 105 | + <div style={{ fontWeight: 700, fontSize: 14, color: "#0f766e", marginBottom: 4 }}>📷 示例:领域体系「{SAMPLE_SYSTEM.name}」</div> |
| 106 | + <div style={{ fontSize: 12, color: "#6b7280", marginBottom: 10 }}>核心问题:{SAMPLE_SYSTEM.coreQuestion}</div> |
| 107 | + <div style={{ fontSize: 12, fontWeight: 600, marginBottom: 4 }}>🌳 问题树</div> |
| 108 | + {renderNodes(null, 0)} |
| 109 | + <div style={{ fontSize: 12, fontWeight: 600, marginTop: 8, marginBottom: 4 }}>🧬 概念(三问)</div> |
| 110 | + {SAMPLE_SYSTEM.concepts.map((c) => ( |
| 111 | + <div key={c.name} style={{ fontSize: 12, marginBottom: 6, padding: 6, background: "#f9fafb", borderRadius: 6 }}> |
| 112 | + <b>{c.name}</b> |
| 113 | + <div style={{ color: "#6b7280" }}>本质:{c.essence}</div> |
| 114 | + <div style={{ color: "#6b7280" }}>边界:{c.boundary}</div> |
| 115 | + <div style={{ color: "#6b7280" }}>联系:{c.relation}</div> |
| 116 | + </div> |
| 117 | + ))} |
| 118 | + <div style={{ fontSize: 12, fontWeight: 600, marginTop: 8, marginBottom: 4 }}>⚙ 模型</div> |
| 119 | + {SAMPLE_SYSTEM.models.map((m) => ( |
| 120 | + <div key={m.name} style={{ fontSize: 12, padding: 6, background: "#f9fafb", borderRadius: 6 }}> |
| 121 | + <b>{m.name}</b>({m.disciplines.join("/")}) |
| 122 | + <div style={{ color: "#6b7280" }}>主张:{m.claim}</div> |
| 123 | + <div style={{ color: "#6b7280" }}>何时成立:{m.validWhen} · 何时失效:{m.invalidWhen}</div> |
| 124 | + </div> |
| 125 | + ))} |
| 126 | + <button |
| 127 | + data-testid="sample-copy" |
| 128 | + onClick={() => void runCopy()} |
| 129 | + disabled={busy} |
| 130 | + style={{ marginTop: 12, fontSize: 13, cursor: "pointer", padding: "8px 18px", borderRadius: 6, border: "1px solid #0f766e", background: "#f0fdfa", color: "#0f766e", fontWeight: 600 }} |
| 131 | + > |
| 132 | + {busy ? "复制中…" : "📋 复制为我的体系"} |
| 133 | + </button> |
| 134 | + {err && <div style={{ fontSize: 11, color: "#dc2626", marginTop: 6 }}>{err}</div>} |
| 135 | + </div> |
| 136 | + ); |
| 137 | +} |
0 commit comments