"use client"; import { zodResolver } from "@hookform/resolvers/zod"; import { useEffect, useState } from "react"; import { Controller, Resolver, useForm } from "react-hook-form"; import { z } from "zod/v4"; import { OptionCombobox } from "@/components/option-combobox"; import { ProjectDialog } from "@/components/project-dialog"; import { Button } from "@/components/ui/button"; import { Field, FieldContent, FieldError, FieldLabel, } from "@/components/ui/field"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Textarea } from "@/components/ui/textarea"; import { fetchKnowledgeBase, type CreateKnowledgeBasePayload, type KnowledgeBase, } from "@/lib/api/admin"; import { KnowledgeBaseType, KnowledgeBaseTypeLabels, KnowledgeChunkProvider, KnowledgeChunkProviderLabels, KnowledgeAnswerMode, KnowledgeAnswerModeLabels, KnowledgeFallbackMode, KnowledgeFallbackModeLabels, } from "@/lib/generated/enums"; import { getEnumLabel, getEnumOptions } from "@/lib/enums"; type KnowledgeBaseEditDialogProps = { open: boolean; saving: boolean; itemId: number | null; onOpenChange: (open: boolean) => void; onSubmit: (payload: CreateKnowledgeBasePayload) => Promise; }; const emptyForm: EditForm = { name: "", description: "", knowledgeType: KnowledgeBaseType.Document, defaultTopK: "5", defaultScoreThreshold: "0.2", defaultRerankLimit: "10", chunkProvider: KnowledgeChunkProvider.Structured, chunkTargetTokens: "300", chunkMaxTokens: "400", chunkOverlapTokens: "40", answerMode: String(KnowledgeAnswerMode.Strict), fallbackMode: String(KnowledgeFallbackMode.NoAnswer), remark: "", }; const knowledgeBaseFormSchema = z.object({ name: z.string().trim().min(1, "名称不能为空").max(100, "名称最多100个字符"), description: z.string().trim().max(500, "描述最多500个字符"), knowledgeType: z.string().trim().min(1, "请选择知识库类型"), defaultTopK: z.string().trim().min(1, "请输入TopK值"), defaultScoreThreshold: z.string().trim().min(1, "请输入分数阈值"), defaultRerankLimit: z.string().trim().min(1, "请输入重排序限制"), chunkProvider: z.string().trim().min(1, "请选择分块策略"), chunkTargetTokens: z.string().trim().min(1, "请输入目标 token 数"), chunkMaxTokens: z.string().trim().min(1, "请输入最大 token 数"), chunkOverlapTokens: z.string().trim().min(1, "请输入重叠 token 数"), answerMode: z.string().trim().min(1, "请选择回答模式"), fallbackMode: z.string().trim().min(1, "请选择回退模式"), remark: z.string().trim().max(500, "备注最多500个字符"), }); type EditForm = z.infer; const editFormResolver = zodResolver( knowledgeBaseFormSchema as never, ) as Resolver< z.input, undefined, z.output >; function buildForm(item: KnowledgeBase | null): EditForm { if (!item) { return emptyForm; } return { name: item.name, description: item.description || "", knowledgeType: item.knowledgeType || KnowledgeBaseType.Document, defaultTopK: String(item.defaultTopK), defaultScoreThreshold: String(item.defaultScoreThreshold), defaultRerankLimit: String(item.defaultRerankLimit), chunkProvider: item.chunkProvider, chunkTargetTokens: String(item.chunkTargetTokens), chunkMaxTokens: String(item.chunkMaxTokens), chunkOverlapTokens: String(item.chunkOverlapTokens), answerMode: String(item.answerMode), fallbackMode: String(item.fallbackMode), remark: item.remark || "", }; } function buildPayload(form: EditForm): CreateKnowledgeBasePayload { return { name: form.name.trim(), description: form.description.trim(), knowledgeType: form.knowledgeType, defaultTopK: Number(form.defaultTopK), defaultScoreThreshold: Number(form.defaultScoreThreshold), defaultRerankLimit: Number(form.defaultRerankLimit), chunkProvider: form.chunkProvider, chunkTargetTokens: Number(form.chunkTargetTokens), chunkMaxTokens: Number(form.chunkMaxTokens), chunkOverlapTokens: Number(form.chunkOverlapTokens), answerMode: Number(form.answerMode), fallbackMode: Number(form.fallbackMode), remark: form.remark.trim(), }; } export function EditDialog({ open, saving, itemId, onOpenChange, onSubmit, }: KnowledgeBaseEditDialogProps) { if (!open) { return null; } return ( ); } type KnowledgeBaseFormDialogBodyProps = { open: boolean; saving: boolean; itemId: number | null; onOpenChange: (open: boolean) => void; onSubmit: (payload: CreateKnowledgeBasePayload) => Promise; }; function KnowledgeBaseFormDialogBody({ open, saving, itemId, onOpenChange, onSubmit, }: KnowledgeBaseFormDialogBodyProps) { const formId = "knowledge-base-edit-form"; const [loading, setLoading] = useState(false); const form = useForm< z.input, undefined, z.output >({ resolver: editFormResolver, defaultValues: buildForm(null), }); const { control, handleSubmit, reset, register, watch, formState: { errors }, } = form; const knowledgeType = watch("knowledgeType"); const isFAQKnowledgeBase = knowledgeType === KnowledgeBaseType.FAQ; useEffect(() => { if (!open) { return; } if (itemId === null) { reset(buildForm(null)); return; } let cancelled = false; async function loadItem() { try { setLoading(true); const data = await fetchKnowledgeBase(itemId!); if (!cancelled) { reset(buildForm(data)); } } catch (error) { if (!cancelled) { console.error("Failed to load knowledge base:", error); } } finally { if (!cancelled) { setLoading(false); } } } void loadItem(); return () => { cancelled = true; }; }, [open, itemId, reset]); async function onFormSubmit(values: EditForm) { const payload = buildPayload(values); await onSubmit(payload); } return ( } > {loading ? (
加载中...
) : (
知识库类型 ( ({ value: String(option.value), label: option.label, }))} placeholder="选择知识库类型" searchPlaceholder="搜索知识库类型" emptyText="没有匹配的知识库类型" onChange={field.onChange} /> )} /> 名称 描述