feat: 智能体创建流程优化与管理员记忆页面

- 创建智能体页面:保存后自动跳转列表页、技能列表增加搜索过滤框
- Bootstrap 模式:开放全部技能可见性,注入专属提示词(skill_list 匹配意图、禁止 skill_manage)
- 管理员记忆页面:新增 AdminMemoryPage 及路由,支持查看/编辑用户记忆条目
- 内存配置与管理员用户接口同步更新

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
xuhdandClaude Sonnet 4.6 committed 2026-05-21 11:43:59 +08:00
1 parent 18c2a4122f
commit 3748513996
12 files changed
+692 -143

No files matched your search

@@ -0,0 +1,371 @@
"use client";
import { useState } from "react";
import { Check, Loader2, Pencil, Plus, Trash2, X } from "lucide-react";
import { toast } from "sonner";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { ScrollArea } from "@/components/ui/scroll-area";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Textarea } from "@/components/ui/textarea";
import {
useAdminAddMemoryEntry,
useAdminDeleteMemoryEntry,
useAdminReplaceMemoryEntry,
useUserMemory,
} from "@/core/admin/hooks";
import { useAllUsers } from "@/core/auth/hooks";
function getEntries(data: Record<string, unknown>): string[] {
const raw = (data as { entries?: unknown }).entries;
if (!Array.isArray(raw)) return [];
return raw.filter((e): e is string => typeof e === "string" && e.trim().length > 0);
}
// ── Memory entry list with inline editing ──────────────────────────────────
function MemoryEntryList({
entries,
userId,
agent,
}: {
entries: string[];
userId: string;
agent: string | null;
}) {
const [editIndex, setEditIndex] = useState<number | null>(null);
const [editDraft, setEditDraft] = useState("");
const [addDraft, setAddDraft] = useState("");
const [isAdding, setIsAdding] = useState(false);
const addMutation = useAdminAddMemoryEntry(userId);
const replaceMutation = useAdminReplaceMemoryEntry(userId);
const deleteMutation = useAdminDeleteMemoryEntry(userId);
const isPending =
addMutation.isPending || replaceMutation.isPending || deleteMutation.isPending;
function startEdit(index: number) {
setEditIndex(index);
setEditDraft(entries[index] ?? "");
}
function cancelEdit() {
setEditIndex(null);
setEditDraft("");
}
async function confirmEdit(index: number) {
const oldText = entries[index];
if (!oldText || editDraft.trim() === oldText.trim()) {
cancelEdit();
return;
}
try {
await replaceMutation.mutateAsync({ oldText, content: editDraft.trim(), agent });
cancelEdit();
} catch (err) {
toast.error(err instanceof Error ? err.message : "修改失败");
}
}
async function handleDelete(entry: string) {
try {
await deleteMutation.mutateAsync({ oldText: entry, agent });
} catch (err) {
toast.error(err instanceof Error ? err.message : "删除失败");
}
}
async function handleAdd() {
const content = addDraft.trim();
if (!content) return;
try {
await addMutation.mutateAsync({ content, agent });
setAddDraft("");
setIsAdding(false);
} catch (err) {
toast.error(err instanceof Error ? err.message : "新增失败");
}
}
return (
<div className="flex flex-col gap-2">
{entries.length === 0 && !isAdding && (
<p className="text-muted-foreground py-6 text-center text-sm">暂无记忆条目</p>
)}
{entries.map((entry, i) => (
<div key={i} className="group rounded-md border p-3">
{editIndex === i ? (
<div className="flex flex-col gap-2">
<Textarea
value={editDraft}
onChange={(e) => setEditDraft(e.target.value)}
className="min-h-[80px] text-sm"
autoFocus
/>
<div className="flex gap-2">
<Button
size="sm"
onClick={() => confirmEdit(i)}
disabled={replaceMutation.isPending}
>
{replaceMutation.isPending ? (
<Loader2 className="size-3 animate-spin" />
) : (
<Check className="size-3" />
)}
保存
</Button>
<Button size="sm" variant="ghost" onClick={cancelEdit}>
<X className="size-3" />
取消
</Button>
</div>
</div>
) : (
<div className="flex items-start gap-2">
<span className="text-muted-foreground mt-0.5 shrink-0 text-sm">·</span>
<span className="flex-1 whitespace-pre-wrap text-sm">{entry}</span>
<div className="flex shrink-0 gap-1 opacity-0 transition-opacity group-hover:opacity-100">
<Button
size="icon"
variant="ghost"
className="size-7"
onClick={() => startEdit(i)}
disabled={isPending}
>
<Pencil className="size-3" />
</Button>
<Button
size="icon"
variant="ghost"
className="size-7 hover:text-destructive"
onClick={() => handleDelete(entry)}
disabled={isPending}
>
{deleteMutation.isPending ? (
<Loader2 className="size-3 animate-spin" />
) : (
<Trash2 className="size-3" />
)}
</Button>
</div>
</div>
)}
</div>
))}
{isAdding ? (
<div className="flex flex-col gap-2 rounded-md border p-3">
<Textarea
placeholder="输入新记忆条目内容…"
value={addDraft}
onChange={(e) => setAddDraft(e.target.value)}
className="min-h-[80px] text-sm"
autoFocus
/>
<div className="flex gap-2">
<Button size="sm" onClick={handleAdd} disabled={addMutation.isPending || !addDraft.trim()}>
{addMutation.isPending ? <Loader2 className="size-3 animate-spin" /> : <Check className="size-3" />}
确认
</Button>
<Button size="sm" variant="ghost" onClick={() => { setIsAdding(false); setAddDraft(""); }}>
<X className="size-3" />
取消
</Button>
</div>
</div>
) : (
<Button
variant="outline"
size="sm"
className="mt-1 w-full"
onClick={() => setIsAdding(true)}
disabled={isPending}
>
<Plus className="size-3" />
新增条目
</Button>
)}
</div>
);
}
// ── Right panel: memory tabs for a selected user ───────────────────────────
function UserMemoryPanel({ userId }: { userId: string }) {
const { data: memory, isLoading } = useUserMemory(userId);
const [selectedAgent, setSelectedAgent] = useState<string | null>(null);
if (isLoading) {
return (
<div className="text-muted-foreground flex flex-1 items-center justify-center gap-2 text-sm">
<Loader2 className="size-4 animate-spin" />
加载中…
</div>
);
}
const globalBucket = memory?.buckets.find((b) => b.name === "global");
const agentBuckets = (memory?.buckets ?? []).filter((b) => b.name !== "global");
const userEntries = globalBucket ? getEntries(globalBucket.data) : [];
const currentAgent = selectedAgent ?? agentBuckets[0]?.name ?? null;
const agentBucket = agentBuckets.find((b) => b.name === currentAgent);
const agentEntries = agentBucket ? getEntries(agentBucket.data) : [];
return (
<Tabs defaultValue="user" className="flex flex-1 flex-col overflow-hidden">
<div className="border-b px-4 pt-4">
<TabsList variant="line">
<TabsTrigger value="user">
用户画像
{userEntries.length > 0 && (
<Badge variant="secondary" className="ml-1.5 text-xs">
{userEntries.length}
</Badge>
)}
</TabsTrigger>
<TabsTrigger value="agent">
Agent 记忆
{agentBuckets.length > 0 && (
<Badge variant="secondary" className="ml-1.5 text-xs">
{agentBuckets.length}
</Badge>
)}
</TabsTrigger>
</TabsList>
</div>
<TabsContent value="user" className="flex-1 overflow-hidden p-0">
<ScrollArea className="h-full">
<div className="p-4">
<p className="text-muted-foreground mb-3 text-xs">
USER.md — 跨 Agent 共享的用户画像
</p>
<MemoryEntryList entries={userEntries} userId={userId} agent={null} />
</div>
</ScrollArea>
</TabsContent>
<TabsContent value="agent" className="flex-1 overflow-hidden p-0">
<ScrollArea className="h-full">
<div className="p-4">
{agentBuckets.length === 0 ? (
<p className="text-muted-foreground py-6 text-center text-sm">该用户暂无 Agent 记忆</p>
) : (
<>
<div className="mb-3 flex items-center gap-2">
<span className="text-muted-foreground text-xs">选择 Agent:</span>
<Select
value={currentAgent ?? ""}
onValueChange={(v) => setSelectedAgent(v)}
>
<SelectTrigger className="h-7 w-48 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
{agentBuckets.map((b) => (
<SelectItem key={b.name} value={b.name} className="text-xs">
{b.display_name || b.name}
</SelectItem>
))}
</SelectContent>
</Select>
<span className="text-muted-foreground text-xs">
— MEMORY.md
</span>
</div>
{currentAgent && (
<MemoryEntryList
entries={agentEntries}
userId={userId}
agent={currentAgent}
/>
)}
</>
)}
</div>
</ScrollArea>
</TabsContent>
</Tabs>
);
}
// ── Main page ──────────────────────────────────────────────────────────────
export default function AdminMemoryPage() {
const { data: users, isLoading: usersLoading } = useAllUsers(true);
const [selectedUserId, setSelectedUserId] = useState<string | null>(null);
const [userQuery, setUserQuery] = useState("");
const filteredUsers = (users ?? []).filter((u) =>
u.email.toLowerCase().includes(userQuery.toLowerCase()),
);
return (
<div className="flex h-full">
{/* Left: user list */}
<div className="flex w-64 shrink-0 flex-col border-r">
<div className="border-b p-3">
<Input
placeholder="搜索用户…"
value={userQuery}
onChange={(e) => setUserQuery(e.target.value)}
className="h-8 text-sm"
/>
</div>
<ScrollArea className="flex-1">
{usersLoading && (
<div className="text-muted-foreground flex items-center justify-center gap-2 py-10 text-sm">
<Loader2 className="size-4 animate-spin" />
加载中…
</div>
)}
{!usersLoading && filteredUsers.length === 0 && (
<p className="text-muted-foreground py-8 text-center text-sm">暂无用户</p>
)}
{filteredUsers.map((user) => (
<button
key={user.id}
className={`flex w-full flex-col items-start gap-0.5 px-3 py-2.5 text-left text-sm transition-colors hover:bg-accent ${
selectedUserId === user.id ? "bg-accent font-medium" : ""
}`}
onClick={() => setSelectedUserId(user.id)}
>
<span className="max-w-full truncate">{user.email}</span>
{user.system_role === "admin" && (
<Badge variant="default" className="h-4 px-1 text-[10px]">
Admin
</Badge>
)}
</button>
))}
</ScrollArea>
</div>
{/* Right: memory tabs */}
<div className="flex flex-1 flex-col overflow-hidden">
{selectedUserId ? (
<UserMemoryPanel key={selectedUserId} userId={selectedUserId} />
) : (
<div className="text-muted-foreground flex flex-1 items-center justify-center text-sm">
请在左侧选择用户
</div>
)}
</div>
</div>
);
}
@@ -1,7 +1,7 @@
"use client";
import { PenLineIcon, PlusIcon, SearchIcon, Trash2Icon } from "lucide-react";
import { useId, useMemo, useState } from "react";
import { useMemo, useState } from "react";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
@@ -345,16 +345,13 @@ export function MemorySettingsPage() {
);
}
/** 记忆设置面板:用户可改的白名单字段。 */
/** 记忆设置面板:仅暴露 Hindsight 这一个可选开关,其余默认开启不展示。 */
function MemoryConfigPanel() {
const { t } = useI18n();
const m = t.settings.memory;
const { configResponse, isLoading } = useMemoryConfig();
const patchConfig = usePatchMemoryConfig();
const maxTokensId = useId();
const bankMissionId = useId();
async function patch(overrides: Record<string, unknown>) {
try {
await patchConfig.mutateAsync(overrides);
@@ -367,9 +364,7 @@ function MemoryConfigPanel() {
const config: MemoryV2Config | null = configResponse?.config ?? null;
const writable = new Set(configResponse?.writable_fields ?? []);
function isWritable(field: string) {
return writable.has(field);
}
const hindsightOn = config?.injection.include_hindsight ?? false;
return (
<SettingsSection title={m.settingsTitle} description={m.settingsDesc}>
@@ -377,123 +372,43 @@ function MemoryConfigPanel() {
<div className="text-muted-foreground text-sm">{t.common.loading}</div>
) : (
<div className="space-y-4 rounded-lg border p-4">
<ToggleRow
label={m.injectionEnabled}
description={m.injectionEnabledDesc}
checked={config.injection.enabled}
disabled={!isWritable("injection.enabled") || patchConfig.isPending}
onChange={(v) => void patch({ injection: { enabled: v } })}
/>
<ToggleRow
label={m.includeBuiltin}
description={m.includeBuiltinDesc}
checked={config.injection.include_builtin}
disabled={
!isWritable("injection.include_builtin") || patchConfig.isPending
}
onChange={(v) => void patch({ injection: { include_builtin: v } })}
/>
{/* 只暴露 Hindsight 开关,记忆总开关和本地记忆默认开启不展示 */}
<ToggleRow
label={m.includeHindsight}
description={m.includeHindsightDesc}
checked={config.injection.include_hindsight}
checked={hindsightOn}
disabled={
!isWritable("injection.include_hindsight") || patchConfig.isPending
!writable.has("injection.include_hindsight") || patchConfig.isPending
}
onChange={(v) => void patch({ injection: { include_hindsight: v } })}
/>
<div className="flex flex-wrap items-center justify-between gap-3 border-t pt-4">
<div className="min-w-0">
<label htmlFor={maxTokensId} className="text-sm font-medium">
{m.maxTokens}
</label>
<p className="text-muted-foreground text-sm">{m.maxTokensDesc}</p>
</div>
<Input
id={maxTokensId}
type="number"
min="100"
max="8000"
step="100"
className="w-32"
defaultValue={config.injection.max_tokens}
disabled={!isWritable("injection.max_tokens") || patchConfig.isPending}
onBlur={(event) => {
const value = Number(event.target.value);
if (
Number.isFinite(value) &&
value !== config.injection.max_tokens
) {
void patch({ injection: { max_tokens: value } });
}
}}
/>
</div>
{/* Hindsight 细节设置:仅在 Hindsight 开启时展示 */}
{hindsightOn && (
<>
<div className="flex flex-wrap items-center justify-between gap-3 border-t pt-4">
<div className="min-w-0">
<label className="text-sm font-medium">{m.recallBudget}</label>
<p className="text-muted-foreground text-sm">{m.recallBudgetDesc}</p>
</div>
<Select
value={config.hindsight.recall_budget}
disabled={!writable.has("hindsight.recall_budget") || patchConfig.isPending}
onValueChange={(v) => void patch({ hindsight: { recall_budget: v } })}
>
<SelectTrigger className="w-44">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="low">{m.recallBudgetLow}</SelectItem>
<SelectItem value="mid">{m.recallBudgetMid}</SelectItem>
<SelectItem value="high">{m.recallBudgetHigh}</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex flex-wrap items-center justify-between gap-3 border-t pt-4">
<div className="min-w-0">
<label className="text-sm font-medium">{m.memoryMode}</label>
<p className="text-muted-foreground text-sm">{m.memoryModeDesc}</p>
</div>
<Select
value={config.hindsight.memory_mode}
disabled={!isWritable("hindsight.memory_mode") || patchConfig.isPending}
onValueChange={(v) => void patch({ hindsight: { memory_mode: v } })}
>
<SelectTrigger className="w-44">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="context">{m.memoryModeContext}</SelectItem>
<SelectItem value="tools">{m.memoryModeTools}</SelectItem>
<SelectItem value="hybrid">{m.memoryModeHybrid}</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex flex-wrap items-center justify-between gap-3 border-t pt-4">
<div className="min-w-0">
<label className="text-sm font-medium">{m.recallBudget}</label>
<p className="text-muted-foreground text-sm">{m.recallBudgetDesc}</p>
</div>
<Select
value={config.hindsight.recall_budget}
disabled={
!isWritable("hindsight.recall_budget") || patchConfig.isPending
}
onValueChange={(v) => void patch({ hindsight: { recall_budget: v } })}
>
<SelectTrigger className="w-44">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="low">{m.recallBudgetLow}</SelectItem>
<SelectItem value="mid">{m.recallBudgetMid}</SelectItem>
<SelectItem value="high">{m.recallBudgetHigh}</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2 border-t pt-4">
<label htmlFor={bankMissionId} className="text-sm font-medium">
{m.bankMission}
</label>
<p className="text-muted-foreground text-sm">{m.bankMissionDesc}</p>
<Textarea
id={bankMissionId}
rows={2}
defaultValue={config.hindsight.bank_mission}
placeholder={m.bankMissionPlaceholder}
disabled={!isWritable("hindsight.bank_mission") || patchConfig.isPending}
onBlur={(event) => {
const value = event.target.value;
if (value !== config.hindsight.bank_mission) {
void patch({ hindsight: { bank_mission: value } });
}
}}
/>
</div>
</>
)}
</div>
)}
</SettingsSection>
@@ -2,6 +2,7 @@
import {
BellIcon,
BrainIcon,
CalendarClockIcon,
ChevronsUpDown,
GaugeIcon,
@@ -160,6 +161,14 @@ export function WorkspaceNavMenu() {
用户管理
</DropdownMenuItem>
)}
{isAdmin && (
<DropdownMenuItem
onClick={() => navigate("/page/workspace/admin/memory")}
>
<BrainIcon />
记忆管理
</DropdownMenuItem>
)}
{isAdmin && (
<DropdownMenuItem
onClick={() =>
+48
View File
@@ -25,6 +25,7 @@ export interface AdminMessage {
/** One memory bucket: the global file or a per-agent file. */
export interface AdminMemoryBucket {
name: string;
display_name?: string;
data: Record<string, unknown>;
}
@@ -70,3 +71,50 @@ export async function getUserMemory(
"Failed to load memory",
);
}
export async function addAdminMemoryEntry(
userId: string,
content: string,
agent: string | null,
): Promise<AdminMemoryBucket> {
return parse(
await apiFetch(`${base()}/${encodeURIComponent(userId)}/memory/entries`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ content, agent }),
}),
"Failed to add memory entry",
);
}
export async function replaceAdminMemoryEntry(
userId: string,
oldText: string,
content: string,
agent: string | null,
): Promise<AdminMemoryBucket> {
return parse(
await apiFetch(`${base()}/${encodeURIComponent(userId)}/memory/entries`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ old_text: oldText, content, agent }),
}),
"Failed to replace memory entry",
);
}
export async function deleteAdminMemoryEntry(
userId: string,
oldText: string,
agent: string | null,
): Promise<AdminMemoryBucket> {
const params = new URLSearchParams({ old_text: oldText });
if (agent) params.set("agent", agent);
return parse(
await apiFetch(
`${base()}/${encodeURIComponent(userId)}/memory/entries?${params}`,
{ method: "DELETE" },
),
"Failed to delete memory entry",
);
}
+41 -2
View File
@@ -1,6 +1,13 @@
import { useQuery } from "@tanstack/react-query";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { getUserMemory, listUserThreadMessages, listUserThreads } from "./api";
import {
addAdminMemoryEntry,
deleteAdminMemoryEntry,
getUserMemory,
listUserThreadMessages,
listUserThreads,
replaceAdminMemoryEntry,
} from "./api";
/** Threads owned by a given user. Disabled until a user is picked. */
export function useUserThreads(userId: string | null) {
@@ -31,3 +38,35 @@ export function useUserMemory(userId: string | null) {
enabled: !!userId,
});
}
function useMemoryInvalidate(userId: string) {
const qc = useQueryClient();
return () => qc.invalidateQueries({ queryKey: ["admin-activity", "memory", userId] });
}
export function useAdminAddMemoryEntry(userId: string) {
const invalidate = useMemoryInvalidate(userId);
return useMutation({
mutationFn: ({ content, agent }: { content: string; agent: string | null }) =>
addAdminMemoryEntry(userId, content, agent),
onSuccess: invalidate,
});
}
export function useAdminReplaceMemoryEntry(userId: string) {
const invalidate = useMemoryInvalidate(userId);
return useMutation({
mutationFn: ({ oldText, content, agent }: { oldText: string; content: string; agent: string | null }) =>
replaceAdminMemoryEntry(userId, oldText, content, agent),
onSuccess: invalidate,
});
}
export function useAdminDeleteMemoryEntry(userId: string) {
const invalidate = useMemoryInvalidate(userId);
return useMutation({
mutationFn: ({ oldText, agent }: { oldText: string; agent: string | null }) =>
deleteAdminMemoryEntry(userId, oldText, agent),
onSuccess: invalidate,
});
}
@@ -0,0 +1,5 @@
import AdminMemoryComponent from "@/components/workspace/admin/admin-memory-page";
export default function AdminMemoryPage() {
return <AdminMemoryComponent />;
}
+30 -2
View File
@@ -5,6 +5,7 @@ import {
CheckCircleIcon,
InfoIcon,
SaveIcon,
SearchIcon,
} from "lucide-react";
import { useRouter } from "next/navigation";
import { useCallback, useEffect, useMemo, useState } from "react";
@@ -76,6 +77,7 @@ export default function NewAgentPage() {
const [selectedSkills, setSelectedSkills] = useState<string[]>([]);
const [isSkillSelectionTouched, setIsSkillSelectionTouched] = useState(false);
const [detailSkillName, setDetailSkillName] = useState<string | null>(null);
const [skillSearch, setSkillSearch] = useState("");
const [threadPrepared, setThreadPrepared] = useState(false);
const { skills, isLoading: isSkillsLoading } = useSkills();
const { models, isLoading: isModelsLoading } = useModels();
@@ -85,6 +87,16 @@ export default function NewAgentPage() {
[skills],
);
const filteredSkills = useMemo(() => {
const q = skillSearch.trim().toLowerCase();
if (!q) return enabledSkills;
return enabledSkills.filter(
(skill) =>
skill.name.toLowerCase().includes(q) ||
(skill.description ?? "").toLowerCase().includes(q),
);
}, [enabledSkills, skillSearch]);
useEffect(() => {
if (isSkillSelectionTouched) {
return;
@@ -230,7 +242,8 @@ export default function NewAgentPage() {
return;
}
toast.success(`智能体「${saved.name}」已保存`);
}, [ensureAgent]);
router.push("/page/workspace/agents");
}, [ensureAgent, router]);
const handleChatSubmit = useCallback(
async (text: string) => {
@@ -439,6 +452,17 @@ export default function NewAgentPage() {
已选 {selectedSkills.length} 个
</span>
</div>
{enabledSkills.length > 0 && (
<div className="relative">
<SearchIcon className="text-muted-foreground absolute left-2 top-1/2 size-3.5 -translate-y-1/2" />
<Input
placeholder="搜索技能名称或描述…"
value={skillSearch}
onChange={(e) => setSkillSearch(e.currentTarget.value)}
className="h-8 pl-7 text-xs"
/>
</div>
)}
<div className="max-h-[240px] overflow-y-auto rounded-md border p-2">
{isSkillsLoading ? (
<p className="text-muted-foreground text-sm">技能加载中...</p>
@@ -446,9 +470,13 @@ export default function NewAgentPage() {
<p className="text-muted-foreground text-sm">
当前没有可用技能。
</p>
) : filteredSkills.length === 0 ? (
<p className="text-muted-foreground text-sm">
没有匹配的技能。
</p>
) : (
<div className="flex flex-wrap gap-2">
{enabledSkills.map((skill) => {
{filteredSkills.map((skill) => {
const checked = selectedSkills.includes(skill.name);
return (
<div
@@ -28,6 +28,7 @@ import SkillEvolutionPage from "./SkillEvolutionPage";
import SkillsPage from "./SkillsPage";
import UsersPage from "./UsersPage";
import AdminUserActivityPage from "./AdminUserActivityPage";
import AdminMemoryPage from "./AdminMemoryPage";
import TagsPage from "./TagsPage";
import RecommendedQuestionsPage from "./RecommendedQuestionsPage";
@@ -223,6 +224,18 @@ export default function WorkspaceRoutes({
)
}
/>
<Route
path="admin/memory"
element={
getStoredAuth()?.system_role === "admin" ? (
<WorkspaceLayout {...layoutProps}>
<AdminMemoryPage />
</WorkspaceLayout>
) : (
<Navigate to="/page/workspace/chats/new" replace />
)
}
/>
<Route
path="admin/tags"
element={