"use client"; import { FormEvent, useMemo, useState } from "react"; import { Edit3, Plus, Search, Trash2 } from "lucide-react"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Dialog } from "@/components/ui/dialog"; import { Input, Textarea } from "@/components/ui/input"; import { Select } from "@/components/ui/select"; import { Table } from "@/components/ui/table"; import type { AiKnowledgeScope, AiKnowledgeStatus, KnowledgeEntry, KnowledgeEntryInput } from "@/modules/ai/types"; import type { ApiResult } from "@/modules/core/server/api"; import type { Permission } from "@/modules/core/types"; type KnowledgeManagementClientProps = { initialEntries: KnowledgeEntry[]; permissions: Permission[]; }; type KnowledgeFormState = { scope: AiKnowledgeScope; title: string; category: string; tags: string; body: string; status: AiKnowledgeStatus; }; const emptyForm: KnowledgeFormState = { scope: "organization", title: "", category: "", tags: "", body: "", status: "enabled", }; const scopeOptions = [ { value: "organization", label: "机构私有" }, { value: "platform", label: "平台共享" }, ]; const statusOptions = [ { value: "enabled", label: "启用" }, { value: "disabled", label: "停用" }, ]; function entryToForm(entry: KnowledgeEntry): KnowledgeFormState { return { scope: entry.scope, title: entry.title, category: entry.category, tags: entry.tags, body: entry.body, status: entry.status, }; } function matchesEntry(entry: KnowledgeEntry, query: string): boolean { const normalized = query.trim().toLowerCase(); if (!normalized) { return true; } return [entry.title, entry.category, entry.tags, entry.body].join(" ").toLowerCase().includes(normalized); } export function KnowledgeManagementClient({ initialEntries, permissions }: KnowledgeManagementClientProps): React.ReactElement { const [entries, setEntries] = useState(initialEntries); const [query, setQuery] = useState(""); const [editingId, setEditingId] = useState(null); const [deleteTarget, setDeleteTarget] = useState(null); const [form, setForm] = useState(emptyForm); const [isDialogOpen, setIsDialogOpen] = useState(false); const [isPending, setIsPending] = useState(false); const [message, setMessage] = useState(""); const canManage = permissions.includes("knowledge:manage"); const filteredEntries = useMemo(() => entries.filter((entry) => matchesEntry(entry, query)), [entries, query]); const isEditing = editingId !== null; function updateField(key: K, value: KnowledgeFormState[K]): void { setForm((current) => ({ ...current, [key]: value })); } function beginCreate(): void { setEditingId(null); setForm(emptyForm); setMessage(""); setIsDialogOpen(true); } function beginEdit(entry: KnowledgeEntry): void { setEditingId(entry.id); setForm(entryToForm(entry)); setMessage(""); setIsDialogOpen(true); } function closeDialog(): void { if (isPending) { return; } setIsDialogOpen(false); setEditingId(null); setForm(emptyForm); } async function refreshEntries(): Promise { const response = await fetch("/api/ai/knowledge"); const result = (await response.json()) as ApiResult<{ entries: KnowledgeEntry[] }>; if (result.success) { setEntries(result.entries); } } async function submit(event: FormEvent): Promise { event.preventDefault(); if (!canManage) { setMessage("当前角色无权维护知识库"); return; } setIsPending(true); setMessage(""); const payload: KnowledgeEntryInput = { ...form, }; const response = await fetch(isEditing ? `/api/ai/knowledge/${editingId}` : "/api/ai/knowledge", { method: isEditing ? "PATCH" : "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(payload), }); const result = (await response.json()) as ApiResult<{ entry: KnowledgeEntry }>; setIsPending(false); if (!result.success) { setMessage(result.reason); return; } await refreshEntries(); closeDialog(); setMessage(result.reason); } async function remove(): Promise { if (!deleteTarget || !canManage) { return; } setIsPending(true); setMessage(""); const response = await fetch(`/api/ai/knowledge/${deleteTarget.id}`, { method: "DELETE" }); const result = (await response.json()) as ApiResult<{ entry: KnowledgeEntry }>; setIsPending(false); if (!result.success) { setMessage(result.reason); return; } await refreshEntries(); setDeleteTarget(null); setMessage(result.reason); } async function toggleStatus(entry: KnowledgeEntry): Promise { const nextStatus: AiKnowledgeStatus = entry.status === "enabled" ? "disabled" : "enabled"; const response = await fetch(`/api/ai/knowledge/${entry.id}`, { method: "PATCH", headers: { "content-type": "application/json" }, body: JSON.stringify({ ...entry, status: nextStatus }), }); const result = (await response.json()) as ApiResult<{ entry: KnowledgeEntry }>; if (!result.success) { setMessage(result.reason); return; } await refreshEntries(); setMessage(result.reason); } return (

知识库

维护平台共享和机构私有知识,保存后会重建本地关键词检索分块。

知识条目 共 {filteredEntries.length} 条匹配记录。
{message ? (

{message}

) : null}
标题 范围 分类 标签 状态 操作 {filteredEntries.map((entry) => (

{entry.title}

{entry.body}

{entry.scope === "platform" ? "平台共享" : "机构私有"} {entry.category || "-"} {entry.tags || "-"} {entry.status === "enabled" ? "启用" : "停用"}
))} {filteredEntries.length === 0 ? ( 暂无知识库条目 ) : null}
范围 updateField("status", value as AiKnowledgeStatus)} options={statusOptions} value={form.status} />