Co-authored-by: Cursor <cursoragent@cursor.com>

# Conflicts:
#	frontend-web/src/components/workspace/workspace-nav-chat-list.tsx
#	frontend-web/src/pages/WorkspaceRoutes.tsx
#	offline-backend-20260512/backend/app/gateway/app.py
#	offline-backend-20260512/backend/app/gateway/deps.py
#	offline-backend-20260512/backend/pyproject.toml
This commit is contained in:
董宏凯 committed 2026-09-07 14:46:36 +08:00
commit cf9fe58905
109 files changed
+18863 -982

No files matched your search

+20 -1
View File
@@ -8,6 +8,7 @@ import {
SparklesIcon,
} from "lucide-react";
import { useEffect, useMemo, useState } from "react";
import { useSearchParams } from "react-router-dom";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
@@ -21,14 +22,28 @@ 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";
import { useSkillKnowledgeBindings } from "@/strategy-components/api/skill-knowledge";
import { SkillAddressesPanel } from "./SkillAddressesPage";
const PAGE_SIZE = 10;
export default function AdminSkillsPage() {
const [searchParams, setSearchParams] = useSearchParams();
const [search, setSearch] = useState("");
const [page, setPage] = useState(1);
const { skills, isLoading, error } = useAllSkillsAdmin(true);
const knowledgeBindings = useSkillKnowledgeBindings();
const activeTab = useMemo(() => {
const tab = searchParams.get("tab");
return tab === "addresses" ? tab : "skills";
}, [searchParams]);
const changeTab = (value: string) => {
const next = new URLSearchParams(searchParams);
if (value === "skills") next.delete("tab");
else next.set("tab", value);
setSearchParams(next, { replace: true });
};
const storedAuth = useMemo(() => getStoredAuth(), []);
const currentUserId = storedAuth?.user_id ?? null;
@@ -56,7 +71,8 @@ export default function AdminSkillsPage() {
<WorkspaceHeader />
<WorkspaceBody>
<Tabs
defaultValue="skills"
value={activeTab}
onValueChange={changeTab}
className="flex size-full min-h-0 flex-col gap-0"
>
<div className="flex items-center justify-between gap-4 border-b px-6 pt-4">
@@ -126,6 +142,9 @@ export default function AdminSkillsPage() {
skill={skill}
currentUserId={currentUserId}
isAdmin
knowledgeBindings={(knowledgeBindings.data ?? []).filter(
(binding) => binding.skill_name === skill.name,
)}
/>
))}
</>
+71 -12
View File
@@ -128,6 +128,26 @@ function readPendingKnowledgeSelection(agentId: string | undefined): string[] |
}
}
function readPendingAssistantKnowledgeSelection(agentId: string | undefined): string[] | null {
if (typeof window === "undefined" || !agentId) return null;
try {
const raw = sessionStorage.getItem("pendingAgentMessage");
if (!raw) return null;
const parsed = JSON.parse(raw) as {
agentId?: string;
assistantKnowledgeBaseIds?: unknown;
};
if (parsed.agentId !== agentId || !Array.isArray(parsed.assistantKnowledgeBaseIds)) {
return null;
}
return parsed.assistantKnowledgeBaseIds
.map((item) => String(item).trim())
.filter(Boolean);
} catch {
return null;
}
}
function stringifyForHiddenContext(value: unknown): string {
if (typeof value === "string") return value;
try {
@@ -344,11 +364,17 @@ export default function AgentChatPage({
const pendingKnowledgeSelection = useRef(
readPendingKnowledgeSelection(agent_id),
);
const pendingAssistantKnowledgeSelection = useRef(
readPendingAssistantKnowledgeSelection(agent_id),
);
const [selectedKnowledgeBaseIds, setSelectedKnowledgeBaseIds] = useState<string[]>(
() => pendingKnowledgeSelection.current ?? [],
);
const [selectedAssistantKnowledgeBaseIds, setSelectedAssistantKnowledgeBaseIds] = useState<string[]>(
() => pendingAssistantKnowledgeSelection.current ?? [],
);
const [knowledgeSelectionReady, setKnowledgeSelectionReady] = useState(
() => pendingKnowledgeSelection.current !== null,
() => pendingKnowledgeSelection.current !== null || pendingAssistantKnowledgeSelection.current !== null,
);
const [knowledgeSelectionLoading, setKnowledgeSelectionLoading] = useState(false);
const knowledgeSelectionThread = useRef<string | null>(null);
@@ -359,6 +385,10 @@ export default function AgentChatPage({
setSelectedKnowledgeBaseIds(ids);
setKnowledgeSelectionReady(true);
}, []);
const changeAssistantKnowledgeSelection = useCallback((ids: string[]) => {
setSelectedAssistantKnowledgeBaseIds(ids);
setKnowledgeSelectionReady(true);
}, []);
useEffect(() => {
activeThreadIdRef.current = threadId;
@@ -368,17 +398,23 @@ export default function AgentChatPage({
if (knowledgeSelectionAgent.current === agent_id) return;
knowledgeSelectionAgent.current = agent_id;
const pending = readPendingKnowledgeSelection(agent_id);
const pendingAssistant = readPendingAssistantKnowledgeSelection(agent_id);
pendingKnowledgeSelection.current = pending;
pendingAssistantKnowledgeSelection.current = pendingAssistant;
knowledgeSelectionThread.current = null;
setSelectedKnowledgeBaseIds(pending ?? []);
setKnowledgeSelectionReady(pending !== null);
setSelectedAssistantKnowledgeBaseIds(pendingAssistant ?? []);
setKnowledgeSelectionReady(pending !== null || pendingAssistant !== null);
}, [agent_id]);
useEffect(() => {
if (isNewThread) {
if (knowledgeSelectionThread.current !== "__new__") {
setSelectedKnowledgeBaseIds(pendingKnowledgeSelection.current ?? []);
setKnowledgeSelectionReady(pendingKnowledgeSelection.current !== null);
setSelectedAssistantKnowledgeBaseIds(pendingAssistantKnowledgeSelection.current ?? []);
setKnowledgeSelectionReady(
pendingKnowledgeSelection.current !== null || pendingAssistantKnowledgeSelection.current !== null,
);
}
knowledgeSelectionThread.current = "__new__";
setKnowledgeSelectionLoading(false);
@@ -393,18 +429,29 @@ export default function AgentChatPage({
if (knowledgeSelectionThread.current !== threadId) return;
const metadata = record.metadata as Record<string, unknown> | undefined;
const raw = metadata?.llmwiki_knowledge_base_ids;
const assistantRaw = metadata?.assistant_knowledge_base_ids;
const llmwikiIds = Array.isArray(raw)
? raw.map((item) => String(item).trim()).filter(Boolean)
: [];
const assistantIds = Array.isArray(assistantRaw)
? assistantRaw.map((item) => String(item).trim()).filter(Boolean)
: [];
if (Array.isArray(raw)) {
setSelectedKnowledgeBaseIds(
raw.map((item) => String(item).trim()).filter(Boolean),
);
setKnowledgeSelectionReady(true);
setSelectedKnowledgeBaseIds(llmwikiIds);
} else {
setSelectedKnowledgeBaseIds([]);
setKnowledgeSelectionReady(false);
}
if (Array.isArray(assistantRaw)) {
setSelectedAssistantKnowledgeBaseIds(assistantIds);
} else {
setSelectedAssistantKnowledgeBaseIds([]);
}
setKnowledgeSelectionReady(Array.isArray(raw) || Array.isArray(assistantRaw));
})
.catch(() => {
if (knowledgeSelectionThread.current === threadId) {
setSelectedKnowledgeBaseIds([]);
setSelectedAssistantKnowledgeBaseIds([]);
setKnowledgeSelectionReady(false);
}
})
@@ -427,10 +474,13 @@ export default function AgentChatPage({
() => ({
...(taskId ? taskThreadMetadata(taskContext) : {}),
...(knowledgeSelectionReady
? { llmwiki_knowledge_base_ids: selectedKnowledgeBaseIds }
? {
llmwiki_knowledge_base_ids: selectedKnowledgeBaseIds,
assistant_knowledge_base_ids: selectedAssistantKnowledgeBaseIds,
}
: {}),
}),
[knowledgeSelectionReady, selectedKnowledgeBaseIds, taskContext, taskId],
[knowledgeSelectionReady, selectedAssistantKnowledgeBaseIds, selectedKnowledgeBaseIds, taskContext, taskId],
);
// 任务工作区(taskBind):每次运行都带隐藏任务上下文(任务id/名称/内容),
// 新建对话的消息历史里没有开场信息时模型也能拿到任务id(见 taskBindRunContext)。
@@ -446,7 +496,10 @@ export default function AgentChatPage({
...settings.context,
agent_id,
...(knowledgeSelectionReady
? { llmwiki_knowledge_base_ids: selectedKnowledgeBaseIds }
? {
llmwiki_knowledge_base_ids: selectedKnowledgeBaseIds,
assistant_knowledge_base_ids: selectedAssistantKnowledgeBaseIds,
}
: {}),
...(taskBindRunContextValue ?? {}),
},
@@ -1043,9 +1096,13 @@ export default function AgentChatPage({
source="agent"
agentId={agent_id}
value={selectedKnowledgeBaseIds}
assistantValue={selectedAssistantKnowledgeBaseIds}
onChange={changeKnowledgeSelection}
onAssistantChange={changeAssistantKnowledgeSelection}
autoSelectAll={
isNewThread && pendingKnowledgeSelection.current === null
isNewThread &&
pendingKnowledgeSelection.current === null &&
pendingAssistantKnowledgeSelection.current === null
}
disabled={isDemoMode || knowledgeSelectionLoading}
label="知识空间"
@@ -1130,7 +1187,9 @@ export default function AgentChatPage({
source="agent"
agentId={agent_id}
value={selectedKnowledgeBaseIds}
assistantValue={selectedAssistantKnowledgeBaseIds}
onChange={changeKnowledgeSelection}
onAssistantChange={changeAssistantKnowledgeSelection}
disabled={isDemoMode || knowledgeSelectionLoading}
readOnly
label="知识空间"
@@ -0,0 +1,559 @@
"use client";
import { useQuery } from "@tanstack/react-query";
import {
ArrowDownWideNarrow,
Bot,
ChevronLeft,
ChevronRight,
Download,
FileText,
FileX2,
Library,
Loader2,
MessagesSquare,
MessageSquareShare,
RefreshCw,
Search,
} from "lucide-react";
import Link from "next/link";
import { useEffect, useMemo, useRef, useState } from "react";
import { toast } from "sonner";
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 { Skeleton } from "@/components/ui/skeleton";
import {
WorkspaceBody,
WorkspaceContainer,
WorkspaceHeader,
} from "@/components/workspace/workspace-container";
import {
downloadArtifactFile,
downloadArtifactMarkdownAsWord,
} from "@/core/artifacts/download";
import {
fetchArtifactLibrary,
type ArtifactLibraryItem,
type ArtifactLibraryKind,
type ArtifactLibrarySource,
} from "@/core/artifacts/library";
import { useI18n } from "@/core/i18n/hooks";
import { bootstrapResumePath, pathOfThread } from "@/core/threads/utils";
import { formatDateTime } from "@/core/utils/datetime";
import {
getFileExtensionDisplayName,
getFileIcon,
} from "@/core/utils/files";
import { desensitize } from "@/lib/desensitize";
import { cn } from "@/lib/utils";
const PAGE_SIZE = 50;
const FILTERS: Array<{ value: ArtifactLibraryKind; label: string }> = [
{ value: "all", label: "全部" },
{ value: "document", label: "文档" },
{ value: "spreadsheet", label: "表格" },
{ value: "text_data", label: "文本与数据" },
{ value: "archive", label: "压缩包" },
{ value: "other", label: "其他" },
];
const SOURCE_FILTERS: Array<{
value: ArtifactLibrarySource;
label: string;
}> = [
{ value: "all", label: "全部来源" },
{ value: "general_qa", label: "通用问答" },
{ value: "agent", label: "智能体" },
];
type DownloadFormat = "original" | "word";
function isMarkdownArtifact(name: string): boolean {
return /\.(md|markdown)$/i.test(name);
}
function artifactDownloadKey(
item: ArtifactLibraryItem,
format: DownloadFormat,
): string {
return `${item.threadId}:${item.path}:${format}`;
}
function formatBytes(bytes: number): string {
if (!Number.isFinite(bytes) || bytes <= 0) return "0 B";
const units = ["B", "KB", "MB", "GB"];
const index = Math.min(
Math.floor(Math.log(bytes) / Math.log(1024)),
units.length - 1,
);
const value = bytes / 1024 ** index;
const digits = index === 0 || value >= 100 ? 0 : value >= 10 ? 1 : 2;
return `${value.toFixed(digits)} ${units[index]}`;
}
function sourceConversationPath(item: ArtifactLibraryItem): string {
const basePath =
item.threadType === "bootstrap"
? bootstrapResumePath(item.threadId)
: pathOfThread({
thread_id: item.threadId,
metadata: item.agentId ? { agent_id: item.agentId } : undefined,
});
const search = new URLSearchParams({ artifact: item.path });
return `${basePath}?${search.toString()}`;
}
function FileRow({
item,
downloading,
onDownload,
}: {
item: ArtifactLibraryItem;
downloading: DownloadFormat | null;
onDownload: (item: ArtifactLibraryItem, format: DownloadFormat) => void;
}) {
const sourcePath = sourceConversationPath(item);
const displayName = desensitize(item.name);
const sourceTitle = desensitize(item.threadTitle || "新对话");
const sourceLabel = item.sourceType === "agent" ? "智能体" : "通用问答";
const sourceName =
item.sourceType === "agent"
? desensitize(item.agentName || item.agentId || "未知智能体")
: sourceLabel;
const SourceIcon = item.sourceType === "agent" ? Bot : MessagesSquare;
const busy = downloading !== null;
return (
<tr className="group border-b transition-colors hover:bg-muted/35">
<td className="min-w-0 py-3 pr-4 pl-1">
<Link
href={sourcePath}
className="flex min-w-0 items-center gap-3 rounded-md outline-none focus-visible:ring-2 focus-visible:ring-ring"
title={`打开原对话并查看 ${displayName}`}
>
<span className="flex size-10 shrink-0 items-center justify-center rounded-lg bg-muted text-foreground/75">
{getFileIcon(item.name, "size-5")}
</span>
<span className="min-w-0">
<span className="block truncate text-sm font-medium text-foreground group-hover:text-primary">
{displayName}
</span>
<span className="mt-1 block truncate text-xs text-muted-foreground lg:hidden">
{getFileExtensionDisplayName(item.name)} · {sourceName} · 来自「
{sourceTitle}」
</span>
<span className="mt-1 hidden text-xs text-muted-foreground lg:block">
{getFileExtensionDisplayName(item.name)} 文件
</span>
</span>
</Link>
</td>
<td className="hidden min-w-0 px-3 py-3 lg:table-cell">
<div className="flex min-w-0 items-center gap-2.5">
<span className="flex size-8 shrink-0 items-center justify-center rounded-md bg-muted text-muted-foreground">
<SourceIcon className="size-4" />
</span>
<span className="min-w-0">
<span className="block truncate text-sm font-medium text-foreground">
{sourceName}
</span>
<Link
href={sourcePath}
className="block truncate text-xs text-muted-foreground underline-offset-4 hover:text-foreground hover:underline"
title={`打开原对话:${sourceTitle}`}
>
{item.sourceType === "agent" ? "智能体 · " : ""}
{sourceTitle}
</Link>
</span>
</div>
</td>
<td className="whitespace-nowrap px-3 py-3 text-sm text-muted-foreground">
{formatDateTime(item.modifiedAt)}
</td>
<td className="hidden whitespace-nowrap px-3 py-3 text-sm text-muted-foreground md:table-cell">
{formatBytes(item.sizeBytes)}
</td>
<td className="py-3 pr-1 pl-3 text-right">
<div className="flex items-center justify-end gap-1">
<Button
type="button"
size="icon"
variant="ghost"
className="size-8"
disabled={busy}
onClick={() => onDownload(item, "original")}
title={`下载原文件:${displayName}`}
aria-label={`下载原文件:${displayName}`}
>
{downloading === "original" ? (
<Loader2 className="size-4 animate-spin" />
) : (
<Download className="size-4" />
)}
</Button>
{isMarkdownArtifact(item.name) && (
<Button
type="button"
size="sm"
variant="ghost"
className="h-8 gap-1 px-2 text-xs"
disabled={busy}
onClick={() => onDownload(item, "word")}
title={`下载为 Word:${displayName}`}
aria-label={`下载为 Word:${displayName}`}
>
{downloading === "word" ? (
<Loader2 className="size-4 animate-spin" />
) : (
<FileText className="size-4" />
)}
Word
</Button>
)}
<Button
asChild
size="icon"
variant="ghost"
className="size-8"
title={`打开原对话:${sourceTitle}`}
>
<Link href={sourcePath} aria-label={`打开原对话:${sourceTitle}`}>
<MessageSquareShare className="size-4" />
</Link>
</Button>
</div>
</td>
</tr>
);
}
function LoadingRows() {
return Array.from({ length: 5 }, (_, index) => (
<tr className="border-b" key={index}>
<td className="py-3 pr-4 pl-1">
<div className="flex items-center gap-3">
<Skeleton className="size-10 shrink-0" />
<div className="min-w-0 flex-1 space-y-2">
<Skeleton className="h-4 w-2/3" />
<Skeleton className="h-3 w-1/3" />
</div>
</div>
</td>
<td className="hidden px-3 lg:table-cell"><Skeleton className="h-4 w-3/4" /></td>
<td className="px-3"><Skeleton className="h-4 w-24" /></td>
<td className="hidden px-3 md:table-cell"><Skeleton className="h-4 w-16" /></td>
<td className="pr-1 pl-3"><Skeleton className="ml-auto size-8" /></td>
</tr>
));
}
export default function ArtifactLibraryPage() {
const { t } = useI18n();
const [search, setSearch] = useState("");
const [debouncedSearch, setDebouncedSearch] = useState("");
const [fileKind, setFileKind] = useState<ArtifactLibraryKind>("all");
const [sourceType, setSourceType] = useState<ArtifactLibrarySource>("all");
const [page, setPage] = useState(1);
const [downloadingKey, setDownloadingKey] = useState<string | null>(null);
const forceRefreshRef = useRef(false);
useEffect(() => {
document.title = `资料库 - ${t.pages.appName}`;
}, [t.pages.appName]);
useEffect(() => {
const timer = window.setTimeout(() => setDebouncedSearch(search.trim()), 300);
return () => window.clearTimeout(timer);
}, [search]);
useEffect(() => {
setPage(1);
}, [debouncedSearch, fileKind, sourceType]);
const offset = (page - 1) * PAGE_SIZE;
const libraryQuery = useQuery({
queryKey: ["artifact-library", debouncedSearch, fileKind, sourceType, page],
queryFn: async ({ signal }) => {
const refresh = forceRefreshRef.current;
try {
return await fetchArtifactLibrary({
query: debouncedSearch,
fileKind,
sourceType,
offset,
limit: PAGE_SIZE,
refresh,
signal,
});
} finally {
forceRefreshRef.current = false;
}
},
retry: false,
});
const total = libraryQuery.data?.total ?? 0;
const items = libraryQuery.data?.items ?? [];
const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE));
const hasPrevious = page > 1;
const hasNext = page < totalPages;
useEffect(() => {
if (page > totalPages) setPage(totalPages);
}, [page, totalPages]);
const itemKeys = useMemo(
() =>
new Set(
items.flatMap((item) => [
artifactDownloadKey(item, "original"),
artifactDownloadKey(item, "word"),
]),
),
[items],
);
useEffect(() => {
if (downloadingKey && !itemKeys.has(downloadingKey)) setDownloadingKey(null);
}, [downloadingKey, itemKeys]);
const handleDownload = async (
item: ArtifactLibraryItem,
format: DownloadFormat,
) => {
const key = artifactDownloadKey(item, format);
if (downloadingKey) return;
setDownloadingKey(key);
try {
if (format === "word") {
await downloadArtifactMarkdownAsWord({
filepath: item.path,
threadId: item.threadId,
});
toast.success("Word 文件已生成");
} else {
await downloadArtifactFile({
filepath: item.path,
threadId: item.threadId,
});
}
} catch (error) {
console.error(error);
toast.error(error instanceof Error ? error.message : "下载失败");
} finally {
setDownloadingKey(null);
}
};
const handleRefresh = () => {
forceRefreshRef.current = true;
void libraryQuery.refetch();
};
return (
<WorkspaceContainer>
<WorkspaceHeader />
<WorkspaceBody className="items-stretch">
<div className="flex min-h-0 w-full flex-1 flex-col">
<header className="shrink-0 border-b bg-card/40 px-5 py-6 sm:px-8">
<div className="mx-auto flex w-full max-w-6xl flex-col gap-5">
<div className="flex flex-col justify-between gap-4 sm:flex-row sm:items-start">
<div>
<div className="flex items-center gap-2">
<Library className="size-6 text-primary" />
<h1 className="text-2xl font-semibold tracking-tight">资料库</h1>
</div>
<p className="mt-2 text-sm text-muted-foreground">
汇总当前账号在对话中生成的文件,不包含上传文件、图片、音频和视频。
</p>
</div>
<Button
type="button"
variant="outline"
size="sm"
onClick={handleRefresh}
disabled={libraryQuery.isFetching}
>
<RefreshCw className={cn("size-4", libraryQuery.isFetching && "animate-spin")} />
刷新
</Button>
</div>
<div className="relative max-w-xl">
<Search className="pointer-events-none absolute top-1/2 left-3 size-4 -translate-y-1/2 text-muted-foreground" />
<Input
type="search"
className="h-11 pl-9"
placeholder="搜索文件名、来源对话或智能体名称"
value={search}
onChange={(event) => setSearch(event.target.value)}
/>
</div>
<div className="flex flex-wrap items-center justify-between gap-4">
<div className="flex flex-wrap items-center gap-3">
<div className="flex flex-wrap gap-1" aria-label="文件类型筛选">
{FILTERS.map((filter) => (
<Button
key={filter.value}
type="button"
size="sm"
variant={fileKind === filter.value ? "secondary" : "ghost"}
aria-pressed={fileKind === filter.value}
onClick={() => setFileKind(filter.value)}
>
{filter.label}
</Button>
))}
</div>
<Select
value={sourceType}
onValueChange={(value) =>
setSourceType(value as ArtifactLibrarySource)
}
>
<SelectTrigger
size="sm"
className="w-32"
aria-label="筛选文件来源"
>
<SelectValue />
</SelectTrigger>
<SelectContent>
{SOURCE_FILTERS.map((filter) => (
<SelectItem key={filter.value} value={filter.value}>
{filter.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex items-center gap-2 text-xs text-muted-foreground">
<ArrowDownWideNarrow className="size-4" />
最近生成
</div>
</div>
</div>
</header>
<ScrollArea className="min-h-0 flex-1">
<main className="mx-auto w-full max-w-6xl px-5 py-5 sm:px-8">
<div className="mb-2 text-xs text-muted-foreground tabular-nums">
{libraryQuery.isLoading ? "正在统计文件…" : `共 ${total} 个文件`}
</div>
<div className="overflow-x-auto">
<table className="w-full table-fixed border-collapse">
<thead>
<tr className="border-b text-left text-xs font-normal text-muted-foreground">
<th className="w-[48%] py-3 pr-4 pl-1 font-normal lg:w-[35%]">文件名</th>
<th className="hidden w-[25%] px-3 py-3 font-normal lg:table-cell">来源</th>
<th className="w-[34%] px-3 py-3 font-normal md:w-[22%] lg:w-[16%]">生成时间</th>
<th className="hidden w-[13%] px-3 py-3 font-normal lg:w-[10%] md:table-cell">大小</th>
<th className="w-[18%] py-3 pr-1 pl-3 text-right font-normal md:w-[17%] lg:w-[14%]">
<span className="sr-only">操作</span>
</th>
</tr>
</thead>
<tbody>
{libraryQuery.isLoading ? (
<LoadingRows />
) : (
items.map((item) => {
const key = `${item.threadId}:${item.path}`;
const activeDownload = downloadingKey?.startsWith(`${key}:`)
? downloadingKey.endsWith(":word")
? "word"
: "original"
: null;
return (
<FileRow
key={key}
item={item}
downloading={activeDownload}
onDownload={(target, format) =>
void handleDownload(target, format)
}
/>
);
})
)}
</tbody>
</table>
</div>
{!libraryQuery.isLoading && libraryQuery.isError && (
<div className="flex flex-col items-center gap-3 py-16 text-center">
<FileX2 className="size-9 text-muted-foreground" />
<div>
<p className="text-sm font-medium">资料库加载失败</p>
<p className="mt-1 text-xs text-muted-foreground">
{libraryQuery.error instanceof Error
? libraryQuery.error.message
: "请稍后重试"}
</p>
</div>
<Button variant="outline" size="sm" onClick={handleRefresh}>
重新加载
</Button>
</div>
)}
{!libraryQuery.isLoading && !libraryQuery.isError && items.length === 0 && (
<div className="flex flex-col items-center gap-3 py-20 text-center">
<FileX2 className="size-9 text-muted-foreground" />
<div>
<p className="text-sm font-medium">
{debouncedSearch || fileKind !== "all" || sourceType !== "all"
? "没有找到匹配的文件"
: "还没有生成文件"}
</p>
<p className="mt-1 text-xs text-muted-foreground">
对话中生成的文档、表格和数据文件会自动出现在这里。
</p>
</div>
</div>
)}
{!libraryQuery.isLoading && !libraryQuery.isError && totalPages > 1 && (
<div className="flex items-center justify-center gap-3 py-7">
<Button
variant="outline"
size="sm"
disabled={!hasPrevious}
onClick={() => setPage((current) => Math.max(1, current - 1))}
>
<ChevronLeft className="size-4" />
上一页
</Button>
<span className="text-sm text-muted-foreground tabular-nums">
{page} / {totalPages}
</span>
<Button
variant="outline"
size="sm"
disabled={!hasNext}
onClick={() => setPage((current) => current + 1)}
>
下一页
<ChevronRight className="size-4" />
</Button>
</div>
)}
</main>
</ScrollArea>
</div>
</WorkspaceBody>
</WorkspaceContainer>
);
}
@@ -0,0 +1,631 @@
import {
ArrowLeft,
BookOpenText,
Database,
Edit3,
Loader2,
Plus,
RefreshCw,
Search,
Trash2,
Upload,
} from "lucide-react";
import { useEffect, useMemo, useRef, useState } from "react";
import { useSearchParams } from "react-router-dom";
import { useQueryClient } from "@tanstack/react-query";
import { toast } from "sonner";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Textarea } from "@/components/ui/textarea";
import { KnowledgeProductNav } from "@/components/workspace/knowledge-product-nav";
import { getStoredAuth } from "@/core/auth";
import { buildAppHashUrl } from "@/core/llmwiki/source-links";
import {
searchAssistantWiki,
useAssistantImportJobs,
useAssistantKnowledgeActions,
useAssistantKnowledgeBases,
useAssistantKnowledgeSources,
useAssistantWikiPages,
type AssistantKnowledgeBase,
} from "@/strategy-components/api/assistant-knowledge";
import {
WikiLibraryReader,
type LibraryWikiPage,
} from "@/strategy-components/components/resource-management/llmwiki/WikiLibraryReader";
import { WikiPackageTransfers } from "@/strategy-components/components/resource-management/llmwiki/WikiPackageTransfers";
export default function AssistantKnowledgePage() {
const isAdmin = getStoredAuth()?.system_role === "admin";
const [params, setParams] = useSearchParams();
const baseId = params.get("base_id");
const slug = params.get("wikiSlug");
const bases = useAssistantKnowledgeBases();
const actions = useAssistantKnowledgeActions();
const pages = useAssistantWikiPages("", baseId);
const sources = useAssistantKnowledgeSources(baseId);
const jobs = useAssistantImportJobs(baseId);
const client = useQueryClient();
const [tab, setTab] = useState("wiki");
const [createOpen, setCreateOpen] = useState(false);
const [editingBase, setEditingBase] =
useState<AssistantKnowledgeBase | null>(null);
const [name, setName] = useState("");
const [query, setQuery] = useState("");
const [searching, setSearching] = useState(false);
const [searchResult, setSearchResult] = useState<Awaited<
ReturnType<typeof searchAssistantWiki>
> | null>(null);
const upload = useRef<HTMLInputElement>(null);
const base = bases.data?.find((b) => b.id === baseId);
const libraryPages = useMemo<LibraryWikiPage[]>(
() =>
(pages.data || []).map((p) => ({
...p,
content: p.content_markdown,
status: "published",
aliases: p.aliases || [],
source_refs: p.source_refs || [],
in_links: p.in_links || [],
out_links: p.out_links || [],
version: 1,
})),
[pages.data],
);
const active =
libraryPages.find((p) => p.slug === slug) ||
libraryPages.find((p) => p.page_type === "index") ||
libraryPages[0] ||
null;
const navigatePage = (nextSlug: string) => {
setParams({ base_id: baseId!, wikiSlug: nextSlug });
setTab("wiki");
};
const finishedJobs = (jobs.data || [])
.filter((j) => j.status === "completed")
.map((j) => j.id)
.join(",");
useEffect(() => {
void client.invalidateQueries({
queryKey: ["assistant-knowledge", "pages"],
});
void client.invalidateQueries({
queryKey: ["assistant-knowledge", "bases"],
});
}, [finishedJobs, client]);
useEffect(() => {
setSearchResult(null);
setQuery("");
}, [baseId]);
const create = async () => {
if (!name.trim()) return;
try {
const b = await actions.createBase.mutateAsync({ name: name.trim() });
setCreateOpen(false);
setName("");
setParams({ base_id: b.id });
} catch (e) {
toast.error((e as Error).message);
}
};
const search = async () => {
if (!query.trim() || !baseId) return;
setSearching(true);
try {
setSearchResult(await searchAssistantWiki(query, baseId));
} catch (e) {
toast.error((e as Error).message);
} finally {
setSearching(false);
}
};
const refresh = () =>
void client.invalidateQueries({ queryKey: ["assistant-knowledge"] });
const removeBase = async (target: AssistantKnowledgeBase) => {
if (target.is_global) {
toast.error("知识梳理总库承担全库沉淀,不能删除");
return;
}
if (
!window.confirm(
`确定删除助手知识库“${target.name}”吗?库内 Wiki、向量、实体、关系和导入记录都会删除。`,
)
)
return;
try {
await actions.deleteBase.mutateAsync(target.id);
if (baseId === target.id) setParams({});
toast.success("助手知识库已删除");
} catch (error) {
toast.error((error as Error).message);
}
};
return (
<div className="flex size-full min-h-0 flex-col bg-background">
<KnowledgeProductNav />
{!baseId ? (
<main className="flex-1 overflow-auto p-6 lg:p-8">
<header className="mb-8 flex items-center justify-between">
<div>
<h1 className="text-2xl font-semibold">助手知识库</h1>
<p className="mt-2 text-sm text-muted-foreground">
浏览分类
Wiki、关联知识与来源。普通知识库向量化完成后自动沉淀到知识梳理。
</p>
</div>
{isAdmin && (
<Button onClick={() => setCreateOpen(true)}>
<Plus className="mr-2 size-4" />
新建知识库
</Button>
)}
</header>
{bases.isLoading && <Loader2 className="animate-spin" />}
{bases.error && (
<p className="text-destructive">{bases.error.message}</p>
)}
<div className="grid gap-5 md:grid-cols-2 xl:grid-cols-3">
{bases.data?.map((b) => (
<div
key={b.id}
className="group overflow-hidden rounded-2xl border bg-card text-left shadow-sm transition hover:border-primary/40 hover:shadow-md"
>
<button
type="button"
onClick={() => setParams({ base_id: b.id })}
className="w-full p-6 text-left"
>
<div className="mb-5 flex items-center justify-between">
<div className="rounded-xl bg-emerald-100 p-3 text-emerald-700 dark:bg-emerald-950">
<Database className="size-6" />
</div>
{b.is_global && (
<Badge variant="secondary">全库知识梳理</Badge>
)}
</div>
<h2 className="text-lg font-semibold group-hover:text-primary">
{b.name}
</h2>
<p className="mt-2 line-clamp-2 min-h-10 text-sm text-muted-foreground">
{b.description ||
(b.is_global
? "汇聚普通知识库的分析成果,保留分类、标签和知识关联。"
: "独立的 Wiki 知识空间,支持导入、导出与向量检索。")}
</p>
<div className="mt-6 flex gap-5 text-xs text-muted-foreground">
<span>{b.page_count || 0} Wiki</span>
<span>{b.source_count || 0} 来源</span>
<span>{b.relation_count || 0} 关系</span>
</div>
</button>
{isAdmin && (
<div className="flex items-center gap-2 border-t bg-muted/20 px-6 py-3">
<Button
size="sm"
variant="outline"
disabled={actions.updateBase.isPending}
onClick={() => setEditingBase(b)}
>
<Edit3 className="mr-2 size-4" />
编辑
</Button>
{!b.is_global && (
<Button
size="sm"
variant="ghost"
className="ml-auto text-destructive hover:bg-destructive/10 hover:text-destructive"
disabled={actions.deleteBase.isPending}
onClick={() => void removeBase(b)}
>
<Trash2 className="mr-2 size-4" />
删除
</Button>
)}
{b.is_global && (
<span className="ml-auto text-xs text-muted-foreground">
系统大库不可删除
</span>
)}
</div>
)}
</div>
))}
</div>
{!bases.isLoading && !bases.data?.length && isAdmin && (
<Button
className="mt-5"
onClick={() =>
actions.initialize.mutate("知识梳理总库", {
onError: (e) => toast.error(e.message),
})
}
>
初始化知识梳理总库
</Button>
)}
</main>
) : (
<>
<header className="flex shrink-0 flex-wrap items-center gap-3 border-b px-5 py-4">
<Button
variant="ghost"
size="icon"
aria-label="返回助手知识库"
onClick={() => setParams({})}
>
<ArrowLeft className="size-4" />
</Button>
<div className="rounded-xl bg-emerald-100 p-2.5 text-emerald-700 dark:bg-emerald-950">
<BookOpenText className="size-5" />
</div>
<div className="min-w-0">
<h1 className="text-lg font-semibold">
{base?.name || "助手知识库"}
</h1>
<p className="mt-1 text-xs text-muted-foreground">
{base?.is_global
? "全库知识梳理 · 自动同步分析成果"
: "Wiki 知识空间"}
</p>
</div>
<div className="ml-auto flex gap-2">
<Button variant="outline" size="sm" onClick={refresh}>
<RefreshCw className="mr-2 size-4" />
刷新
</Button>
{isAdmin && (
<Button
variant="outline"
size="sm"
disabled={!base || actions.updateBase.isPending}
onClick={() => base && setEditingBase(base)}
>
<Edit3 className="mr-2 size-4" />
编辑
</Button>
)}
{isAdmin && base && !base.is_global && (
<Button
variant="outline"
size="sm"
className="text-destructive hover:bg-destructive/10 hover:text-destructive"
disabled={actions.deleteBase.isPending}
onClick={() => void removeBase(base)}
>
<Trash2 className="mr-2 size-4" />
删除
</Button>
)}
{isAdmin && (
<Button
variant="outline"
size="sm"
disabled={actions.uploadWeKnoraPackage.isPending}
onClick={() => upload.current?.click()}
>
<Upload className="mr-2 size-4" />
导入数据包
</Button>
)}
</div>
<input
ref={upload}
type="file"
accept=".jsonl,.ndjson"
className="hidden"
onChange={async (e) => {
const file = e.target.files?.[0];
e.target.value = "";
if (!file) return;
try {
await actions.uploadWeKnoraPackage.mutateAsync({
file,
assistantBaseId: baseId,
});
toast.success("文件已上传,后台正在导入");
} catch (err) {
toast.error((err as Error).message);
}
}}
/>
</header>
<Tabs
value={tab}
onValueChange={setTab}
className="flex min-h-0 flex-1 flex-col gap-0"
>
<div className="shrink-0 border-b px-5 py-2">
<TabsList>
<TabsTrigger value="wiki">
Wiki({base?.page_count || 0})
</TabsTrigger>
<TabsTrigger value="search">知识检索</TabsTrigger>
<TabsTrigger value="sources">来源与同步</TabsTrigger>
{isAdmin && (
<TabsTrigger value="transfer">导入 / 导出记录</TabsTrigger>
)}
</TabsList>
</div>
<TabsContent value="wiki" className="mt-0 flex min-h-0 flex-1">
<WikiLibraryReader
key={baseId}
pages={libraryPages}
activePage={active}
onNavigate={navigatePage}
pageHref={(s) =>
buildAppHashUrl(
`/page/workspace/knowledge/assistant?${new URLSearchParams({ base_id: baseId, wikiSlug: s })}`,
)
}
mappingId={
active?.origins?.find((o) => o.mapping_id)?.mapping_id
}
initiallyCollapseTree
/>
</TabsContent>
<TabsContent value="search" className="overflow-auto p-6">
<div className="mb-5 flex max-w-3xl gap-2">
<Input
placeholder="用自然语言检索知识,例如:如何处理设备过热?"
value={query}
onChange={(e) => setQuery(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") void search();
}}
/>
<Button disabled={searching} onClick={() => void search()}>
{searching ? (
<Loader2 className="size-4 animate-spin" />
) : (
<Search className="size-4" />
)}
检索
</Button>
</div>
{searchResult && (
<>
<Badge variant="outline">
{searchResult.retrieval_mode === "vector"
? "知识正文 / 片段 · 向量检索"
: searchResult.retrieval_mode === "empty"
? "没有匹配结果"
: "知识正文 / 片段 · 关键词检索(暂无匹配模型的向量)"}
</Badge>
<div className="mt-4 space-y-3">
{searchResult.results.map((p) => (
<button
key={p.id}
type="button"
onClick={() => {
if (p.page_type !== "chunk") navigatePage(p.slug);
}}
className={`block w-full max-w-4xl rounded-xl border p-5 text-left ${p.page_type === "chunk" ? "cursor-default" : "hover:bg-muted"}`}
>
<div className="font-medium">
{p.title}
{p.page_type === "chunk" && (
<Badge variant="secondary" className="ml-2">
知识片段
</Badge>
)}
<span className="float-right text-xs text-muted-foreground">
{p.score?.toFixed(4)}
</span>
</div>
<p className="mt-2 line-clamp-3 text-sm text-muted-foreground">
{p.matched_section?.content ||
p.matched_content ||
p.content_markdown}
</p>
</button>
))}
</div>
</>
)}
</TabsContent>
<TabsContent value="sources" className="overflow-auto p-6">
<div className="grid gap-4 lg:grid-cols-2">
{sources.data?.map((s) => (
<div className="rounded-xl border p-5" key={s.id}>
<h3 className="font-semibold">{s.source_name}</h3>
<p className="mt-2 text-xs text-muted-foreground">
上次同步:{new Date(s.updated_at).toLocaleString()}
</p>
{isAdmin && (
<Button
className="mt-4"
size="sm"
variant="outline"
disabled={actions.reimportSource.isPending}
onClick={() =>
actions.reimportSource.mutate(s.id, {
onSuccess: () => toast.success("已提交同步任务"),
onError: (e) => toast.error(e.message),
})
}
>
<RefreshCw className="mr-2 size-4" />
同步最新数据
</Button>
)}
</div>
))}
</div>
</TabsContent>
{isAdmin && (
<TabsContent
value="transfer"
className="space-y-5 overflow-auto p-6"
>
<WikiPackageTransfers id={baseId} assistant />
<h2 className="text-base font-semibold">导入与同步记录</h2>
{jobs.data?.map((j) => (
<div key={j.id} className="rounded-xl border p-4">
<div className="flex items-center gap-3">
<Badge variant="outline">
{{
completed: "已完成",
failed: "失败",
running: "处理中",
queued: "排队中",
}[j.status] || j.status}
</Badge>
<span className="text-xs text-muted-foreground">
{new Date(j.created_at).toLocaleString()}
</span>
</div>
<p className="mt-3 text-sm">
{j.counts?.wiki_pages || 0} Wiki ·{" "}
{j.counts?.vectors || 0} 向量 · {j.counts?.entities || 0}{" "}
实体 · {j.counts?.relations || 0} 关系
</p>
{j.last_error_message && (
<p className="mt-2 text-sm text-destructive">
{j.last_error_message}
</p>
)}
</div>
))}
</TabsContent>
)}
</Tabs>
</>
)}
<Dialog open={createOpen} onOpenChange={setCreateOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>新建助手知识库</DialogTitle>
</DialogHeader>
<Input
placeholder="知识库名称"
value={name}
onChange={(e) => setName(e.target.value)}
/>
<DialogFooter>
<Button
disabled={!name.trim() || actions.createBase.isPending}
onClick={() => void create()}
>
创建
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{editingBase && (
<EditAssistantBaseDialog
key={editingBase.id}
base={editingBase}
pending={actions.updateBase.isPending}
onClose={() => setEditingBase(null)}
onSave={async (input) => {
try {
await actions.updateBase.mutateAsync({
id: editingBase.id,
...input,
});
setEditingBase(null);
toast.success("助手知识库信息已保存");
} catch (error) {
toast.error((error as Error).message);
}
}}
/>
)}
</div>
);
}
function EditAssistantBaseDialog({
base,
pending,
onClose,
onSave,
}: {
base: AssistantKnowledgeBase;
pending: boolean;
onClose: () => void;
onSave: (input: { name: string; description: string }) => Promise<void>;
}) {
const [name, setName] = useState(base.name);
const [description, setDescription] = useState(base.description || "");
return (
<Dialog
open
onOpenChange={(open) => {
if (!open && !pending) onClose();
}}
>
<DialogContent>
<form
onSubmit={(event) => {
event.preventDefault();
if (!pending && name.trim()) {
void onSave({
name: name.trim(),
description: description.trim(),
});
}
}}
>
<DialogHeader>
<DialogTitle>编辑助手知识库</DialogTitle>
<DialogDescription>
修改知识库名称和描述。知识内容、向量和关联关系不会受到影响。
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
<label className="text-sm font-medium" htmlFor="assistant-base-name">
名称
</label>
<Input
id="assistant-base-name"
value={name}
onChange={(event) => setName(event.target.value)}
maxLength={255}
disabled={pending}
required
/>
</div>
<div className="space-y-2">
<label
className="text-sm font-medium"
htmlFor="assistant-base-description"
>
描述
</label>
<Textarea
id="assistant-base-description"
value={description}
onChange={(event) => setDescription(event.target.value)}
maxLength={2048}
rows={4}
disabled={pending}
/>
</div>
</div>
<DialogFooter>
<Button type="button" variant="outline" disabled={pending} onClick={onClose}>
取消
</Button>
<Button type="submit" disabled={pending || !name.trim()}>
{pending && <Loader2 className="mr-2 size-4 animate-spin" />}
保存
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
}
+44 -9
View File
@@ -141,6 +141,7 @@ export default function ChatPage() {
// per-thread header toggle so stale localStorage can't re-enable it.
const [knowledgeRagEnabled] = useKnowledgeRagEnabled();
const [selectedKnowledgeBaseIds, setSelectedKnowledgeBaseIds] = useState<string[]>([]);
const [selectedAssistantKnowledgeBaseIds, setSelectedAssistantKnowledgeBaseIds] = useState<string[]>([]);
const [knowledgeSelectionReady, setKnowledgeSelectionReady] = useState(false);
const [knowledgeSelectionLoading, setKnowledgeSelectionLoading] = useState(false);
const knowledgeSelectionThread = useRef<string | null>(null);
@@ -149,18 +150,26 @@ export default function ChatPage() {
setSelectedKnowledgeBaseIds(ids);
setKnowledgeSelectionReady(true);
}, []);
const changeAssistantKnowledgeSelection = useCallback((ids: string[]) => {
setSelectedAssistantKnowledgeBaseIds(ids);
setKnowledgeSelectionReady(true);
}, []);
const streamContext = useMemo(
() => ({
...settings.context,
knowledge_rag_enabled: knowledgeRagEnabled,
...(knowledgeSelectionReady
? { llmwiki_knowledge_base_ids: selectedKnowledgeBaseIds }
? {
llmwiki_knowledge_base_ids: selectedKnowledgeBaseIds,
assistant_knowledge_base_ids: selectedAssistantKnowledgeBaseIds,
}
: {}),
}),
[
settings.context,
knowledgeRagEnabled,
knowledgeSelectionReady,
selectedAssistantKnowledgeBaseIds,
selectedKnowledgeBaseIds,
],
);
@@ -199,6 +208,7 @@ export default function ChatPage() {
setHasStartedConversation(false);
setPromptPrefixOn(false);
setSelectedKnowledgeBaseIds([]);
setSelectedAssistantKnowledgeBaseIds([]);
setKnowledgeSelectionReady(false);
setKnowledgeSelectionLoading(false);
knowledgeSelectionThread.current = "__new__";
@@ -224,18 +234,29 @@ export default function ChatPage() {
if (knowledgeSelectionThread.current !== threadId) return;
const metadata = record.metadata as Record<string, unknown> | undefined;
const raw = metadata?.llmwiki_knowledge_base_ids;
const assistantRaw = metadata?.assistant_knowledge_base_ids;
const llmwikiIds = Array.isArray(raw)
? raw.map((item) => String(item).trim()).filter(Boolean)
: [];
const assistantIds = Array.isArray(assistantRaw)
? assistantRaw.map((item) => String(item).trim()).filter(Boolean)
: [];
if (Array.isArray(raw)) {
setSelectedKnowledgeBaseIds(
raw.map((item) => String(item).trim()).filter(Boolean),
);
setKnowledgeSelectionReady(true);
setSelectedKnowledgeBaseIds(llmwikiIds);
} else {
setSelectedKnowledgeBaseIds([]);
setKnowledgeSelectionReady(false);
}
if (Array.isArray(assistantRaw)) {
setSelectedAssistantKnowledgeBaseIds(assistantIds);
} else {
setSelectedAssistantKnowledgeBaseIds([]);
}
setKnowledgeSelectionReady(Array.isArray(raw) || Array.isArray(assistantRaw));
})
.catch(() => {
if (knowledgeSelectionThread.current === threadId) {
setSelectedKnowledgeBaseIds([]);
setSelectedAssistantKnowledgeBaseIds([]);
setKnowledgeSelectionReady(false);
}
})
@@ -258,6 +279,7 @@ export default function ChatPage() {
if (!known.some((a) => a.id === selectedAgentId)) {
setSelectedAgentId(null);
setSelectedKnowledgeBaseIds([]);
setSelectedAssistantKnowledgeBaseIds([]);
setKnowledgeSelectionReady(false);
}
}, [selectedAgentId, selectorAgents, mineAgents, squareAgents, selectorLoading]);
@@ -268,7 +290,10 @@ export default function ChatPage() {
threadId: isNewThread ? undefined : threadId,
context: streamContext,
threadMetadata: knowledgeSelectionReady
? { llmwiki_knowledge_base_ids: selectedKnowledgeBaseIds }
? {
llmwiki_knowledge_base_ids: selectedKnowledgeBaseIds,
assistant_knowledge_base_ids: selectedAssistantKnowledgeBaseIds,
}
: undefined,
isMock,
onStart: (createdThreadId) => {
@@ -442,7 +467,10 @@ export default function ChatPage() {
metadata: {
agent_id: selectedAgentId,
...(knowledgeSelectionReady
? { llmwiki_knowledge_base_ids: selectedKnowledgeBaseIds }
? {
llmwiki_knowledge_base_ids: selectedKnowledgeBaseIds,
assistant_knowledge_base_ids: selectedAssistantKnowledgeBaseIds,
}
: {}),
},
});
@@ -453,7 +481,10 @@ export default function ChatPage() {
threadId: created.thread_id,
text,
...(knowledgeSelectionReady
? { knowledgeBaseIds: selectedKnowledgeBaseIds }
? {
knowledgeBaseIds: selectedKnowledgeBaseIds,
assistantKnowledgeBaseIds: selectedAssistantKnowledgeBaseIds,
}
: {}),
}),
);
@@ -515,6 +546,7 @@ export default function ChatPage() {
hideAdminPrompt,
preferencesLoading,
selectedAgentId,
selectedAssistantKnowledgeBaseIds,
selectedKnowledgeBaseIds,
knowledgeSelectionReady,
agentHandoffPending,
@@ -618,6 +650,7 @@ export default function ChatPage() {
const handleAgentChange = useCallback((agentId: string | null) => {
setSelectedAgentId(agentId);
setSelectedKnowledgeBaseIds([]);
setSelectedAssistantKnowledgeBaseIds([]);
setKnowledgeSelectionReady(false);
saveLastAgentId(agentId);
}, []);
@@ -900,7 +933,9 @@ export default function ChatPage() {
source={selectedAgentId ? "agent" : "all"}
agentId={selectedAgentId}
value={selectedKnowledgeBaseIds}
assistantValue={selectedAssistantKnowledgeBaseIds}
onChange={changeKnowledgeSelection}
onAssistantChange={changeAssistantKnowledgeSelection}
autoSelectAll={isLandingState && !!selectedAgentId}
disabled={
env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true" ||
@@ -3,6 +3,11 @@ import { useState } from "react";
import { Navigate, useParams, useSearchParams } from "react-router-dom";
import { Button } from "@/components/ui/button";
import {
KNOWLEDGE_NORMAL_VIEW_VALUE,
KnowledgeProductNav,
useKnowledgeBaseProductVersion,
} from "@/components/workspace/knowledge-product-nav";
import {
Dialog,
DialogContent,
@@ -70,7 +75,9 @@ export default function LlmWikiKnowledgePage({
const { noteId = "" } = useParams();
const [searchParams] = useSearchParams();
const runtime = useLlmWikiRuntime();
const productVersion = useKnowledgeBaseProductVersion();
const wikiSlug = searchParams.get("wikiSlug")?.trim() || "";
const explicitNormalView = searchParams.get("view") === KNOWLEDGE_NORMAL_VIEW_VALUE;
if (mode === "detail" && noteId && wikiSlug) {
return <Navigate to={buildWeKnoraWikiPagePath(noteId, wikiSlug)} replace />;
@@ -80,7 +87,14 @@ export default function LlmWikiKnowledgePage({
return <RuntimeLoading />;
}
if (mode === "list" && productVersion.data === "v2" && !explicitNormalView) {
return <Navigate to="/page/workspace/knowledge/assistant" replace />;
}
if (runtime.isError || runtime.data?.provider !== "weknora") {
if (mode === "list" || mode === "detail") {
return <Navigate to="/page/workspace/knowledge/assistant" replace />;
}
if (mode === "graph") {
return <KnowledgeGraphPage />;
}
@@ -95,5 +109,5 @@ export default function LlmWikiKnowledgePage({
return <WeKnoraKnowledgeDetailPage />;
}
return <WeKnoraKnowledgeWorkspace />;
return <div className="flex size-full min-h-0 flex-col"><KnowledgeProductNav /><div className="min-h-0 flex-1"><WeKnoraKnowledgeWorkspace /></div></div>;
}
+49 -3
View File
@@ -1,9 +1,55 @@
import { BookOpenCheck, Sparkles } from "lucide-react";
import { useMemo } from "react";
import { useSearchParams } from "react-router-dom";
import { SkillSettingsPage } from "@/components/workspace/settings/skill-settings-page";
import { SkillKnowledgePanel } from "@/components/workspace/skills/SkillKnowledgePanel";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
export default function SkillsPage() {
const [searchParams, setSearchParams] = useSearchParams();
const tab = useMemo(
() => (searchParams.get("tab") === "knowledge" ? "knowledge" : "skills"),
[searchParams],
);
const changeTab = (value: string) => {
const next = new URLSearchParams(searchParams);
if (value === "knowledge") next.set("tab", "knowledge");
else next.delete("tab");
setSearchParams(next, { replace: true });
};
return (
<div className="flex size-full min-h-0 flex-col overflow-auto p-6">
<SkillSettingsPage />
</div>
<Tabs
value={tab}
onValueChange={changeTab}
className="flex size-full min-h-0 flex-col gap-0 overflow-hidden"
>
<header className="flex flex-wrap items-center justify-between gap-4 border-b bg-background px-6 py-4">
<div>
<h1 className="text-xl font-semibold">技能</h1>
<p className="mt-1 text-sm text-muted-foreground">
浏览和维护技能,也可以把多个技能的知识一次归纳到一个 Wiki 知识库。
</p>
</div>
<TabsList>
<TabsTrigger value="skills">
<Sparkles className="mr-2 size-4" />
技能列表
</TabsTrigger>
<TabsTrigger value="knowledge">
<BookOpenCheck className="mr-2 size-4" />
归纳到知识库
</TabsTrigger>
</TabsList>
</header>
<TabsContent value="skills" className="mt-0 min-h-0 flex-1 overflow-auto p-6">
<SkillSettingsPage embedded />
</TabsContent>
<TabsContent value="knowledge" className="mt-0 min-h-0 flex-1 overflow-hidden">
<SkillKnowledgePanel />
</TabsContent>
</Tabs>
);
}
+192 -122
View File
@@ -1,87 +1,96 @@
import { useEffect, useState, type CSSProperties, type ReactNode } from 'react';
import { Navigate, Route, Routes, useLocation, useParams } from 'react-router-dom';
import { useEffect, useState, type CSSProperties, type ReactNode } from "react";
import {
Navigate,
Route,
Routes,
useLocation,
useParams,
} from "react-router-dom";
import { getStoredAuth } from '@/core/auth';
import { getAPIClient } from '@/core/api';
import { isSentimentAgentVisible } from '@/core/sentiment-agent/config';
import { getStoredAuth } from "@/core/auth";
import { getAPIClient } from "@/core/api";
import { isSentimentAgentVisible } from "@/core/sentiment-agent/config";
import {
resolveChatsIframeEntryRedirect,
resolveStandardChatFallback,
} from '@/core/page-layout/chats-iframe-route';
import { isEmbeddedHost } from '@/core/embed/frame-context';
} from "@/core/page-layout/chats-iframe-route";
import { isEmbeddedHost } from "@/core/embed/frame-context";
import { SidebarInset, SidebarProvider } from '@/components/ui/sidebar';
import { PromptInputProvider } from '@/components/ai-elements/prompt-input';
import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar";
import { PromptInputProvider } from "@/components/ai-elements/prompt-input";
import {
CompactShortcutButtons,
LayoutModeSwitcher,
ThemeSwitcher,
} from '@/components/workspace/appearance-controls';
import { WorkspaceSidebar } from '@/components/workspace/workspace-sidebar';
import { UserIdentityBadge } from '@/components/workspace/user-identity-badge';
import { CommandPalette } from '@/components/workspace/command-palette';
import { Toaster } from 'sonner';
import { SubtasksProvider } from '@/core/tasks/context';
import { ArtifactsProvider } from '@/components/workspace/artifacts';
import { DocumentRewritePresentationProvider } from '@/core/document-rewrite/context';
} from "@/components/workspace/appearance-controls";
import { WorkspaceSidebar } from "@/components/workspace/workspace-sidebar";
import { UserIdentityBadge } from "@/components/workspace/user-identity-badge";
import { CommandPalette } from "@/components/workspace/command-palette";
import { Toaster } from "sonner";
import { SubtasksProvider } from "@/core/tasks/context";
import { ArtifactsProvider } from "@/components/workspace/artifacts";
import { DocumentRewritePresentationProvider } from "@/core/document-rewrite/context";
import { RouteBaseProvider } from '@/core/route-base';
import { agentRefOfThread } from '@/core/threads/utils';
import { RouteBaseProvider } from "@/core/route-base";
import { agentRefOfThread } from "@/core/threads/utils";
import ChatsPage from './ChatsPage';
import ChatPage from './ChatPage';
import IframeChatPage from './IframeChatPage';
import AgentsPage from './AgentsPage';
import AgentChatPage from './AgentChatPage';
import SentimentAgentChatPage from './SentimentAgentChatPage';
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 LlmWikiKnowledgePage from './LlmWikiKnowledgePage';
import WeKnoraWikiManagementPage from '@/strategy-components/components/resource-management/llmwiki/WeKnoraWikiManagementPage';
import WeKnoraWikiPageView from '@/strategy-components/components/resource-management/llmwiki/WeKnoraWikiPageView';
import MarkdownDocxEditorPage from './MarkdownDocxEditorPage';
import LlmMetricsPage from './LlmMetricsPage';
import CheckpointMigrationPage from './CheckpointMigrationPage';
import MemoryPage from './MemoryPage';
import PromptPrefixSettingsPage from './PromptPrefixSettingsPage';
import PrivateSquareSettingsPage from './PrivateSquareSettingsPage';
import ModelConcurrencySettingsPage from './ModelConcurrencySettingsPage';
import ModelManagementPage from './ModelManagementPage';
import PublishSquaresSettingsPage from './PublishSquaresSettingsPage';
import SkillCompressionSettingsPage from './SkillCompressionSettingsPage';
import SkillEvolutionPage from './SkillEvolutionPage';
import SkillsPage from './SkillsPage';
import UsersPage from './UsersPage';
import AdminUserActivityPage from './AdminUserActivityPage';
import AdminMemoryPage from './AdminMemoryPage';
import AdminUserHubPage from './AdminUserHubPage';
import TagsPage from './TagsPage';
import PositionsPage from './PositionsPage';
import PositionRolesPage from './PositionRolesPage';
import PluginManagementPage from './PluginManagementPage';
import RecommendedQuestionsPage from './RecommendedQuestionsPage';
import AIWritingPage from '@/open-canvas/pages/AIWritingPage';
import DeepResearchPage from './DeepResearchPage';
import ReportStructureCreatePage from './deep-research/ReportStructureCreatePage';
import EnterpriseResearchPage from './EnterpriseResearchPage';
import StudioNotebooksPage from './StudioNotebooksPage';
import StudioNotebookDetailPage from './StudioNotebookDetailPage';
import RoundtablePlanningPage from '@/roundtable-planning/pages/RoundtablePlanningPage';
import PositionRoundtablePage from '@/position-roundtable/pages/PositionRoundtablePage';
import EightBfRoundtablePage from '@/eightbf-roundtable/pages/EightBfRoundtablePage';
import BusinessChainEditorPage from '@/roundtable-planning/pages/BusinessChainEditorPage';
import BusinessChainListPage from '@/roundtable-planning/pages/BusinessChainListPage';
import DispatchChainPreviewPage from '@/roundtable-planning/pages/DispatchChainPreviewPage';
import '@/roundtable-planning/styles/roundtable-planning.css';
import ChatsPage from "./ChatsPage";
import ChatPage from "./ChatPage";
import ArtifactLibraryPage from "./ArtifactLibraryPage";
import IframeChatPage from "./IframeChatPage";
import AgentsPage from "./AgentsPage";
import AgentChatPage from "./AgentChatPage";
import SentimentAgentChatPage from "./SentimentAgentChatPage";
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 LlmWikiKnowledgePage from "./LlmWikiKnowledgePage";
import AssistantKnowledgePage from "./AssistantKnowledgePage";
import WeKnoraWikiManagementPage from "@/strategy-components/components/resource-management/llmwiki/WeKnoraWikiManagementPage";
import WeKnoraWikiPageView from "@/strategy-components/components/resource-management/llmwiki/WeKnoraWikiPageView";
import WeKnoraPackageTransfersPage from "@/strategy-components/components/resource-management/llmwiki/WeKnoraPackageTransfersPage";
import MarkdownDocxEditorPage from "./MarkdownDocxEditorPage";
import LlmMetricsPage from "./LlmMetricsPage";
import CheckpointMigrationPage from "./CheckpointMigrationPage";
import MemoryPage from "./MemoryPage";
import PromptPrefixSettingsPage from "./PromptPrefixSettingsPage";
import PrivateSquareSettingsPage from "./PrivateSquareSettingsPage";
import ModelConcurrencySettingsPage from "./ModelConcurrencySettingsPage";
import ModelManagementPage from "./ModelManagementPage";
import PublishSquaresSettingsPage from "./PublishSquaresSettingsPage";
import SkillCompressionSettingsPage from "./SkillCompressionSettingsPage";
import SkillEvolutionPage from "./SkillEvolutionPage";
import SkillsPage from "./SkillsPage";
import UsersPage from "./UsersPage";
import AdminUserActivityPage from "./AdminUserActivityPage";
import AdminMemoryPage from "./AdminMemoryPage";
import AdminUserHubPage from "./AdminUserHubPage";
import TagsPage from "./TagsPage";
import PositionsPage from "./PositionsPage";
import PositionRolesPage from "./PositionRolesPage";
import PluginManagementPage from "./PluginManagementPage";
import RecommendedQuestionsPage from "./RecommendedQuestionsPage";
import AIWritingPage from "@/open-canvas/pages/AIWritingPage";
import DeepResearchPage from "./DeepResearchPage";
import ReportStructureCreatePage from "./deep-research/ReportStructureCreatePage";
import EnterpriseResearchPage from "./EnterpriseResearchPage";
import StudioNotebooksPage from "./StudioNotebooksPage";
import StudioNotebookDetailPage from "./StudioNotebookDetailPage";
import RoundtablePlanningPage from "@/roundtable-planning/pages/RoundtablePlanningPage";
import PositionRoundtablePage from "@/position-roundtable/pages/PositionRoundtablePage";
import EightBfRoundtablePage from "@/eightbf-roundtable/pages/EightBfRoundtablePage";
import BusinessChainEditorPage from "@/roundtable-planning/pages/BusinessChainEditorPage";
import BusinessChainListPage from "@/roundtable-planning/pages/BusinessChainListPage";
import DispatchChainPreviewPage from "@/roundtable-planning/pages/DispatchChainPreviewPage";
import "@/roundtable-planning/styles/roundtable-planning.css";
import ReportCollaborationPage, {
ReportCollaborationSessionPage,
} from '@/report-collaboration/pages/ReportCollaborationPage';
import '@/report-collaboration/styles/report-collaboration.css';
import { cn } from '@/lib/utils';
} from "@/report-collaboration/pages/ReportCollaborationPage";
import "@/report-collaboration/styles/report-collaboration.css";
import { cn } from "@/lib/utils";
function NotebookDetailPageWithKey() {
const { notebookId } = useParams<{ notebookId: string }>();
@@ -119,11 +128,12 @@ function ReportCollaborationSessionRoute({
function shouldShowCompactTopActions(pathname: string): boolean {
return (
pathname === '/page/workspace/agents' ||
pathname === '/page/workspace/skills' ||
pathname === '/page/workspace/memory' ||
pathname === '/page/workspace/knowledge' ||
pathname.startsWith('/page/workspace/knowledge/')
pathname === "/page/workspace/agents" ||
pathname === "/page/workspace/skills" ||
pathname === "/page/workspace/memory" ||
pathname === "/page/workspace/library" ||
pathname === "/page/workspace/knowledge" ||
pathname.startsWith("/page/workspace/knowledge/")
);
}
@@ -181,47 +191,52 @@ export function WorkspaceLayout({
// 全量模式下通用问答(/page/workspace/chats*)的历史侧栏默认展开;其余紧凑页
// (如智能体对话,为聊天区更宽而默认收起)维持原默认。
const pathname = useLocation().pathname;
const isGeneralQa = pathname.startsWith('/page/workspace/chats');
const isGeneralQa = pathname.startsWith("/page/workspace/chats");
const showCompactTopActions =
headerHidden && !compactActionsHidden && shouldShowCompactTopActions(pathname);
headerHidden &&
!compactActionsHidden &&
shouldShowCompactTopActions(pathname);
const pageSidebarWidth = pageSidebarHidden
? '0rem'
? "0rem"
: pageSidebarDark
? pageSidebarCollapsed
? '0rem'
: '118px'
? "0rem"
: "118px"
: pageSidebarCollapsed
? '4rem'
: '16rem';
? "4rem"
: "16rem";
return (
<SidebarProvider
className={[
'relative h-full w-full overflow-x-hidden',
'[&_[data-slot=sidebar-container]]:absolute',
'[&_[data-slot=sidebar-container]]:left-0',
'[&_[data-slot=sidebar-container]]:top-0',
].join(' ')}
"relative h-full w-full overflow-x-hidden",
"[&_[data-slot=sidebar-container]]:absolute",
"[&_[data-slot=sidebar-container]]:left-0",
"[&_[data-slot=sidebar-container]]:top-0",
].join(" ")}
style={
{
'--page-sidebar-width': pageSidebarWidth,
'--header-height': headerHidden ? '0rem' : '4rem',
"--page-sidebar-width": pageSidebarWidth,
"--header-height": headerHidden ? "0rem" : "4rem",
} as CSSProperties
}
// Agent and general-chat routes share this layout component. Keep their
// sidebar state separate: otherwise React preserves the collapsed agent
// state when a history item switches to `/chats/:thread_id`, while the
// new page's header button is meant to control GeneralQaSidebar.
key={`${String(compactWorkspaceSidebar)}:${isGeneralQa ? 'general' : 'workspace'}`}
key={`${String(compactWorkspaceSidebar)}:${isGeneralQa ? "general" : "workspace"}`}
data-page-sidebar-collapsed={pageSidebarCollapsed}
defaultOpen={
forceWorkspaceSidebarCollapsed
? false
: (defaultHistorySidebarOpen ?? (isGeneralQa || !compactWorkspaceSidebar))
: (defaultHistorySidebarOpen ??
(isGeneralQa || !compactWorkspaceSidebar))
}
open={forceWorkspaceSidebarCollapsed ? false : undefined}
onOpenChange={forceWorkspaceSidebarCollapsed ? () => undefined : undefined}
onOpenChange={
forceWorkspaceSidebarCollapsed ? () => undefined : undefined
}
disableToggle={forceWorkspaceSidebarCollapsed}
>
{!workspaceSidebarHidden && (
@@ -231,7 +246,7 @@ export function WorkspaceLayout({
/>
)}
<SidebarInset
className={cn('min-w-0', insetOverflowHidden && 'overflow-hidden')}
className={cn("min-w-0", insetOverflowHidden && "overflow-hidden")}
>
{showCompactTopActions ? (
<>
@@ -268,9 +283,14 @@ export function ChatRuntime({ children }: { children: ReactNode }) {
*/
function NewChatContent() {
const location = useLocation();
const agentId = new URLSearchParams(location.search).get('agent');
const agentId = new URLSearchParams(location.search).get("agent");
if (agentId) {
return <AgentChatPage key={`${agentId}:${location.key}`} embeddedAgentId={agentId} />;
return (
<AgentChatPage
key={`${agentId}:${location.key}`}
embeddedAgentId={agentId}
/>
);
}
return <ChatPage />;
}
@@ -417,8 +437,8 @@ function AgentAwareChatThreadContent() {
};
// Task-bound threads need their task host to preserve the purpose gate,
// action partitioning, and other task-specific controls.
const isTaskThread = typeof record.metadata?.taskId === 'string';
setAgentId(isTaskThread ? null : agentRefOfThread(record) ?? null);
const isTaskThread = typeof record.metadata?.taskId === "string";
setAgentId(isTaskThread ? null : (agentRefOfThread(record) ?? null));
})
.catch(() => {
if (!cancelled) setAgentId(null);
@@ -545,13 +565,34 @@ export default function WorkspaceRoutes({
path="chats/:thread_id"
element={<ChatThreadEntryRoute layoutProps={layoutProps} />}
/>
<Route
path="library"
element={
<WorkspaceLayout
{...layoutProps}
workspaceSidebarHidden={
layoutProps.workspaceSidebarHidden ||
!layoutProps.pageSidebarHidden
}
>
<ArtifactLibraryPage />
</WorkspaceLayout>
}
/>
<Route
path="chats_iframe/new"
element={<IframeChatEntryRoute layoutProps={layoutProps} hasThreadParam={false} />}
element={
<IframeChatEntryRoute
layoutProps={layoutProps}
hasThreadParam={false}
/>
}
/>
<Route
path="chats_iframe/:thread_id"
element={<IframeChatEntryRoute layoutProps={layoutProps} hasThreadParam />}
element={
<IframeChatEntryRoute layoutProps={layoutProps} hasThreadParam />
}
/>
<Route
path="agents"
@@ -585,7 +626,10 @@ export default function WorkspaceRoutes({
path="agents/sentiment-analysis/chats"
element={
isSentimentAgentVisible() ? (
<Navigate to="/page/workspace/agents/sentiment-analysis/chats/new" replace />
<Navigate
to="/page/workspace/agents/sentiment-analysis/chats/new"
replace
/>
) : (
<Navigate to="/page/workspace/agents" replace />
)
@@ -615,7 +659,9 @@ export default function WorkspaceRoutes({
<AgentChatPage
showAgentList
compactAppearanceActions={
headerHidden && !compactActionsHidden ? <CompactAppearanceActions /> : undefined
headerHidden && !compactActionsHidden ? (
<CompactAppearanceActions />
) : undefined
}
/>
</ChatRuntime>
@@ -714,7 +760,9 @@ export default function WorkspaceRoutes({
element={
<WorkspaceLayout {...layoutProps}>
<ChatRuntime>
<RoundtablePlanningPage compactChrome={headerHidden && !compactActionsHidden} />
<RoundtablePlanningPage
compactChrome={headerHidden && !compactActionsHidden}
/>
</ChatRuntime>
</WorkspaceLayout>
}
@@ -724,7 +772,9 @@ export default function WorkspaceRoutes({
element={
<WorkspaceLayout {...layoutProps}>
<ChatRuntime>
<EightBfRoundtablePage compactChrome={headerHidden && !compactActionsHidden} />
<EightBfRoundtablePage
compactChrome={headerHidden && !compactActionsHidden}
/>
</ChatRuntime>
</WorkspaceLayout>
}
@@ -734,7 +784,9 @@ export default function WorkspaceRoutes({
element={
<WorkspaceLayout {...layoutProps}>
<ChatRuntime>
<PositionRoundtablePage compactChrome={headerHidden && !compactActionsHidden} />
<PositionRoundtablePage
compactChrome={headerHidden && !compactActionsHidden}
/>
</ChatRuntime>
</WorkspaceLayout>
}
@@ -794,6 +846,14 @@ export default function WorkspaceRoutes({
</WorkspaceLayout>
}
/>
<Route
path="knowledge/assistant"
element={
<WorkspaceLayout {...layoutProps}>
<AssistantKnowledgePage />
</WorkspaceLayout>
}
/>
<Route
path="knowledge/:noteId/wiki/view"
element={
@@ -810,6 +870,14 @@ export default function WorkspaceRoutes({
</WorkspaceLayout>
}
/>
<Route
path="knowledge/:noteId/packages"
element={
<WorkspaceLayout {...layoutProps}>
<WeKnoraPackageTransfersPage />
</WorkspaceLayout>
}
/>
<Route
path="knowledge/markdown-editor"
element={
@@ -834,7 +902,9 @@ export default function WorkspaceRoutes({
path="scheduled/tasks"
element={
<WorkspaceLayout {...layoutProps}>
<ScheduledTasksPage compactChrome={headerHidden && !compactActionsHidden} />
<ScheduledTasksPage
compactChrome={headerHidden && !compactActionsHidden}
/>
</WorkspaceLayout>
}
/>
@@ -850,7 +920,7 @@ export default function WorkspaceRoutes({
<Route
path="admin"
element={
getStoredAuth()?.system_role === 'admin' ? (
getStoredAuth()?.system_role === "admin" ? (
<WorkspaceLayout {...layoutProps} pageSidebarHidden>
<AdminUserHubPage />
</WorkspaceLayout>
@@ -875,7 +945,7 @@ export default function WorkspaceRoutes({
<Route
path="admin/scheduled-tasks"
element={
getStoredAuth()?.system_role === 'admin' ? (
getStoredAuth()?.system_role === "admin" ? (
<WorkspaceLayout {...layoutProps}>
<AdminScheduledTasksPage />
</WorkspaceLayout>
@@ -887,7 +957,7 @@ export default function WorkspaceRoutes({
<Route
path="admin/agents"
element={
getStoredAuth()?.system_role === 'admin' ? (
getStoredAuth()?.system_role === "admin" ? (
<WorkspaceLayout {...layoutProps}>
<AdminAgentsPage />
</WorkspaceLayout>
@@ -899,7 +969,7 @@ export default function WorkspaceRoutes({
<Route
path="admin/skills"
element={
getStoredAuth()?.system_role === 'admin' ? (
getStoredAuth()?.system_role === "admin" ? (
<WorkspaceLayout {...layoutProps}>
<AdminSkillsPage />
</WorkspaceLayout>
@@ -919,7 +989,7 @@ export default function WorkspaceRoutes({
<Route
path="admin/plugins"
element={
getStoredAuth()?.system_role === 'admin' ? (
getStoredAuth()?.system_role === "admin" ? (
<WorkspaceLayout {...layoutProps}>
<PluginManagementPage />
</WorkspaceLayout>
@@ -931,7 +1001,7 @@ export default function WorkspaceRoutes({
<Route
path="admin/positions"
element={
getStoredAuth()?.system_role === 'admin' ? (
getStoredAuth()?.system_role === "admin" ? (
<WorkspaceLayout {...layoutProps}>
<PositionsPage />
</WorkspaceLayout>
@@ -943,7 +1013,7 @@ export default function WorkspaceRoutes({
<Route
path="admin/position-roles"
element={
getStoredAuth()?.system_role === 'admin' ? (
getStoredAuth()?.system_role === "admin" ? (
<WorkspaceLayout {...layoutProps}>
<PositionRolesPage />
</WorkspaceLayout>
@@ -955,7 +1025,7 @@ export default function WorkspaceRoutes({
<Route
path="settings/recommended-questions"
element={
getStoredAuth()?.system_role === 'admin' ? (
getStoredAuth()?.system_role === "admin" ? (
<WorkspaceLayout {...layoutProps}>
<RecommendedQuestionsPage />
</WorkspaceLayout>
@@ -967,7 +1037,7 @@ export default function WorkspaceRoutes({
<Route
path="settings/prompt-prefix"
element={
getStoredAuth()?.system_role === 'admin' ? (
getStoredAuth()?.system_role === "admin" ? (
<WorkspaceLayout {...layoutProps}>
<PromptPrefixSettingsPage />
</WorkspaceLayout>
@@ -979,7 +1049,7 @@ export default function WorkspaceRoutes({
<Route
path="settings/private-square"
element={
getStoredAuth()?.system_role === 'admin' ? (
getStoredAuth()?.system_role === "admin" ? (
<WorkspaceLayout {...layoutProps}>
<PrivateSquareSettingsPage />
</WorkspaceLayout>
@@ -991,7 +1061,7 @@ export default function WorkspaceRoutes({
<Route
path="admin/models"
element={
getStoredAuth()?.system_role === 'admin' ? (
getStoredAuth()?.system_role === "admin" ? (
<WorkspaceLayout {...layoutProps}>
<ModelManagementPage />
</WorkspaceLayout>
@@ -1003,7 +1073,7 @@ export default function WorkspaceRoutes({
<Route
path="settings/model-concurrency"
element={
getStoredAuth()?.system_role === 'admin' ? (
getStoredAuth()?.system_role === "admin" ? (
<WorkspaceLayout {...layoutProps}>
<ModelConcurrencySettingsPage />
</WorkspaceLayout>
@@ -1015,7 +1085,7 @@ export default function WorkspaceRoutes({
<Route
path="settings/publish-squares"
element={
getStoredAuth()?.system_role === 'admin' ? (
getStoredAuth()?.system_role === "admin" ? (
<WorkspaceLayout {...layoutProps}>
<PublishSquaresSettingsPage />
</WorkspaceLayout>
@@ -1027,7 +1097,7 @@ export default function WorkspaceRoutes({
<Route
path="settings/skill-compression"
element={
getStoredAuth()?.system_role === 'admin' ? (
getStoredAuth()?.system_role === "admin" ? (
<WorkspaceLayout {...layoutProps}>
<SkillCompressionSettingsPage />
</WorkspaceLayout>
@@ -1039,7 +1109,7 @@ export default function WorkspaceRoutes({
<Route
path="settings/llm-metrics"
element={
getStoredAuth()?.system_role === 'admin' ? (
getStoredAuth()?.system_role === "admin" ? (
<WorkspaceLayout {...layoutProps}>
<LlmMetricsPage />
</WorkspaceLayout>
@@ -1051,7 +1121,7 @@ export default function WorkspaceRoutes({
<Route
path="settings/checkpoint-migration"
element={
getStoredAuth()?.system_role === 'admin' ? (
getStoredAuth()?.system_role === "admin" ? (
<WorkspaceLayout {...layoutProps}>
<CheckpointMigrationPage />
</WorkspaceLayout>