feat: update knowledge and agent UI

This commit is contained in:
吉鑫升 committed 2026-06-11 16:18:38 +08:00
1 parent bebe4feccf
commit 7efd811e75
27 files changed
+2949 -782

No files matched your search

+73 -29
View File
@@ -1,13 +1,13 @@
"use client";
import { BotIcon, PlusSquare } from "lucide-react";
import { BotIcon, MessageCircleQuestionIcon, PlusSquare } from "lucide-react";
import { useParams, useRouter } from "next/navigation";
import { useRouteBase } from "@/core/route-base";
import { useCallback, useEffect, useRef, useState } from "react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import type { PromptInputMessage } from "@/components/ai-elements/prompt-input";
import { Button } from "@/components/ui/button";
import { AgentInfoSidebar } from "@/components/workspace/agent-info-sidebar";
import { AgentDetailPanel } from "@/components/workspace/agents/agent-detail-panel";
import { AgentWelcome } from "@/components/workspace/agent-welcome";
import { ArtifactTrigger } from "@/components/workspace/artifacts";
import { ChatBox, useThreadChat } from "@/components/workspace/chats";
@@ -19,7 +19,7 @@ import { ThreadTitle } from "@/components/workspace/thread-title";
import { TodoList } from "@/components/workspace/todo-list";
import { TokenUsageIndicator } from "@/components/workspace/token-usage-indicator";
import { Tooltip } from "@/components/workspace/tooltip";
import { useAgent } from "@/core/agents";
import { normalizeAgentQuestions, useAgent } from "@/core/agents";
import { useI18n } from "@/core/i18n/hooks";
import { useModels } from "@/core/models/hooks";
import { useNotification } from "@/core/notification/hooks";
@@ -28,6 +28,7 @@ import { useThreadStream } from "@/core/threads/hooks";
import { textOfMessage } from "@/core/threads/utils";
import { env } from "@/env";
import { desensitize } from "@/lib/desensitize";
import { cn } from "@/lib/utils";
export default function AgentChatPage() {
const routeBase = useRouteBase();
@@ -61,6 +62,11 @@ export default function AgentChatPage() {
const agentDisplayName = desensitize(
agent?.name ?? (isAgentLoading ? t.common.loading : ""),
);
// 常用问题 shown as a list beside the input box on the landing screen.
const commonQuestions = useMemo(
() => normalizeAgentQuestions(agent?.common_questions),
[agent?.common_questions],
);
const { threadId, setThreadId, isNewThread, setIsNewThread } =
useThreadChat();
@@ -225,33 +231,70 @@ export default function AgentChatPage() {
<AgentWelcome
agent={agent}
agentName={agentDisplayName}
onAskQuestion={(text) => handleSubmit({ text, files: [] })}
disabled={isDemoMode}
className="mb-7"
/>
<div className="w-full max-w-(--container-width-md)">
<InputBox
className="w-full"
isNewThread
hideAdminRecommendedQuestions
threadId={threadId}
assistantId={agent_id}
autoFocus
status={status}
context={settings.context}
disabled={isDemoMode}
onContextChange={(context) =>
setSettings("context", context)
}
onFollowupsVisibilityChange={setShowFollowups}
onSubmit={handleSubmit}
onStop={handleStop}
/>
{isDemoMode && (
<div className="text-muted-foreground/67 mt-3 w-full text-center text-xs">
{t.common.notAvailableInDemoMode}
{/* 常用问题 sit as a list to the LEFT of the input box (not as
floating pills above it). On narrow screens the list stacks
above the input instead of squeezing it. */}
<div
className={cn(
"flex w-full flex-col gap-3 sm:flex-row sm:items-end sm:gap-4",
commonQuestions.length > 0
? "max-w-(--container-width-lg)"
: "max-w-(--container-width-md)",
)}
>
{commonQuestions.length > 0 && (
<div className="border-border/60 bg-card/60 w-full shrink-0 rounded-xl border p-3 sm:w-64">
<div className="text-muted-foreground mb-2 flex items-center gap-1.5 text-xs font-medium">
<MessageCircleQuestionIcon className="size-3.5" />
常用问题
</div>
<div className="flex max-h-44 flex-col gap-1.5 overflow-y-auto">
{commonQuestions.map((question, index) => (
<button
key={index}
type="button"
disabled={isDemoMode}
onClick={() =>
handleSubmit({ text: question.prompt, files: [] })
}
title={desensitize(question.prompt)}
className={cn(
"border-border/60 bg-background/60 hover:bg-accent hover:border-border w-full truncate rounded-lg border px-3 py-2 text-left text-sm transition-colors",
"disabled:cursor-not-allowed disabled:opacity-50",
)}
>
{desensitize(question.title)}
</button>
))}
</div>
</div>
)}
<div className="min-w-0 flex-1">
<InputBox
className="w-full"
isNewThread
hideAdminRecommendedQuestions
threadId={threadId}
assistantId={agent_id}
autoFocus
status={status}
context={settings.context}
disabled={isDemoMode}
onContextChange={(context) =>
setSettings("context", context)
}
onFollowupsVisibilityChange={setShowFollowups}
onSubmit={handleSubmit}
onStop={handleStop}
/>
{isDemoMode && (
<div className="text-muted-foreground/67 mt-3 w-full text-center text-xs">
{t.common.notAvailableInDemoMode}
</div>
)}
</div>
</div>
</div>
) : (
@@ -304,8 +347,9 @@ export default function AgentChatPage() {
)}
</main>
{/* ── Right: read-only agent profile sidebar ───────────────────── */}
<AgentInfoSidebar
{/* ── Right: agent detail panel (profile + inline editing for
admins / owners; read-only for everyone else) ───────────────── */}
<AgentDetailPanel
agent={agent}
agentName={agentDisplayName}
disabled={thread.isLoading || isDemoMode}
+31 -24
View File
@@ -17,13 +17,11 @@ import { ScrollArea } from "@/components/ui/scroll-area";
import { Textarea } from "@/components/ui/textarea";
import { EmptyDocumentState } from "@/components/wiki/EmptyDocumentState";
import { KnowledgeSidebar } from "@/components/wiki/KnowledgeSidebar";
import { TocPanel } from "@/components/wiki/TocPanel";
import { WikiContent } from "@/components/wiki/WikiContent";
import { WikiHeader } from "@/components/wiki/WikiHeader";
import {
buildKnowledgeTree,
downloadMarkdown,
extractToc,
findAncestorFolderIds,
} from "@/components/wiki/wikiUtils";
import { ModelNameSelect } from "@/components/workspace/model-name-select";
@@ -41,6 +39,16 @@ import MarkdownRenderer, {
type WikiLinkTarget,
} from "@/strategy-components/MarkdownRenderer";
const SIDEBAR_COLLAPSED_KEY = "wiki.sidebar.collapsed";
function readSidebarCollapsed(): boolean {
try {
return window.localStorage.getItem(SIDEBAR_COLLAPSED_KEY) === "1";
} catch {
return false;
}
}
function useDebounced<T>(value: T, delay = 350): T {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
@@ -461,6 +469,7 @@ export default function KnowledgeBasePage() {
const [expandedIds, setExpandedIds] = useState<Set<string>>(new Set());
const treeInitializedRef = useRef(false);
const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);
const [sidebarCollapsed, setSidebarCollapsed] = useState(readSidebarCollapsed);
// --- dialogs ---
const [createOpen, setCreateOpen] = useState(false);
@@ -469,9 +478,6 @@ export default function KnowledgeBasePage() {
const [dedupOpen, setDedupOpen] = useState(false);
const [vaultPath, setVaultPath] = useState<string | null>(null);
// --- reading state ---
const [activeHeadingId, setActiveHeadingId] = useState<string | null>(null);
const params = useMemo(
() => ({
keyword: debouncedKeyword || undefined,
@@ -488,10 +494,6 @@ export default function KnowledgeBasePage() {
const { data: note, isLoading: noteLoading } = useKnowledgeNote(noteId);
const { data: wikiLinks } = useNoteWikilinks(noteId);
const tocItems = useMemo(
() => (note ? extractToc(note.content_md) : []),
[note],
);
// First load: expand the root-level folders so the tree reads as a directory.
useEffect(() => {
@@ -529,11 +531,16 @@ export default function KnowledgeBasePage() {
navigate(`/page/workspace/knowledge/${item.id}`);
};
const handleTocClick = (id: string) => {
setActiveHeadingId(id);
document
.getElementById(id)
?.scrollIntoView({ behavior: "smooth", block: "start" });
const handleToggleSidebarCollapse = () => {
setSidebarCollapsed((prev) => {
const next = !prev;
try {
window.localStorage.setItem(SIDEBAR_COLLAPSED_KEY, next ? "1" : "0");
} catch {
// localStorage unavailable (private mode) — state stays session-only.
}
return next;
});
};
const handleWikiLink = (link: WikiLinkTarget) => {
@@ -555,7 +562,14 @@ export default function KnowledgeBasePage() {
onCreate={() => setCreateOpen(true)}
/>
<div className="grid min-h-0 flex-1 grid-cols-1 md:grid-cols-[300px_minmax(0,1fr)] xl:grid-cols-[300px_minmax(0,1fr)_280px]">
<div
className={cn(
"grid min-h-0 flex-1 grid-cols-1",
sidebarCollapsed && !mobileSidebarOpen
? "md:grid-cols-[44px_minmax(0,1fr)]"
: "md:grid-cols-[288px_minmax(0,1fr)]",
)}
>
{mobileSidebarOpen ? (
<div
className="fixed inset-0 z-40 bg-black/30 md:hidden"
@@ -578,6 +592,8 @@ export default function KnowledgeBasePage() {
onExportNote={downloadMarkdown}
total={data?.total ?? 0}
isLoading={isLoading}
collapsed={sidebarCollapsed && !mobileSidebarOpen}
onToggleCollapse={handleToggleSidebarCollapse}
className={cn(
"max-md:fixed max-md:inset-y-0 max-md:left-0 max-md:z-50 max-md:w-[300px] max-md:shadow-xl max-md:transition-transform max-md:duration-200",
mobileSidebarOpen ? "max-md:translate-x-0" : "max-md:-translate-x-full",
@@ -599,19 +615,10 @@ export default function KnowledgeBasePage() {
) : (
<WikiContent
note={note}
toc={tocItems}
wikiLinks={wikiLinks}
onWikiLinkClick={handleWikiLink}
onActiveHeadingChange={setActiveHeadingId}
/>
)}
<TocPanel
items={tocItems}
activeId={activeHeadingId}
onItemClick={handleTocClick}
className="hidden xl:block"
/>
</div>
<CreateNoteDialog open={createOpen} onOpenChange={setCreateOpen} />
+324 -39
View File
@@ -1,4 +1,4 @@
import { Loader2, Network, Tags } from "lucide-react";
import { Loader2, Network, RotateCcw, Tags, Undo2 } from "lucide-react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useNavigate } from "react-router-dom";
import { toast } from "sonner";
@@ -13,6 +13,8 @@ import { GraphHeader } from "@/components/knowledge-graph/GraphHeader";
import { GraphSidebar } from "@/components/knowledge-graph/GraphSidebar";
import { GraphTooltip } from "@/components/knowledge-graph/GraphTooltip";
import type {
ExploreStep,
GraphMode,
LayoutType,
NodeKind,
TooltipState,
@@ -20,6 +22,7 @@ import type {
import {
computeDegrees,
computeGraphStats,
getEdgeId,
getEdgeLabel,
getNodeKind,
getNodeTitle,
@@ -48,16 +51,23 @@ export default function KnowledgeGraphPage() {
const [layoutType, setLayoutType] = useState<LayoutType>("force");
const [showLabels, setShowLabels] = useState(true);
const [showSidebar, setShowSidebar] = useState(true);
const [showDetail, setShowDetail] = useState(true);
const [showDetail, setShowDetail] = useState(false);
const [zoomLevel, setZoomLevel] = useState(1);
const [tooltip, setTooltip] = useState<TooltipState | null>(null);
const [graphMode, setGraphMode] = useState<GraphMode>("explore");
const [exploreRootId, setExploreRootId] = useState<string | null>(null);
const [expandedNodeIds, setExpandedNodeIds] = useState<Set<string>>(new Set());
const [visibleNodeIds, setVisibleNodeIds] = useState<Set<string>>(new Set());
const [visibleEdgeIds, setVisibleEdgeIds] = useState<Set<string>>(new Set());
const [nodeDepthMap, setNodeDepthMap] = useState<Map<string, number>>(new Map());
const [exploreHistory, setExploreHistory] = useState<ExploreStep[]>([]);
// Auto-collapse the side panels on narrow viewports.
useEffect(() => {
const detailQuery = window.matchMedia("(max-width: 1280px)");
const sidebarQuery = window.matchMedia("(max-width: 900px)");
const apply = () => {
setShowDetail(!detailQuery.matches);
if (detailQuery.matches) setShowDetail(false);
setShowSidebar(!sidebarQuery.matches);
};
apply();
@@ -122,6 +132,133 @@ export default function KnowledgeGraphPage() {
);
}, [allEdges, filteredNodes, disabledEdgeLabels]);
const resetExplore = useCallback(() => {
setExploreRootId(null);
setExpandedNodeIds(new Set());
setVisibleNodeIds(new Set());
setVisibleEdgeIds(new Set());
setNodeDepthMap(new Map());
setExploreHistory([]);
}, []);
const rebuildExploreState = useCallback((history: ExploreStep[]) => {
const nextVisibleNodeIds = new Set<string>();
const nextVisibleEdgeIds = new Set<string>();
const nextNodeDepthMap = new Map<string, number>();
const nextExpandedNodeIds = new Set<string>();
for (const step of history) {
for (const nodeId of step.addedNodeIds) {
nextVisibleNodeIds.add(nodeId);
if (!nextNodeDepthMap.has(nodeId)) nextNodeDepthMap.set(nodeId, step.depth);
}
for (const edgeId of step.addedEdgeIds) nextVisibleEdgeIds.add(edgeId);
if (step.depth > 0) nextExpandedNodeIds.add(step.nodeId);
}
setVisibleNodeIds(nextVisibleNodeIds);
setVisibleEdgeIds(nextVisibleEdgeIds);
setNodeDepthMap(nextNodeDepthMap);
setExpandedNodeIds(nextExpandedNodeIds);
}, []);
const startExploreFromNode = useCallback((nodeId: string) => {
setExploreRootId(nodeId);
setVisibleNodeIds(new Set([nodeId]));
setVisibleEdgeIds(new Set());
setExpandedNodeIds(new Set());
setNodeDepthMap(new Map([[nodeId, 0]]));
setExploreHistory([
{
nodeId,
depth: 0,
addedNodeIds: [nodeId],
addedEdgeIds: [],
timestamp: Date.now(),
},
]);
window.setTimeout(() => canvasRef.current?.focusNode(nodeId), 0);
}, []);
const expandOneHop = useCallback(
(nodeId: string) => {
const currentDepth = nodeDepthMap.get(nodeId) ?? 0;
const nextDepth = currentDepth + 1;
const addedNodeIds: string[] = [];
const addedEdgeIds: string[] = [];
const nextVisibleNodeIds = new Set(visibleNodeIds);
const nextVisibleEdgeIds = new Set(visibleEdgeIds);
const nextNodeDepthMap = new Map(nodeDepthMap);
for (const edge of filteredEdges) {
if (edge.source !== nodeId && edge.target !== nodeId) continue;
const edgeId = getEdgeId(edge);
const neighborId = edge.source === nodeId ? edge.target : edge.source;
if (!nodeById.has(neighborId)) continue;
if (!nextVisibleEdgeIds.has(edgeId)) {
nextVisibleEdgeIds.add(edgeId);
addedEdgeIds.push(edgeId);
}
if (!nextVisibleNodeIds.has(neighborId)) {
nextVisibleNodeIds.add(neighborId);
nextNodeDepthMap.set(neighborId, nextDepth);
addedNodeIds.push(neighborId);
}
}
nextVisibleNodeIds.add(nodeId);
nextNodeDepthMap.set(nodeId, currentDepth);
setVisibleNodeIds(nextVisibleNodeIds);
setVisibleEdgeIds(nextVisibleEdgeIds);
setNodeDepthMap(nextNodeDepthMap);
setExpandedNodeIds((prev) => new Set(prev).add(nodeId));
setExploreHistory((prev) => [
...prev,
{ nodeId, depth: nextDepth, addedNodeIds, addedEdgeIds, timestamp: Date.now() },
]);
},
[filteredEdges, nodeById, nodeDepthMap, visibleEdgeIds, visibleNodeIds],
);
const undoExploreStep = useCallback(() => {
if (exploreHistory.length <= 1) {
resetExplore();
return;
}
const nextHistory = exploreHistory.slice(0, -1);
setExploreHistory(nextHistory);
rebuildExploreState(nextHistory);
}, [exploreHistory, rebuildExploreState, resetExplore]);
useEffect(() => {
if (graphMode === "explore") resetExplore();
}, [
disabledNodeKinds,
disabledEdgeLabels,
hiddenNodeIds,
searchKeyword,
resetExplore,
]);
const displayedGraph = useMemo(() => {
if (graphMode === "full") return { nodes: filteredNodes, edges: filteredEdges };
if (!exploreRootId) return { nodes: filteredNodes, edges: [] };
return {
nodes: allNodes.filter((node) => visibleNodeIds.has(node.id)),
edges: allEdges.filter((edge) => visibleEdgeIds.has(getEdgeId(edge))),
};
}, [
allEdges,
allNodes,
exploreRootId,
filteredEdges,
filteredNodes,
graphMode,
visibleEdgeIds,
visibleNodeIds,
]);
const searchMatchIds = useMemo(() => {
const keyword = searchKeyword.trim().toLowerCase();
if (!keyword) return null;
@@ -146,7 +283,7 @@ export default function KnowledgeGraphPage() {
if (!selectedNodeId) return [];
const items: Array<{ node: (typeof allNodes)[number]; relation: string }> = [];
const seen = new Set<string>();
for (const e of filteredEdges) {
for (const e of displayedGraph.edges) {
const otherId =
e.source === selectedNodeId
? e.target
@@ -162,7 +299,7 @@ export default function KnowledgeGraphPage() {
return items.sort(
(a, b) => (degrees.get(b.node.id) ?? 0) - (degrees.get(a.node.id) ?? 0),
);
}, [selectedNodeId, filteredEdges, nodeById, degrees, allNodes]);
}, [selectedNodeId, displayedGraph.edges, nodeById, degrees, allNodes]);
// Note detail for note-type nodes (summary / tags / timestamps / status).
const noteId =
@@ -172,14 +309,44 @@ export default function KnowledgeGraphPage() {
const { data: note, isLoading: noteLoading } = useKnowledgeNote(noteId);
// --------------------------------------------------------------- handlers
const handleSelectNode = useCallback((id: string | null) => {
setSelectedNodeId(id);
}, []);
const handleSelectNode = useCallback(
(id: string | null) => {
if (!id) {
setSelectedNodeId(null);
setShowDetail(false);
return;
}
const handlePickNode = useCallback((id: string) => {
setSelectedNodeId(id);
canvasRef.current?.focusNode(id);
}, []);
setSelectedNodeId(id);
setShowDetail(true);
if (graphMode !== "explore") return;
if (!exploreRootId) {
startExploreFromNode(id);
return;
}
if (!visibleNodeIds.has(id)) return;
if (!expandedNodeIds.has(id)) expandOneHop(id);
},
[
expandedNodeIds,
expandOneHop,
exploreRootId,
graphMode,
startExploreFromNode,
visibleNodeIds,
],
);
const handlePickNode = useCallback(
(id: string) => {
setSelectedNodeId(id);
setShowDetail(true);
if (graphMode === "explore" && !exploreRootId) startExploreFromNode(id);
canvasRef.current?.focusNode(id);
},
[exploreRootId, graphMode, startExploreFromNode],
);
const handleSearchEnter = useCallback(() => {
if (!searchMatchIds || searchMatchIds.size === 0) return;
@@ -205,10 +372,14 @@ export default function KnowledgeGraphPage() {
});
}, []);
const handleHideNode = useCallback((id: string) => {
setHiddenNodeIds((prev) => new Set(prev).add(id));
setSelectedNodeId((current) => (current === id ? null : current));
}, []);
const handleHideNode = useCallback(
(id: string) => {
setHiddenNodeIds((prev) => new Set(prev).add(id));
setSelectedNodeId((current) => (current === id ? null : current));
if (selectedNodeId === id) setShowDetail(false);
},
[selectedNodeId],
);
const handleRestoreHidden = useCallback(() => {
setHiddenNodeIds(new Set());
@@ -238,6 +409,35 @@ export default function KnowledgeGraphPage() {
[navigate],
);
const handleSetExploreRoot = useCallback(
(id: string) => {
setGraphMode("explore");
startExploreFromNode(id);
setSelectedNodeId(id);
setShowDetail(true);
},
[startExploreFromNode],
);
const exploreDepth = useMemo(() => {
let max = 0;
for (const depth of nodeDepthMap.values()) max = Math.max(max, depth);
return max;
}, [nodeDepthMap]);
const exploreRootTitle = exploreRootId
? getNodeTitle(nodeById.get(exploreRootId) ?? { id: exploreRootId })
: null;
const selectedExploreInfo =
graphMode === "explore" && selectedNodeId
? {
depth: nodeDepthMap.get(selectedNodeId) ?? null,
expanded: expandedNodeIds.has(selectedNodeId),
isRoot: selectedNodeId === exploreRootId,
}
: null;
// ---------------------------------------------------------------- overlay
let overlay: React.ReactNode = null;
if (isLoading) {
@@ -284,6 +484,50 @@ export default function KnowledgeGraphPage() {
const toolbar = (
<>
<div className="kg-floating-pill">
<span className="kg-pill-label">模式</span>
<button
type="button"
className={`kg-pill-button${graphMode === "explore" ? " kg-pill-button-active" : ""}`}
onClick={() => setGraphMode("explore")}
>
探索模式
</button>
<button
type="button"
className={`kg-pill-button${graphMode === "full" ? " kg-pill-button-active" : ""}`}
onClick={() => setGraphMode("full")}
>
全量图谱
</button>
</div>
{graphMode === "explore" ? (
<>
<div className="kg-floating-pill">
起点:{exploreRootTitle ? exploreRootTitle : "未选择"}
</div>
<div className="kg-floating-pill">
深度 {exploreDepth} · 已显示 {displayedGraph.nodes.length} 节点 /{" "}
{displayedGraph.edges.length} 关系
</div>
<button
type="button"
className="kg-explore-button"
disabled={exploreHistory.length === 0}
onClick={undoExploreStep}
>
<Undo2 size={13} /> 返回上一层
</button>
<button
type="button"
className="kg-explore-button"
disabled={!exploreRootId}
onClick={resetExplore}
>
<RotateCcw size={13} /> 重置探索
</button>
</>
) : null}
<div className="kg-floating-pill">
<span className="kg-pill-label">布局</span>
<button
@@ -313,16 +557,23 @@ export default function KnowledgeGraphPage() {
</>
);
const hint =
graphMode === "explore" ? (
<div className="kg-canvas-hint">
{!exploreRootId
? "点击任意节点作为起点,开始逐层探索知识关系。"
: "点击已显示节点,可继续展开它的一跳邻居。已展开的节点和路径会保留。"}
</div>
) : null;
return (
<div className="kg-page" ref={pageRef}>
<GraphHeader
nodeCount={filteredNodes.length}
edgeCount={filteredEdges.length}
nodeCount={displayedGraph.nodes.length}
edgeCount={displayedGraph.edges.length}
totalNodes={allNodes.length}
totalEdges={allEdges.length}
refreshing={isFetching}
showSidebar={showSidebar}
showDetail={showDetail}
onBack={() => navigate("/page/workspace/knowledge")}
onRelayout={() => canvasRef.current?.relayout()}
onFitView={() => canvasRef.current?.fitView()}
@@ -331,8 +582,6 @@ export default function KnowledgeGraphPage() {
onExport={() => canvasRef.current?.exportImage()}
onFullscreen={handleFullscreen}
onRefresh={handleRefresh}
onToggleSidebar={() => setShowSidebar((v) => !v)}
onToggleDetail={() => setShowDetail((v) => !v)}
/>
<div
@@ -361,24 +610,56 @@ export default function KnowledgeGraphPage() {
onToggleEdgeLabel={handleToggleEdgeLabel}
onRestoreHidden={handleRestoreHidden}
onPickNode={handlePickNode}
onClose={() => setShowSidebar(false)}
/>
) : null}
<GraphCanvas
ref={canvasRef}
nodes={filteredNodes}
edges={filteredEdges}
degrees={degrees}
selectedNodeId={selectedNodeId}
searchMatchIds={searchMatchIds}
layoutType={layoutType}
showLabels={showLabels}
onSelectNode={handleSelectNode}
onTooltip={setTooltip}
onZoomChange={setZoomLevel}
toolbar={toolbar}
overlay={overlay}
/>
<div className="kg-canvas-shell">
{!showSidebar ? (
<button
type="button"
className="kg-side-rail-toggle kg-side-rail-toggle-left"
onClick={() => setShowSidebar(true)}
>
筛选
</button>
) : null}
<GraphCanvas
ref={canvasRef}
nodes={displayedGraph.nodes}
edges={displayedGraph.edges}
degrees={degrees}
selectedNodeId={selectedNodeId}
searchMatchIds={searchMatchIds}
layoutType={layoutType}
showLabels={showLabels}
onSelectNode={handleSelectNode}
onTooltip={setTooltip}
onZoomChange={setZoomLevel}
explore={
graphMode === "explore"
? {
preview: !exploreRootId,
rootId: exploreRootId,
nodeDepthMap,
expandedNodeIds,
}
: null
}
toolbar={toolbar}
hint={hint}
overlay={overlay}
/>
{!showDetail ? (
<button
type="button"
className="kg-side-rail-toggle kg-side-rail-toggle-right"
onClick={() => setShowDetail(true)}
>
详情
</button>
) : null}
</div>
{showDetail ? (
<GraphDetailPanel
@@ -387,10 +668,14 @@ export default function KnowledgeGraphPage() {
neighbors={selectedNeighbors}
note={noteId ? note : undefined}
noteLoading={Boolean(noteId) && noteLoading}
explore={selectedExploreInfo}
onViewSource={handleViewSource}
onLocate={(id) => canvasRef.current?.focusNode(id)}
onHide={handleHideNode}
onPickNeighbor={handlePickNode}
onExpandNode={expandOneHop}
onSetExploreRoot={handleSetExploreRoot}
onClose={() => setShowDetail(false)}
/>
) : null}
</div>
@@ -400,8 +685,8 @@ export default function KnowledgeGraphPage() {
edgeLabels={[...edgeLabelCounts.keys()]}
zoomLevel={zoomLevel}
selectedTitle={selectedNode ? getNodeTitle(selectedNode) : null}
visibleNodes={filteredNodes.length}
visibleEdges={filteredEdges.length}
visibleNodes={displayedGraph.nodes.length}
visibleEdges={displayedGraph.edges.length}
/>
<GraphTooltip tooltip={tooltip} />