mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-15 02:16:56 +00:00
fix(kg): fix filter reload, graph perf, stats layout, double-click interaction
- Fix filter not reloading: remove placeholderData from entity list and graph queries so data clears immediately on filter change; add key prop to force remount on filter change - Fix graph lag/crash: reduce GRAPH_LIMIT to 80, increase Barnes-Hut theta for faster approximation, increase alphaDecay for faster convergence - Move entity type stats to separate line below toolbar (better readability) - Change node click: single-click highlights edges, double-click opens detail - Increase entity list limit to 200 (table view only, graph stays at 80)
This commit is contained in:
1 parent
c2bd5349e5
commit
9e037bca8a
4 files changed
+33
-28
No files matched your search
@@ -96,7 +96,7 @@ export function KnowledgeGraphPage() {
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<KGEntitiesTab agentId={agentId} userId={userIdFilter || undefined} />
|
||||
<KGEntitiesTab key={`${agentId}-${userIdFilter}`} agentId={agentId} userId={userIdFilter || undefined} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -27,11 +27,10 @@ export function useKnowledgeGraph(filters: KGFilters) {
|
||||
if (filters.userId) params.user_id = filters.userId;
|
||||
if (filters.entityType) params.type = filters.entityType;
|
||||
if (filters.query) params.q = filters.query;
|
||||
params.limit = "100";
|
||||
params.limit = "200";
|
||||
return (await http.get<KGEntity[]>(`/v1/agents/${filters.agentId}/kg/entities`, params)) ?? [];
|
||||
},
|
||||
enabled: !!filters.agentId,
|
||||
placeholderData: (prev) => prev,
|
||||
});
|
||||
|
||||
const entities = data ?? [];
|
||||
@@ -141,12 +140,11 @@ export function useKGGraph(agentId: string, userId?: string) {
|
||||
queryKey: queryKeys.kg.graph(agentId, userId),
|
||||
queryFn: async () => {
|
||||
if (!agentId) return { entities: [], relations: [] };
|
||||
const params: Record<string, string> = { limit: "200" };
|
||||
const params: Record<string, string> = { limit: "80" };
|
||||
if (userId) params.user_id = userId;
|
||||
return http.get<KGGraphData>(`/v1/agents/${agentId}/kg/graph`, params);
|
||||
},
|
||||
enabled: !!agentId,
|
||||
placeholderData: (prev) => prev,
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
@@ -63,8 +63,8 @@ export function KGEntitiesTab({ agentId, userId }: KGEntitiesTabProps) {
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
{/* Search + stats + actions — single compact row */}
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
{/* Toolbar row: search + actions */}
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Input
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
@@ -81,17 +81,6 @@ export function KGEntitiesTab({ agentId, userId }: KGEntitiesTabProps) {
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Inline stats */}
|
||||
{stats && (
|
||||
<div className="flex gap-2 text-[10px] text-muted-foreground ml-1">
|
||||
<span>{t("kg.stats.entities", { count: stats.entity_count })}</span>
|
||||
<span>{t("kg.stats.relations", { count: stats.relation_count })}</span>
|
||||
{Object.entries(stats.entity_types).map(([type, count]) => (
|
||||
<span key={type}>{type}: {count}</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex-1" />
|
||||
|
||||
{/* View mode toggle */}
|
||||
@@ -115,7 +104,7 @@ export function KGEntitiesTab({ agentId, userId }: KGEntitiesTabProps) {
|
||||
</div>
|
||||
|
||||
<Button variant="outline" size="sm" onClick={() => refresh()} disabled={fetching} className="gap-1 h-8 px-2.5">
|
||||
<RefreshCw className={"h-3.5 w-3.5" + (fetching ? " animate-spin" : "")} />
|
||||
<RefreshCw className={`h-3.5 w-3.5${fetching ? " animate-spin" : ""}`} />
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onClick={() => setDedupOpen(true)} className="gap-1 h-8 px-2.5">
|
||||
<Merge className="h-3.5 w-3.5" /> {t("kg.dedup.button")}
|
||||
@@ -125,6 +114,18 @@ export function KGEntitiesTab({ agentId, userId }: KGEntitiesTabProps) {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Stats row — separate line for entity type breakdown */}
|
||||
{stats && (
|
||||
<div className="flex flex-wrap gap-x-3 gap-y-1 mb-3 text-[10px] text-muted-foreground">
|
||||
<span className="font-medium">{t("kg.stats.entities", { count: stats.entity_count })}</span>
|
||||
<span className="font-medium">{t("kg.stats.relations", { count: stats.relation_count })}</span>
|
||||
<span className="text-muted-foreground/50">|</span>
|
||||
{Object.entries(stats.entity_types).map(([type, count]) => (
|
||||
<span key={type}>{type}: {count}</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Content area */}
|
||||
<div className="min-h-0 flex-1">
|
||||
{viewMode === "graph" ? (
|
||||
|
||||
@@ -19,7 +19,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { useUiStore } from "@/stores/use-ui-store";
|
||||
import type { KGEntity, KGRelation } from "@/types/knowledge-graph";
|
||||
|
||||
const GRAPH_LIMIT = 200;
|
||||
const GRAPH_LIMIT = 80;
|
||||
|
||||
// Dual-theme palette — separate dark/light values for readability on both backgrounds
|
||||
interface TypeColor {
|
||||
@@ -124,18 +124,20 @@ function computeForceLayout(nodes: Node[], edges: Edge[], entities: KGEntity[]):
|
||||
const chargeMul = n > 150 ? -350 : n > 100 ? -300 : n > 60 ? -250 : n > 30 ? -180 : -120;
|
||||
const centerPull = n > 150 ? 0.015 : n > 100 ? 0.02 : n > 60 ? 0.03 : n > 30 ? 0.05 : 0.08;
|
||||
const collideBase = n > 150 ? 60 : n > 100 ? 55 : n > 60 ? 50 : n > 30 ? 40 : 35;
|
||||
// Barnes-Hut theta: higher = faster approximation, slightly less accurate
|
||||
const theta = n > 60 ? 1.2 : 0.9;
|
||||
const charge = forceManyBody().strength((d: any) => chargeMul * (d.mass ?? 1)).theta(theta);
|
||||
const simulation = forceSimulation(simNodes)
|
||||
.force("link", forceLink(simLinks).id((d: any) => d.id).distance(linkDist).strength(0.5))
|
||||
.force("charge", forceManyBody().strength((d: any) => chargeMul * (d.mass ?? 1)))
|
||||
.force("charge", charge)
|
||||
.force("center", forceCenter(w / 2, h / 2))
|
||||
.force("x", forceX(w / 2).strength(centerPull))
|
||||
.force("y", forceY(h / 2).strength(centerPull))
|
||||
.force("collide", forceCollide().radius((d: any) => collideBase + (d.mass ?? 1) * 3).strength(0.7))
|
||||
.alphaDecay(n > 60 ? 0.05 : 0.0228) // faster convergence for large graphs
|
||||
.stop();
|
||||
const ticks = Math.ceil(Math.log(simulation.alphaMin()) / Math.log(1 - simulation.alphaDecay()));
|
||||
// Cap ticks for large graphs — diminishing returns past 200 iterations
|
||||
const maxTicks = n > 100 ? 200 : ticks;
|
||||
for (let i = 0; i < Math.min(ticks, maxTicks); i++) simulation.tick();
|
||||
for (let i = 0; i < ticks; i++) simulation.tick();
|
||||
return nodes.map((n, i) => ({ ...n, position: { x: simNodes[i]!.x ?? 0, y: simNodes[i]!.y ?? 0 } }));
|
||||
}
|
||||
|
||||
@@ -252,12 +254,16 @@ function KGGraphViewInner({ entities: allEntities, relations: allRelations, onEn
|
||||
|
||||
const [, startTransition] = useTransition();
|
||||
|
||||
// Single click: highlight connected edges only
|
||||
const handleNodeClick = useCallback((_: React.MouseEvent, node: Node) => {
|
||||
startTransition(() => setSelectedNodeId((prev) => (prev === node.id ? null : node.id)));
|
||||
// Defer dialog open to next frame — decouples edge restyle from dialog mount cost
|
||||
}, [startTransition]);
|
||||
|
||||
// Double click: open entity detail modal
|
||||
const handleNodeDoubleClick = useCallback((_: React.MouseEvent, node: Node) => {
|
||||
const entity = entityMap.get(node.id);
|
||||
if (entity && onEntityClick) setTimeout(() => onEntityClick(entity), 0);
|
||||
}, [entityMap, onEntityClick, startTransition]);
|
||||
if (entity && onEntityClick) onEntityClick(entity);
|
||||
}, [entityMap, onEntityClick]);
|
||||
|
||||
const handlePaneClick = useCallback(() => {
|
||||
startTransition(() => setSelectedNodeId(null));
|
||||
@@ -277,7 +283,7 @@ function KGGraphViewInner({ entities: allEntities, relations: allRelations, onEn
|
||||
<ReactFlow
|
||||
nodes={nodes} edges={edges}
|
||||
onNodesChange={onNodesChange} onEdgesChange={onEdgesChange}
|
||||
onNodeClick={handleNodeClick} onPaneClick={handlePaneClick}
|
||||
onNodeClick={handleNodeClick} onNodeDoubleClick={handleNodeDoubleClick} onPaneClick={handlePaneClick}
|
||||
nodeTypes={nodeTypes} colorMode={colorMode}
|
||||
minZoom={0.1} maxZoom={3}
|
||||
nodesConnectable={false} nodesDraggable={true} elementsSelectable={true}
|
||||
|
||||
Reference in new issue
Block a user