diff --git a/ui/web/src/pages/knowledge-graph/knowledge-graph-page.tsx b/ui/web/src/pages/knowledge-graph/knowledge-graph-page.tsx index 0afa2d36..8b867bff 100644 --- a/ui/web/src/pages/knowledge-graph/knowledge-graph-page.tsx +++ b/ui/web/src/pages/knowledge-graph/knowledge-graph-page.tsx @@ -96,7 +96,7 @@ export function KnowledgeGraphPage() { } /> ) : ( - + )} diff --git a/ui/web/src/pages/memory/hooks/use-knowledge-graph.ts b/ui/web/src/pages/memory/hooks/use-knowledge-graph.ts index 765c7dc1..2eb4e659 100644 --- a/ui/web/src/pages/memory/hooks/use-knowledge-graph.ts +++ b/ui/web/src/pages/memory/hooks/use-knowledge-graph.ts @@ -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(`/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 = { limit: "200" }; + const params: Record = { limit: "80" }; if (userId) params.user_id = userId; return http.get(`/v1/agents/${agentId}/kg/graph`, params); }, enabled: !!agentId, - placeholderData: (prev) => prev, }); return { diff --git a/ui/web/src/pages/memory/kg-entities-tab.tsx b/ui/web/src/pages/memory/kg-entities-tab.tsx index 60830572..d85e930a 100644 --- a/ui/web/src/pages/memory/kg-entities-tab.tsx +++ b/ui/web/src/pages/memory/kg-entities-tab.tsx @@ -63,8 +63,8 @@ export function KGEntitiesTab({ agentId, userId }: KGEntitiesTabProps) { return (
- {/* Search + stats + actions — single compact row */} -
+ {/* Toolbar row: search + actions */} +
setSearchQuery(e.target.value)} @@ -81,17 +81,6 @@ export function KGEntitiesTab({ agentId, userId }: KGEntitiesTabProps) { )} - {/* Inline stats */} - {stats && ( -
- {t("kg.stats.entities", { count: stats.entity_count })} - {t("kg.stats.relations", { count: stats.relation_count })} - {Object.entries(stats.entity_types).map(([type, count]) => ( - {type}: {count} - ))} -
- )} -
{/* View mode toggle */} @@ -115,7 +104,7 @@ export function KGEntitiesTab({ agentId, userId }: KGEntitiesTabProps) {
+ {/* Stats row — separate line for entity type breakdown */} + {stats && ( +
+ {t("kg.stats.entities", { count: stats.entity_count })} + {t("kg.stats.relations", { count: stats.relation_count })} + | + {Object.entries(stats.entity_types).map(([type, count]) => ( + {type}: {count} + ))} +
+ )} + {/* Content area */}
{viewMode === "graph" ? ( diff --git a/ui/web/src/pages/memory/kg-graph-view.tsx b/ui/web/src/pages/memory/kg-graph-view.tsx index a79f6bc0..38a3b418 100644 --- a/ui/web/src/pages/memory/kg-graph-view.tsx +++ b/ui/web/src/pages/memory/kg-graph-view.tsx @@ -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