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,210 @@
|
||||
import { Crosshair, EyeOff, FileText, Loader2, MousePointerClick } from "lucide-react";
|
||||
|
||||
import type { KnowledgeNote } from "@/core/knowledge/types";
|
||||
|
||||
import type { GraphNode } from "./graphTypes";
|
||||
import {
|
||||
getEdgeInfo,
|
||||
getNodeKind,
|
||||
getNodeTitle,
|
||||
NODE_KIND_META,
|
||||
} from "./graphUtils";
|
||||
|
||||
const SOURCE_TYPE_ZH: Record<string, string> = {
|
||||
thread: "对话沉淀",
|
||||
search: "搜索沉淀",
|
||||
tool: "工具结果",
|
||||
file: "文件导入",
|
||||
manual: "手动创建",
|
||||
report: "报告",
|
||||
};
|
||||
|
||||
const STATUS_ZH: Record<string, string> = {
|
||||
draft: "草稿",
|
||||
approved: "已审核",
|
||||
archived: "已归档",
|
||||
};
|
||||
|
||||
export interface NeighborItem {
|
||||
node: GraphNode;
|
||||
relation: string;
|
||||
}
|
||||
|
||||
interface GraphDetailPanelProps {
|
||||
node: GraphNode | null;
|
||||
degree: number;
|
||||
neighbors: NeighborItem[];
|
||||
/** Note detail (only for note-type nodes; undefined while absent/loading). */
|
||||
note: KnowledgeNote | undefined;
|
||||
noteLoading: boolean;
|
||||
onViewSource: (noteId: string) => void;
|
||||
onLocate: (id: string) => void;
|
||||
onHide: (id: string) => void;
|
||||
onPickNeighbor: (id: string) => void;
|
||||
}
|
||||
|
||||
function formatTime(value: string | null | undefined): string {
|
||||
if (!value) return "—";
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) return value;
|
||||
return date.toLocaleString("zh-CN", { hour12: false });
|
||||
}
|
||||
|
||||
export function GraphDetailPanel(props: GraphDetailPanelProps) {
|
||||
const { node } = props;
|
||||
|
||||
return (
|
||||
<aside className="kg-panel kg-detail">
|
||||
<div className="kg-panel-header">
|
||||
<span className="kg-panel-title">节点详情</span>
|
||||
</div>
|
||||
{!node ? (
|
||||
<div className="kg-detail-empty">
|
||||
<MousePointerClick size={28} />
|
||||
<div className="kg-detail-empty-title">选择一个节点查看详情</div>
|
||||
<div>
|
||||
点击图谱中的文档、主题或实体节点,
|
||||
<br />
|
||||
查看摘要、标签、关联关系和相邻节点。
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<DetailBody {...props} node={node} />
|
||||
)}
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
||||
function DetailBody(props: GraphDetailPanelProps & { node: GraphNode }) {
|
||||
const { node, note } = props;
|
||||
const kind = getNodeKind(node);
|
||||
const kindMeta = NODE_KIND_META[kind];
|
||||
const isNote = (node.type || "").toLowerCase() === "note";
|
||||
|
||||
return (
|
||||
<div className="kg-panel-content">
|
||||
<h2 className="kg-detail-title">{getNodeTitle(node)}</h2>
|
||||
<div>
|
||||
<span
|
||||
className="kg-detail-type"
|
||||
style={{
|
||||
background: `${kindMeta.color}26`,
|
||||
color: kindMeta.color,
|
||||
}}
|
||||
>
|
||||
<span className="kg-legend-dot" style={{ background: kindMeta.color }} />
|
||||
{kindMeta.label}
|
||||
{node.category && node.category !== kindMeta.label
|
||||
? ` · ${node.category}`
|
||||
: ""}
|
||||
</span>
|
||||
<span
|
||||
className="kg-detail-type"
|
||||
style={{ background: "rgba(148, 163, 184, 0.12)", color: "#cbd5e1" }}
|
||||
>
|
||||
度数 {props.degree}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{props.noteLoading ? (
|
||||
<div className="kg-detail-summary" style={{ display: "flex", gap: 8 }}>
|
||||
<Loader2 size={14} className="animate-spin" /> 加载笔记详情...
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{note?.summary ? <p className="kg-detail-summary">{note.summary}</p> : null}
|
||||
|
||||
{note?.tags?.length ? (
|
||||
<div className="kg-tag-list">
|
||||
{note.tags.map((tag) => (
|
||||
<span key={tag} className="kg-tag">
|
||||
#{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{note ? (
|
||||
<div className="kg-detail-meta">
|
||||
<span className="kg-detail-meta-label">来源</span>
|
||||
<span className="kg-detail-meta-value">
|
||||
{SOURCE_TYPE_ZH[note.source_type] ?? note.source_type}
|
||||
</span>
|
||||
<span className="kg-detail-meta-label">状态</span>
|
||||
<span className="kg-detail-meta-value">
|
||||
{STATUS_ZH[note.status] ?? note.status}
|
||||
</span>
|
||||
<span className="kg-detail-meta-label">创建时间</span>
|
||||
<span className="kg-detail-meta-value">{formatTime(note.created_at)}</span>
|
||||
<span className="kg-detail-meta-label">更新时间</span>
|
||||
<span className="kg-detail-meta-value">{formatTime(note.updated_at)}</span>
|
||||
{note.created_by_name ? (
|
||||
<>
|
||||
<span className="kg-detail-meta-label">创建人</span>
|
||||
<span className="kg-detail-meta-value">{note.created_by_name}</span>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="kg-detail-actions">
|
||||
{isNote ? (
|
||||
<button
|
||||
type="button"
|
||||
className="kg-icon-button"
|
||||
onClick={() => props.onViewSource(node.id)}
|
||||
>
|
||||
<FileText size={14} /> 查看原文
|
||||
</button>
|
||||
) : null}
|
||||
<button
|
||||
type="button"
|
||||
className="kg-icon-button"
|
||||
onClick={() => props.onLocate(node.id)}
|
||||
>
|
||||
<Crosshair size={14} /> 定位节点
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="kg-icon-button"
|
||||
onClick={() => props.onHide(node.id)}
|
||||
>
|
||||
<EyeOff size={14} /> 隐藏节点
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<h3 className="kg-detail-section-title">
|
||||
关联节点({props.neighbors.length})
|
||||
</h3>
|
||||
{props.neighbors.length === 0 ? (
|
||||
<div className="kg-search-hint">该节点暂无可见的关联节点</div>
|
||||
) : (
|
||||
<div className="kg-neighbor-list">
|
||||
{props.neighbors.map(({ node: neighbor, relation }) => {
|
||||
const neighborKind = NODE_KIND_META[getNodeKind(neighbor)];
|
||||
const relationInfo = getEdgeInfo(relation);
|
||||
return (
|
||||
<div
|
||||
key={`${neighbor.id}:${relation}`}
|
||||
className="kg-neighbor-item"
|
||||
onClick={() => props.onPickNeighbor(neighbor.id)}
|
||||
>
|
||||
<span
|
||||
className="kg-legend-dot"
|
||||
style={{ background: neighborKind.color, flexShrink: 0 }}
|
||||
/>
|
||||
<span className="kg-neighbor-title">{getNodeTitle(neighbor)}</span>
|
||||
<span
|
||||
className="kg-neighbor-relation"
|
||||
style={{ color: relationInfo.color }}
|
||||
>
|
||||
{relationInfo.zh}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user