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"
/>
-
-
refresh()} disabled={fetching} className="gap-1 h-9">
- {t("kg.refresh")}
+ refresh()} disabled={fetching} className="gap-1 h-8 px-2.5">
+
- setExtractOpen(true)} className="gap-1 h-9">
+ setExtractOpen(true)} className="gap-1 h-8 px-2.5">
{t("kg.extract")}
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 && (
-
- setActiveTab("documents")}
- >
- {t("tabs.documents")}
-
- setActiveTab("kg")}
- >
- {t("tabs.knowledgeGraph")}
-
-
- )}
-
- {/* 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 */}