diff --git a/ui/web/src/i18n/locales/en/memory.json b/ui/web/src/i18n/locales/en/memory.json index 4643a373..6370b407 100644 --- a/ui/web/src/i18n/locales/en/memory.json +++ b/ui/web/src/i18n/locales/en/memory.json @@ -12,7 +12,8 @@ "agent": "Agent", "allAgents": "All agents", "scope": "Scope", - "allScope": "All (global + personal)" + "allScope": "All (global + personal)", + "selectAgent": "Select an agent" }, "tabs": { "documents": "Documents", @@ -85,6 +86,10 @@ "path": "Path:" }, "kg": { + "pageTitle": "Knowledge Graph", + "pageDescription": "Explore entities and relationships extracted from agent memory.", + "selectAgentTitle": "Select an agent", + "selectAgentDescription": "Choose an agent to view its knowledge graph.", "emptyTitle": "No entities", "emptyDescription": "No knowledge graph entities for this agent yet.", "emptySearchDescription": "No entities match your search.", diff --git a/ui/web/src/i18n/locales/vi/memory.json b/ui/web/src/i18n/locales/vi/memory.json index 6fae75b5..7dc0a353 100644 --- a/ui/web/src/i18n/locales/vi/memory.json +++ b/ui/web/src/i18n/locales/vi/memory.json @@ -12,7 +12,8 @@ "agent": "Agent", "allAgents": "Tất cả agent", "scope": "Phạm vi", - "allScope": "Tất cả (toàn cục + cá nhân)" + "allScope": "Tất cả (toàn cục + cá nhân)", + "selectAgent": "Chọn agent" }, "tabs": { "documents": "Tài liệu", @@ -85,6 +86,10 @@ "path": "Đường dẫn:" }, "kg": { + "pageTitle": "Knowledge Graph", + "pageDescription": "Khám phá các thực thể và quan hệ được trích xuất từ bộ nhớ agent.", + "selectAgentTitle": "Chọn agent", + "selectAgentDescription": "Chọn agent để xem đồ thị tri thức.", "emptyTitle": "Không có thực thể", "emptyDescription": "Chưa có thực thể đồ thị tri thức nào cho agent này.", "emptySearchDescription": "Không có thực thể nào khớp với tìm kiếm của bạn.", diff --git a/ui/web/src/i18n/locales/zh/memory.json b/ui/web/src/i18n/locales/zh/memory.json index 21db1aa8..ab1c1474 100644 --- a/ui/web/src/i18n/locales/zh/memory.json +++ b/ui/web/src/i18n/locales/zh/memory.json @@ -12,7 +12,8 @@ "agent": "Agent", "allAgents": "所有Agent", "scope": "范围", - "allScope": "全部(全局 + 个人)" + "allScope": "全部(全局 + 个人)", + "selectAgent": "选择Agent" }, "tabs": { "documents": "文档", @@ -85,6 +86,10 @@ "path": "路径:" }, "kg": { + "pageTitle": "Knowledge Graph", + "pageDescription": "探索从Agent记忆中提取的实体和关系。", + "selectAgentTitle": "选择Agent", + "selectAgentDescription": "选择Agent以查看其知识图谱。", "emptyTitle": "暂无实体", "emptyDescription": "该Agent暂无知识图谱实体。", "emptySearchDescription": "没有与您搜索匹配的实体。", diff --git a/ui/web/src/lib/constants.ts b/ui/web/src/lib/constants.ts index 855d7290..796f4790 100644 --- a/ui/web/src/lib/constants.ts +++ b/ui/web/src/lib/constants.ts @@ -33,6 +33,7 @@ export const ROUTES = { STORAGE: "/storage", PENDING_MESSAGES: "/pending-messages", MEMORY: "/memory", + KNOWLEDGE_GRAPH: "/knowledge-graph", SETUP: "/setup", } as const; diff --git a/ui/web/src/pages/knowledge-graph/knowledge-graph-page.tsx b/ui/web/src/pages/knowledge-graph/knowledge-graph-page.tsx new file mode 100644 index 00000000..4fb14fce --- /dev/null +++ b/ui/web/src/pages/knowledge-graph/knowledge-graph-page.tsx @@ -0,0 +1,60 @@ +import { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Network } from "lucide-react"; +import { EmptyState } from "@/components/shared/empty-state"; +import { useAgents } from "@/pages/agents/hooks/use-agents"; +import { KGEntitiesTab } from "@/pages/memory/kg-entities-tab"; + +export function KnowledgeGraphPage() { + const { t } = useTranslation("memory"); + const { agents } = useAgents(); + const [agentId, setAgentId] = useState(""); + const [userIdFilter, setUserIdFilter] = useState(""); + + return ( +
+ {/* Header + filters in one row */} +
+
+

{t("kg.pageTitle")}

+

{t("kg.pageDescription")}

+
+ + {agentId && ( + setUserIdFilter(e.target.value)} + placeholder={t("filters.allScope")} + className="h-8 rounded-md border bg-background px-2 text-sm w-[160px]" + /> + )} +
+ + {/* Content */} +
+ {!agentId ? ( + + ) : ( + + )} +
+
+ ); +} diff --git a/ui/web/src/pages/memory/kg-entities-tab.tsx b/ui/web/src/pages/memory/kg-entities-tab.tsx index a1a885cc..c7e1570c 100644 --- a/ui/web/src/pages/memory/kg-entities-tab.tsx +++ b/ui/web/src/pages/memory/kg-entities-tab.tsx @@ -61,34 +61,35 @@ export function KGEntitiesTab({ agentId, userId }: KGEntitiesTabProps) { return (
- {/* Stats bar */} - {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} - ))} -
- )} - - {/* Search + actions */} -
+ {/* Search + stats + actions — single compact row */} +
setSearchQuery(e.target.value)} onKeyDown={handleKeyDown} placeholder={t("kg.search.placeholder")} - className="max-w-sm" + className="max-w-[220px] h-8 text-xs" /> - {appliedQuery && ( - )} + + {/* 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 */} @@ -97,7 +98,7 @@ export function KGEntitiesTab({ agentId, userId }: KGEntitiesTabProps) { variant={viewMode === "table" ? "secondary" : "ghost"} size="sm" onClick={() => setViewMode("table")} - className="h-9 rounded-r-none gap-1 px-2.5" + className="h-8 rounded-r-none px-2" > @@ -105,16 +106,16 @@ export function KGEntitiesTab({ agentId, userId }: KGEntitiesTabProps) { variant={viewMode === "graph" ? "secondary" : "ghost"} size="sm" onClick={() => setViewMode("graph")} - className="h-9 rounded-l-none gap-1 px-2.5" + className="h-8 rounded-l-none px-2" >
- -
diff --git a/ui/web/src/pages/memory/kg-graph-view.tsx b/ui/web/src/pages/memory/kg-graph-view.tsx index 54001b8c..0700f857 100644 --- a/ui/web/src/pages/memory/kg-graph-view.tsx +++ b/ui/web/src/pages/memory/kg-graph-view.tsx @@ -1,8 +1,10 @@ import { useMemo, useEffect, useCallback, useRef } from "react"; import { ReactFlow, + ReactFlowProvider, useNodesState, useEdgesState, + useReactFlow, Background, Controls, MiniMap, @@ -88,6 +90,7 @@ function applyForceLayout( nodes: Node[], edges: Edge[], onUpdate: (positioned: Node[]) => void, + onSettled?: () => void, ) { if (nodes.length === 0) return () => {}; @@ -113,6 +116,8 @@ function applyForceLayout( onUpdate(positioned); }); + simulation.on("end", () => onSettled?.()); + // Run fast to settle simulation.alpha(1).restart(); @@ -125,8 +130,17 @@ interface KGGraphViewProps { onEntityClick?: (entity: KGEntity) => void; } -export function KGGraphView({ entities, relations, onEntityClick }: KGGraphViewProps) { +export function KGGraphView(props: KGGraphViewProps) { + return ( + + + + ); +} + +function KGGraphViewInner({ entities, relations, onEntityClick }: KGGraphViewProps) { const { t } = useTranslation("memory"); + const { fitView } = useReactFlow(); const { nodes: initialNodes, edges: initialEdges } = useMemo( () => buildGraph(entities, relations), [entities, relations], @@ -136,13 +150,18 @@ export function KGGraphView({ entities, relations, onEntityClick }: KGGraphViewP const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges); const simRef = useRef<(() => void) | null>(null); + // Fit view after simulation settles so the graph covers the viewport + const handleSettled = useCallback(() => { + requestAnimationFrame(() => fitView({ padding: 0.15, duration: 300 })); + }, [fitView]); + // Run force layout when data changes useEffect(() => { setEdges(initialEdges); simRef.current?.(); - simRef.current = applyForceLayout(initialNodes, initialEdges, setNodes); + simRef.current = applyForceLayout(initialNodes, initialEdges, setNodes, handleSettled); return () => simRef.current?.(); - }, [initialNodes, initialEdges, setNodes, setEdges]); + }, [initialNodes, initialEdges, setNodes, setEdges, handleSettled]); const handleNodeClick = useCallback( (_: React.MouseEvent, node: Node) => { @@ -171,8 +190,8 @@ export function KGGraphView({ entities, relations, onEntityClick }: KGGraphViewP onNodeClick={handleNodeClick} nodeTypes={nodeTypes} fitView - minZoom={0.2} - maxZoom={2} + minZoom={0.1} + maxZoom={3} proOptions={{ hideAttribution: true }} > diff --git a/ui/web/src/pages/memory/memory-page.tsx b/ui/web/src/pages/memory/memory-page.tsx index 9f65acaf..56977dae 100644 --- a/ui/web/src/pages/memory/memory-page.tsx +++ b/ui/web/src/pages/memory/memory-page.tsx @@ -1,6 +1,6 @@ import { useState, useMemo } from "react"; import { useTranslation } from "react-i18next"; -import { Brain, Plus, RefreshCw, Search, Database, Trash2, RotateCw, Network } from "lucide-react"; +import { Brain, Plus, RefreshCw, Search, Database, Trash2, RotateCw } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { Label } from "@/components/ui/label"; @@ -14,7 +14,6 @@ import { useMemoryDocuments } from "./hooks/use-memory"; import { MemoryDocumentDialog } from "./memory-document-dialog"; import { MemoryCreateDialog } from "./memory-create-dialog"; import { MemorySearchDialog } from "./memory-search-dialog"; -import { KGEntitiesTab } from "./kg-entities-tab"; import { useMinLoading } from "@/hooks/use-min-loading"; import { useDeferredLoading } from "@/hooks/use-deferred-loading"; import type { MemoryDocument } from "@/types/memory"; @@ -31,7 +30,6 @@ export function MemoryPage() { const [deleteLoading, setDeleteLoading] = useState(false); const [searchOpen, setSearchOpen] = useState(false); const [indexAllLoading, setIndexAllLoading] = useState(false); - const [activeTab, setActiveTab] = useState<"documents" | "kg">("documents"); const [page, setPage] = useState(1); const [pageSize, setPageSize] = useState(20); @@ -172,33 +170,8 @@ export function MemoryPage() { )}
- {/* Tab bar (only when agent selected) */} - {agentId && ( -
- - -
- )} - - {/* KG tab */} - {agentId && activeTab === "kg" && ( -
- -
- )} - {/* Document table */} -
+
{showSkeleton ? ( ) : documents.length === 0 ? ( diff --git a/ui/web/src/routes.tsx b/ui/web/src/routes.tsx index 43666d74..a89ab120 100644 --- a/ui/web/src/routes.tsx +++ b/ui/web/src/routes.tsx @@ -84,6 +84,9 @@ const PendingMessagesPage = lazy(() => const MemoryPage = lazy(() => import("@/pages/memory/memory-page").then((m) => ({ default: m.MemoryPage })), ); +const KnowledgeGraphPage = lazy(() => + import("@/pages/knowledge-graph/knowledge-graph-page").then((m) => ({ default: m.KnowledgeGraphPage })), +); const ContactsPage = lazy(() => import("@/pages/contacts/contacts-page").then((m) => ({ default: m.ContactsPage })), ); @@ -156,6 +159,7 @@ export function AppRoutes() { } /> } /> } /> + } /> {/* Catch-all → overview */}