Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1
This commit is contained in:
commit
d03f5fbbc0
79 files changed
+9502
-1647
No files matched your search
@@ -0,0 +1,183 @@
|
||||
"use client";
|
||||
|
||||
import { BotIcon, RefreshCwIcon, SearchIcon } from "lucide-react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
WorkspaceBody,
|
||||
WorkspaceContainer,
|
||||
WorkspaceHeader,
|
||||
} from "@/components/workspace/workspace-container";
|
||||
import { AgentCard } from "@/components/workspace/agents/agent-card";
|
||||
import { useAdminSyncAgentSkills, useAllAgentsAdmin } from "@/core/agents";
|
||||
import type { AgentSkillsSyncResult } from "@/core/agents/api";
|
||||
import { desensitize } from "@/lib/desensitize";
|
||||
|
||||
export default function AdminAgentsPage() {
|
||||
const [search, setSearch] = useState("");
|
||||
const { agents, isLoading } = useAllAgentsAdmin(true);
|
||||
const syncSkills = useAdminSyncAgentSkills();
|
||||
const [syncResult, setSyncResult] = useState<AgentSkillsSyncResult | null>(
|
||||
null,
|
||||
);
|
||||
const [syncDialogOpen, setSyncDialogOpen] = useState(false);
|
||||
|
||||
async function handleSyncSkills() {
|
||||
try {
|
||||
const result = await syncSkills.mutateAsync();
|
||||
setSyncResult(result);
|
||||
setSyncDialogOpen(true);
|
||||
if (result.modified === 0) {
|
||||
toast.success("所有智能体的技能均有效,无需清理");
|
||||
} else {
|
||||
toast.success(
|
||||
`已清理 ${result.modified} 个智能体中的失效技能引用`,
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
toast.error(err instanceof Error ? err.message : String(err));
|
||||
}
|
||||
}
|
||||
|
||||
const filteredAgents = useMemo(() => {
|
||||
const needle = search.trim().toLowerCase();
|
||||
if (!needle) return agents;
|
||||
return agents.filter((a) =>
|
||||
desensitize(a.name).toLowerCase().includes(needle),
|
||||
);
|
||||
}, [agents, search]);
|
||||
|
||||
return (
|
||||
<WorkspaceContainer>
|
||||
<WorkspaceHeader />
|
||||
<WorkspaceBody>
|
||||
<div className="flex size-full min-h-0 flex-col">
|
||||
<div className="flex items-start justify-between gap-4 border-b px-6 py-4">
|
||||
<div>
|
||||
<h1 className="text-xl font-semibold">智能体管理</h1>
|
||||
<p className="text-muted-foreground mt-0.5 text-sm">
|
||||
管理员视图:查看、编辑、删除所有用户的智能体(包括内置、用户自定义与私密广场条目)。
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleSyncSkills}
|
||||
disabled={syncSkills.isPending}
|
||||
title="扫描所有智能体的 config.yaml,移除指向已不存在技能的引用"
|
||||
>
|
||||
<RefreshCwIcon
|
||||
className={`mr-1.5 size-4 ${syncSkills.isPending ? "animate-spin" : ""}`}
|
||||
/>
|
||||
{syncSkills.isPending ? "正在同步…" : "同步技能"}
|
||||
</Button>
|
||||
<div className="relative w-64">
|
||||
<SearchIcon className="text-muted-foreground absolute left-2.5 top-1/2 size-4 -translate-y-1/2" />
|
||||
<Input
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
placeholder="按名称搜索"
|
||||
className="pl-8"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto p-6">
|
||||
{isLoading ? (
|
||||
<div className="text-muted-foreground flex h-40 items-center justify-center text-sm">
|
||||
加载中…
|
||||
</div>
|
||||
) : filteredAgents.length === 0 ? (
|
||||
<div className="flex h-64 flex-col items-center justify-center gap-3 text-center">
|
||||
<div className="bg-muted flex h-14 w-14 items-center justify-center rounded-full">
|
||||
<BotIcon className="text-muted-foreground h-7 w-7" />
|
||||
</div>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
当前没有可管理的智能体。
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<p className="text-muted-foreground mb-3 text-xs">
|
||||
共 {filteredAgents.length} 个智能体。
|
||||
</p>
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
||||
{filteredAgents.map((agent) => (
|
||||
<AgentCard key={agent.id} agent={agent} />
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</WorkspaceBody>
|
||||
|
||||
<Dialog open={syncDialogOpen} onOpenChange={setSyncDialogOpen}>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>同步技能 — 清理结果</DialogTitle>
|
||||
<DialogDescription>
|
||||
{syncResult
|
||||
? `扫描 ${syncResult.checked} 个智能体配置,清理 ${syncResult.modified} 个。`
|
||||
: ""}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
{syncResult && syncResult.removals.length > 0 ? (
|
||||
<div className="max-h-[60vh] overflow-y-auto">
|
||||
<ul className="space-y-3 text-sm">
|
||||
{syncResult.removals.map((entry) => (
|
||||
<li
|
||||
key={entry.agent_id}
|
||||
className="rounded-md border px-3 py-2"
|
||||
>
|
||||
<div className="font-medium">
|
||||
{entry.agent_name
|
||||
? desensitize(entry.agent_name)
|
||||
: entry.agent_id}
|
||||
</div>
|
||||
<div className="text-muted-foreground mt-0.5 text-xs">
|
||||
id: {entry.agent_id}
|
||||
{entry.user_id ? ` · owner: ${entry.user_id}` : " · built-in"}
|
||||
</div>
|
||||
<div className="mt-1.5 flex flex-wrap gap-1.5">
|
||||
{entry.removed_skills.map((s) => (
|
||||
<span
|
||||
key={s}
|
||||
className="bg-muted rounded px-1.5 py-0.5 text-xs"
|
||||
>
|
||||
{s}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-muted-foreground text-sm">
|
||||
所有智能体引用的技能均存在,未做任何修改。
|
||||
</p>
|
||||
)}
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setSyncDialogOpen(false)}>
|
||||
关闭
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</WorkspaceContainer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
SearchIcon,
|
||||
SparklesIcon,
|
||||
} from "lucide-react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
WorkspaceBody,
|
||||
WorkspaceContainer,
|
||||
WorkspaceHeader,
|
||||
} from "@/components/workspace/workspace-container";
|
||||
import { SkillRow } from "@/components/workspace/settings/skill-settings-page";
|
||||
import { getStoredAuth } from "@/core/auth";
|
||||
import { useAllSkillsAdmin } from "@/core/skills/hooks";
|
||||
import { desensitize } from "@/lib/desensitize";
|
||||
|
||||
const PAGE_SIZE = 10;
|
||||
|
||||
export default function AdminSkillsPage() {
|
||||
const [search, setSearch] = useState("");
|
||||
const [page, setPage] = useState(1);
|
||||
const { skills, isLoading, error } = useAllSkillsAdmin(true);
|
||||
|
||||
const storedAuth = useMemo(() => getStoredAuth(), []);
|
||||
const currentUserId = storedAuth?.user_id ?? null;
|
||||
|
||||
// Hide archived/stale entries here — they remain accessible from the regular
|
||||
// skills settings dialog. The admin page is for live skills only.
|
||||
const filteredSkills = useMemo(() => {
|
||||
const needle = search.trim().toLowerCase();
|
||||
return skills
|
||||
.filter((s) => (s.state ?? "active") === "active")
|
||||
.filter((s) => !needle || desensitize(s.name).toLowerCase().includes(needle));
|
||||
}, [skills, search]);
|
||||
|
||||
const totalPages = Math.max(1, Math.ceil(filteredSkills.length / PAGE_SIZE));
|
||||
useEffect(() => {
|
||||
if (page > totalPages) setPage(totalPages);
|
||||
}, [page, totalPages]);
|
||||
const pagedSkills = useMemo(
|
||||
() => filteredSkills.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE),
|
||||
[filteredSkills, page],
|
||||
);
|
||||
|
||||
return (
|
||||
<WorkspaceContainer>
|
||||
<WorkspaceHeader />
|
||||
<WorkspaceBody>
|
||||
<div className="flex size-full min-h-0 flex-col">
|
||||
<div className="flex items-start justify-between gap-4 border-b px-6 py-4">
|
||||
<div>
|
||||
<h1 className="text-xl font-semibold">技能管理</h1>
|
||||
<p className="text-muted-foreground mt-0.5 text-sm">
|
||||
管理员视图:查看、编辑、删除所有用户的技能(包括内置技能与未发布的自定义技能)。
|
||||
</p>
|
||||
</div>
|
||||
<div className="relative w-64 shrink-0">
|
||||
<SearchIcon className="text-muted-foreground absolute left-2.5 top-1/2 size-4 -translate-y-1/2" />
|
||||
<Input
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
placeholder="按名称搜索"
|
||||
className="pl-8"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-1 flex-col gap-4 overflow-y-auto p-6">
|
||||
{isLoading && (
|
||||
<div className="text-muted-foreground flex h-40 items-center justify-center text-sm">
|
||||
加载中…
|
||||
</div>
|
||||
)}
|
||||
{error && (
|
||||
<div className="text-destructive text-sm">
|
||||
加载失败:{error.message}
|
||||
</div>
|
||||
)}
|
||||
{!isLoading && !error && filteredSkills.length === 0 && (
|
||||
<div className="flex h-64 flex-col items-center justify-center gap-3 text-center">
|
||||
<div className="bg-muted flex h-14 w-14 items-center justify-center rounded-full">
|
||||
<SparklesIcon className="text-muted-foreground h-7 w-7" />
|
||||
</div>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
当前没有可管理的技能。
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{!isLoading && !error && filteredSkills.length > 0 && (
|
||||
<>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
共 {filteredSkills.length} 个技能。
|
||||
</p>
|
||||
{pagedSkills.map((skill) => (
|
||||
<SkillRow
|
||||
key={skill.name}
|
||||
skill={skill}
|
||||
currentUserId={currentUserId}
|
||||
isAdmin
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
{!isLoading && !error && totalPages > 1 && (
|
||||
<div className="flex items-center justify-end gap-2 pt-1">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
disabled={page <= 1}
|
||||
onClick={() => setPage((p) => Math.max(1, p - 1))}
|
||||
>
|
||||
<ChevronLeftIcon className="size-4" />
|
||||
上一页
|
||||
</Button>
|
||||
<span className="text-muted-foreground text-xs">
|
||||
第 {page} / {totalPages} 页
|
||||
</span>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
disabled={page >= totalPages}
|
||||
onClick={() => setPage((p) => Math.min(totalPages, p + 1))}
|
||||
>
|
||||
下一页
|
||||
<ChevronRightIcon className="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</WorkspaceBody>
|
||||
</WorkspaceContainer>
|
||||
);
|
||||
}
|
||||
@@ -231,14 +231,25 @@ export default function ChatPage() {
|
||||
</header>
|
||||
<main className="flex min-h-0 max-w-full grow flex-col">
|
||||
<div className="flex size-full justify-center">
|
||||
<MessageList
|
||||
className={cn("size-full", !isLandingState && "pt-10")}
|
||||
threadId={threadId}
|
||||
thread={thread}
|
||||
paddingBottom={messageListPaddingBottom}
|
||||
tokenUsageEnabled={tokenUsageEnabled}
|
||||
onClarificationSubmit={handleClarificationSubmit}
|
||||
/>
|
||||
{/*
|
||||
Landing state covers two cases: (a) a brand-new chat that
|
||||
hasn't sent anything yet, and (b) the user clicking 「新对话」
|
||||
mid-stream. In (b) the underlying useStream's internal
|
||||
stream.values takes one or two effect ticks to clear, so
|
||||
rendering MessageList unconditionally would leak the previous
|
||||
conversation under the welcome card. Skip rendering during
|
||||
the landing state to guarantee a clean slate.
|
||||
*/}
|
||||
{!isLandingState && (
|
||||
<MessageList
|
||||
className={cn("size-full", "pt-10")}
|
||||
threadId={threadId}
|
||||
thread={thread}
|
||||
paddingBottom={messageListPaddingBottom}
|
||||
tokenUsageEnabled={tokenUsageEnabled}
|
||||
onClarificationSubmit={handleClarificationSubmit}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="absolute right-0 bottom-0 left-0 z-30 flex justify-center px-4">
|
||||
<div
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
"use client";
|
||||
|
||||
import { Loader2Icon, ShieldIcon } from "lucide-react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import {
|
||||
usePrivateSquareStatus,
|
||||
useUpdatePrivateSquareSettings,
|
||||
} from "@/core/system-settings/hooks";
|
||||
|
||||
export default function PrivateSquareSettingsPage() {
|
||||
const { data, isLoading, error } = usePrivateSquareStatus();
|
||||
const update = useUpdatePrivateSquareSettings();
|
||||
|
||||
const [enabled, setEnabled] = useState(false);
|
||||
const [password, setPassword] = useState("");
|
||||
const [confirm, setConfirm] = useState("");
|
||||
// The status response never contains the password itself, so re-syncing
|
||||
// ``enabled`` from a refetch wouldn't clobber any in-progress edit — but we
|
||||
// still gate the seed on a one-shot ref to match the prompt-prefix page's
|
||||
// semantics: the admin's draft must not flicker back to the server value if
|
||||
// react-query refetches while they're typing.
|
||||
const initializedRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!data || initializedRef.current) return;
|
||||
setEnabled(data.enabled);
|
||||
initializedRef.current = true;
|
||||
}, [data]);
|
||||
|
||||
const handleSave = async () => {
|
||||
if (password && password !== confirm) {
|
||||
toast.error("两次输入的密码不一致");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await update.mutateAsync({
|
||||
enabled,
|
||||
// Only send the password field when the admin typed something —
|
||||
// omitting it leaves the server-side hash untouched. The empty-string
|
||||
// sentinel that clears the password is sent via the explicit "清除
|
||||
// 密码" button below, not by leaving the field blank.
|
||||
...(password ? { password } : {}),
|
||||
});
|
||||
setPassword("");
|
||||
setConfirm("");
|
||||
toast.success("已保存");
|
||||
} catch (e) {
|
||||
toast.error(e instanceof Error ? e.message : "保存失败");
|
||||
}
|
||||
};
|
||||
|
||||
const handleClearPassword = async () => {
|
||||
if (!window.confirm("确认清除私密广场密码?清除后任何人都将无法访问私密广场。")) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await update.mutateAsync({ password: "" });
|
||||
toast.success("已清除密码");
|
||||
} catch (e) {
|
||||
toast.error(e instanceof Error ? e.message : "清除失败");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mx-auto flex w-full max-w-2xl flex-col gap-6 px-6 py-10">
|
||||
<div>
|
||||
<h1 className="text-xl font-semibold">私密广场配置</h1>
|
||||
<p className="text-muted-foreground mt-1 text-sm">
|
||||
管理员可在此开启「私密广场」,并配置访问密码。开启后,用户可将自己发布的智能体放入私密广场;
|
||||
其他用户只有输入下方设置的密码后,才能在「智能体 → 私密广场」页签下查看到这些智能体。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center py-16">
|
||||
<Loader2Icon className="text-muted-foreground size-6 animate-spin" />
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="text-destructive rounded-xl border border-dashed py-16 text-center text-sm">
|
||||
加载失败:{error.message}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-5 rounded-xl border bg-white px-5 py-5">
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<div className="text-sm font-medium">启用私密广场</div>
|
||||
<div className="text-muted-foreground mt-0.5 text-xs">
|
||||
关闭后,「私密广场」页签不会显示;已放入私密广场的智能体也不会被列出。
|
||||
</div>
|
||||
</div>
|
||||
<Switch checked={enabled} onCheckedChange={setEnabled} />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<p className="text-sm font-medium">
|
||||
访问密码
|
||||
{data?.password_set ? (
|
||||
<span className="text-muted-foreground ml-2 text-xs font-normal">
|
||||
(已设置,留空则保持不变)
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-destructive ml-2 text-xs font-normal">
|
||||
(尚未设置)
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
<Input
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder={
|
||||
data?.password_set ? "留空表示不修改密码" : "请输入访问密码"
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<p className="text-sm font-medium">确认密码</p>
|
||||
<Input
|
||||
type="password"
|
||||
value={confirm}
|
||||
onChange={(e) => setConfirm(e.target.value)}
|
||||
placeholder="再次输入新密码"
|
||||
disabled={!password}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<Button
|
||||
variant="ghost"
|
||||
type="button"
|
||||
className="text-destructive hover:text-destructive"
|
||||
onClick={handleClearPassword}
|
||||
disabled={!data?.password_set || update.isPending}
|
||||
>
|
||||
<ShieldIcon className="mr-1.5 size-4" />
|
||||
清除密码
|
||||
</Button>
|
||||
<Button onClick={handleSave} disabled={update.isPending}>
|
||||
{update.isPending && (
|
||||
<Loader2Icon className="mr-1.5 size-4 animate-spin" />
|
||||
)}
|
||||
保存
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import ScheduledTasksPage from "./ScheduledTasksPage";
|
||||
import ScheduledTaskRunDetailPage from "./ScheduledTaskRunDetailPage";
|
||||
import RecommendedQuestionsPage from "./RecommendedQuestionsPage";
|
||||
import PromptPrefixSettingsPage from "./PromptPrefixSettingsPage";
|
||||
import PrivateSquareSettingsPage from "./PrivateSquareSettingsPage";
|
||||
import LlmMetricsPage from "./LlmMetricsPage";
|
||||
import UsersPage from "./UsersPage";
|
||||
import AdminScheduledTasksPage from "./AdminScheduledTasksPage";
|
||||
@@ -129,6 +130,16 @@ export default function StrategyRoutes() {
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="settings/private-square"
|
||||
element={
|
||||
getStoredAuth()?.system_role === "admin" ? (
|
||||
<PrivateSquareSettingsPage />
|
||||
) : (
|
||||
<Navigate to="/page/workspace/chats/new" replace />
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="settings/llm-metrics"
|
||||
element={
|
||||
|
||||
@@ -18,12 +18,15 @@ import ChatPage from "./ChatPage";
|
||||
import AgentsPage from "./AgentsPage";
|
||||
import AgentChatPage from "./AgentChatPage";
|
||||
import NewAgentPage from "./NewAgentPage";
|
||||
import AdminAgentsPage from "./AdminAgentsPage";
|
||||
import AdminScheduledTasksPage from "./AdminScheduledTasksPage";
|
||||
import AdminSkillsPage from "./AdminSkillsPage";
|
||||
import ScheduledTaskRunDetailPage from "./ScheduledTaskRunDetailPage";
|
||||
import ScheduledTasksPage from "./ScheduledTasksPage";
|
||||
import LlmMetricsPage from "./LlmMetricsPage";
|
||||
import MemoryPage from "./MemoryPage";
|
||||
import PromptPrefixSettingsPage from "./PromptPrefixSettingsPage";
|
||||
import PrivateSquareSettingsPage from "./PrivateSquareSettingsPage";
|
||||
import SkillEvolutionPage from "./SkillEvolutionPage";
|
||||
import SkillsPage from "./SkillsPage";
|
||||
import UsersPage from "./UsersPage";
|
||||
@@ -281,6 +284,30 @@ export default function WorkspaceRoutes({
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="admin/agents"
|
||||
element={
|
||||
getStoredAuth()?.system_role === "admin" ? (
|
||||
<WorkspaceLayout {...layoutProps}>
|
||||
<AdminAgentsPage />
|
||||
</WorkspaceLayout>
|
||||
) : (
|
||||
<Navigate to="/page/workspace/chats/new" replace />
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="admin/skills"
|
||||
element={
|
||||
getStoredAuth()?.system_role === "admin" ? (
|
||||
<WorkspaceLayout {...layoutProps}>
|
||||
<AdminSkillsPage />
|
||||
</WorkspaceLayout>
|
||||
) : (
|
||||
<Navigate to="/page/workspace/chats/new" replace />
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="admin/tags"
|
||||
element={
|
||||
@@ -317,6 +344,18 @@ export default function WorkspaceRoutes({
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="settings/private-square"
|
||||
element={
|
||||
getStoredAuth()?.system_role === "admin" ? (
|
||||
<WorkspaceLayout {...layoutProps}>
|
||||
<PrivateSquareSettingsPage />
|
||||
</WorkspaceLayout>
|
||||
) : (
|
||||
<Navigate to="/page/workspace/chats/new" replace />
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="settings/llm-metrics"
|
||||
element={
|
||||
|
||||
Reference in new issue
Block a user