diff --git a/frontend-web/src/components/wiki/KnowledgeManage.tsx b/frontend-web/src/components/wiki/KnowledgeManage.tsx new file mode 100644 index 00000000..808c3894 --- /dev/null +++ b/frontend-web/src/components/wiki/KnowledgeManage.tsx @@ -0,0 +1,429 @@ +import { Plus, Trash2 } from "lucide-react"; +import { useEffect, 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 { ScrollArea } from "@/components/ui/scroll-area"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Textarea } from "@/components/ui/textarea"; +import { + useExtractTemplateActions, + useExtractTemplates, + useFolderActions, + useKnowledgeFolders, +} from "@/core/knowledge/hooks"; +import type { + ExtractTemplateScope, + KnowledgeExtractTemplate, +} from "@/core/knowledge/types"; +import { cn } from "@/lib/utils"; + +const NONE = "__none__"; + +/** A dropdown of managed directories (+ an "unfiled" option). */ +export function FolderSelect({ + value, + onChange, + disabled, + triggerClassName, + allowNone = true, +}: { + value: string | null; + onChange: (v: string | null) => void; + disabled?: boolean; + triggerClassName?: string; + allowNone?: boolean; +}) { + const { data: folders = [] } = useKnowledgeFolders(); + return ( + + ); +} + +/** A dropdown of enabled extraction templates applicable to a scope. */ +export function TemplateSelect({ + value, + onChange, + scope, + disabled, + triggerClassName, +}: { + value: string | null; + onChange: (v: string | null) => void; + scope: ExtractTemplateScope; + disabled?: boolean; + triggerClassName?: string; +}) { + const { data: templates = [] } = useExtractTemplates(scope, true); + return ( + + ); +} + +/** Create / rename / delete knowledge directories. */ +export function FolderManageDialog({ + open, + onOpenChange, +}: { + open: boolean; + onOpenChange: (v: boolean) => void; +}) { + const { data: folders = [], isLoading } = useKnowledgeFolders(); + const { create, rename, remove } = useFolderActions(); + const [newPath, setNewPath] = useState(""); + + const handleCreate = async () => { + const path = newPath.trim(); + if (!path) { + toast.error("请输入目录路径"); + return; + } + try { + await create.mutateAsync(path); + toast.success("目录已创建"); + setNewPath(""); + } catch (error) { + toast.error(error instanceof Error ? error.message : "创建失败"); + } + }; + + const handleRename = async (folderId: string, current: string) => { + const next = window.prompt("重命名 / 移动目录(输入新的完整路径,斜杠分隔)", current); + if (next === null) return; + try { + await rename.mutateAsync({ folderId, path: next }); + toast.success("已更新"); + } catch (error) { + toast.error(error instanceof Error ? error.message : "更新失败"); + } + }; + + const handleDelete = async (folderId: string, path: string) => { + if (!window.confirm(`删除目录「${path}」?其中的知识将变为未归类(不会被删除)。`)) return; + try { + await remove.mutateAsync({ folderId }); + toast.success("目录已删除"); + } catch (error) { + toast.error(error instanceof Error ? error.message : "删除失败"); + } + }; + + return ( + + + + 目录管理 + + 自建知识目录,用斜杠分隔多级(如「投研/行业」)。删除目录只会让其中的知识变为未归类。 + + +
+ setNewPath(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter") void handleCreate(); + }} + /> + +
+ + {isLoading ? ( +
加载中...
+ ) : folders.length === 0 ? ( +
+ 还没有目录,先在上方新建一个。 +
+ ) : ( +
    + {folders.map((f) => ( +
  • + {f.path} +
    + + +
    +
  • + ))} +
+ )} +
+ + + +
+
+ ); +} + +const SCOPE_LABELS: Record = { + thread: "对话", + document: "文档", + both: "通用", +}; + +const EMPTY_DRAFT = { + name: "", + scope: "both" as ExtractTemplateScope, + description: "", + system_prompt: "", + is_default: false, + enabled: true, +}; + +/** Create / edit / delete the editable extraction-prompt templates. */ +export function ExtractTemplateManageDialog({ + open, + onOpenChange, +}: { + open: boolean; + onOpenChange: (v: boolean) => void; +}) { + const { data: templates = [], isLoading } = useExtractTemplates(); + const { create, update, remove } = useExtractTemplateActions(); + const [selectedId, setSelectedId] = useState(null); + const [draft, setDraft] = useState({ ...EMPTY_DRAFT }); + + const selected = useMemo( + () => templates.find((t) => t.id === selectedId) ?? null, + [templates, selectedId], + ); + + // Load the selected template into the editable draft (or reset for "new"). + useEffect(() => { + if (selected) { + setDraft({ + name: selected.name, + scope: selected.scope, + description: selected.description ?? "", + system_prompt: selected.system_prompt, + is_default: selected.is_default, + enabled: selected.enabled, + }); + } else { + setDraft({ ...EMPTY_DRAFT }); + } + }, [selected]); + + const startNew = () => setSelectedId(null); + + const handleSave = async () => { + if (!draft.name.trim() || !draft.system_prompt.trim()) { + toast.error("请填写名称与提示词正文"); + return; + } + try { + if (selectedId) { + await update.mutateAsync({ templateId: selectedId, data: draft }); + toast.success("已保存"); + } else { + const created = await create.mutateAsync(draft); + setSelectedId(created.id); + toast.success("模板已创建"); + } + } catch (error) { + toast.error(error instanceof Error ? error.message : "保存失败"); + } + }; + + const handleDelete = async (t: KnowledgeExtractTemplate) => { + if (!window.confirm(`删除模板「${t.name}」?`)) return; + try { + await remove.mutateAsync(t.id); + if (selectedId === t.id) setSelectedId(null); + toast.success("已删除"); + } catch (error) { + toast.error(error instanceof Error ? error.message : "删除失败"); + } + }; + + return ( + + + + 抽取提示词模板 + + 编辑驱动「知识沉淀」的提炼提示词,可新建多套抽取逻辑。沉淀对话/导入文件时可选用对应模板;未选则用该类型的默认模板。 + + +
+ {/* template list */} +
+ + + {isLoading ? ( +
加载中...
+ ) : ( +
    + {templates.map((t) => ( +
  • + +
  • + ))} +
+ )} +
+
+ + {/* editor */} +
+
+ setDraft((d) => ({ ...d, name: e.target.value }))} + /> + +
+ setDraft((d) => ({ ...d, description: e.target.value }))} + /> +