feat(knowledge): 知识库 wiki/知识图谱重构,新增发布广场选择弹窗
- 知识库页拆分出 wiki 组件(侧边栏/目录树/Markdown 阅读器等) - 知识图谱页拆分出 knowledge-graph 组件并增强交互 - 新增发布广场选择弹窗 publish-square-picker-dialog - config.yaml 新增 MiniMax-M2.5 / Ling-flash-2.0 / DeepSeek-V4-Pro 模型 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
f0aec4cd7f
commit
e684de30bd
30 files changed
+4827
-682
No files matched your search
@@ -0,0 +1,163 @@
|
||||
import { Search } from "lucide-react";
|
||||
|
||||
import type { GraphNode, GraphStats, NodeKind } from "./graphTypes";
|
||||
import {
|
||||
getEdgeInfo,
|
||||
getNodeTitle,
|
||||
NODE_KIND_META,
|
||||
NODE_KIND_ORDER,
|
||||
} from "./graphUtils";
|
||||
|
||||
interface GraphSidebarProps {
|
||||
searchKeyword: string;
|
||||
searchMatchCount: number | null;
|
||||
nodeKindCounts: Map<NodeKind, number>;
|
||||
edgeLabelCounts: Map<string, number>;
|
||||
disabledNodeKinds: Set<NodeKind>;
|
||||
disabledEdgeLabels: Set<string>;
|
||||
stats: GraphStats;
|
||||
topNodes: Array<{ node: GraphNode; degree: number }>;
|
||||
hiddenCount: number;
|
||||
onSearchChange: (keyword: string) => void;
|
||||
onSearchEnter: () => void;
|
||||
onToggleNodeKind: (kind: NodeKind) => void;
|
||||
onToggleEdgeLabel: (label: string) => void;
|
||||
onRestoreHidden: () => void;
|
||||
onPickNode: (id: string) => void;
|
||||
}
|
||||
|
||||
export function GraphSidebar(props: GraphSidebarProps) {
|
||||
const kinds = NODE_KIND_ORDER.filter((k) => (props.nodeKindCounts.get(k) ?? 0) > 0);
|
||||
const edgeLabels = [...props.edgeLabelCounts.entries()].sort((a, b) => b[1] - a[1]);
|
||||
|
||||
return (
|
||||
<aside className="kg-panel kg-sidebar">
|
||||
<div className="kg-panel-header">
|
||||
<span className="kg-panel-title">搜索与筛选</span>
|
||||
{props.hiddenCount > 0 ? (
|
||||
<button
|
||||
type="button"
|
||||
className="kg-text-button"
|
||||
onClick={props.onRestoreHidden}
|
||||
>
|
||||
恢复 {props.hiddenCount} 个隐藏节点
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="kg-panel-content">
|
||||
<div className="kg-search-wrap">
|
||||
<Search size={14} />
|
||||
<input
|
||||
className="kg-search"
|
||||
placeholder="搜索节点标题..."
|
||||
value={props.searchKeyword}
|
||||
onChange={(e) => props.onSearchChange(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") props.onSearchEnter();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{props.searchMatchCount !== null ? (
|
||||
<div
|
||||
className={`kg-search-hint${props.searchMatchCount === 0 ? " kg-search-hint-warn" : ""}`}
|
||||
>
|
||||
{props.searchMatchCount === 0
|
||||
? "没有匹配的节点,换个关键词试试"
|
||||
: `匹配 ${props.searchMatchCount} 个节点,回车定位`}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<section className="kg-filter-section">
|
||||
<div className="kg-filter-title">节点类型</div>
|
||||
{kinds.map((kind) => (
|
||||
<label key={kind} className="kg-check-row">
|
||||
<span className="kg-check-row-main">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={!props.disabledNodeKinds.has(kind)}
|
||||
onChange={() => props.onToggleNodeKind(kind)}
|
||||
/>
|
||||
<span
|
||||
className="kg-legend-dot"
|
||||
style={{ background: NODE_KIND_META[kind].color }}
|
||||
/>
|
||||
<span className="kg-check-label">{NODE_KIND_META[kind].label}</span>
|
||||
</span>
|
||||
<span className="kg-count">{props.nodeKindCounts.get(kind)}</span>
|
||||
</label>
|
||||
))}
|
||||
</section>
|
||||
|
||||
{edgeLabels.length > 0 ? (
|
||||
<section className="kg-filter-section">
|
||||
<div className="kg-filter-title">关系类型</div>
|
||||
{edgeLabels.map(([label, count]) => {
|
||||
const info = getEdgeInfo(label);
|
||||
return (
|
||||
<label key={label} className="kg-check-row">
|
||||
<span className="kg-check-row-main">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={!props.disabledEdgeLabels.has(label)}
|
||||
onChange={() => props.onToggleEdgeLabel(label)}
|
||||
/>
|
||||
<span
|
||||
className="kg-legend-line"
|
||||
style={{
|
||||
borderTopColor: info.color,
|
||||
borderTopStyle: info.dashed ? "dashed" : "solid",
|
||||
}}
|
||||
/>
|
||||
<span className="kg-check-label">{info.zh}</span>
|
||||
</span>
|
||||
<span className="kg-count">{count}</span>
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
<section className="kg-filter-section">
|
||||
<div className="kg-filter-title">图谱统计</div>
|
||||
<div className="kg-stats-grid">
|
||||
<div className="kg-stat-card">
|
||||
<div className="kg-stat-card-value">{props.stats.nodes}</div>
|
||||
<div className="kg-stat-card-label">节点</div>
|
||||
</div>
|
||||
<div className="kg-stat-card">
|
||||
<div className="kg-stat-card-value">{props.stats.edges}</div>
|
||||
<div className="kg-stat-card-label">关系</div>
|
||||
</div>
|
||||
<div className="kg-stat-card">
|
||||
<div className="kg-stat-card-value">
|
||||
{props.stats.avgDegree.toFixed(2)}
|
||||
</div>
|
||||
<div className="kg-stat-card-label">平均度</div>
|
||||
</div>
|
||||
<div className="kg-stat-card">
|
||||
<div className="kg-stat-card-value">{props.stats.largestComponent}</div>
|
||||
<div className="kg-stat-card-label">最大连通分量</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{props.topNodes.length > 0 ? (
|
||||
<section className="kg-filter-section">
|
||||
<div className="kg-filter-title">核心节点</div>
|
||||
{props.topNodes.map(({ node, degree }, i) => (
|
||||
<div
|
||||
key={node.id}
|
||||
className="kg-rank-item"
|
||||
onClick={() => props.onPickNode(node.id)}
|
||||
>
|
||||
<span className="kg-rank-index">{i + 1}</span>
|
||||
<span className="kg-rank-title">{getNodeTitle(node)}</span>
|
||||
<span className="kg-rank-degree">{degree} 度</span>
|
||||
</div>
|
||||
))}
|
||||
</section>
|
||||
) : null}
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user