"use client"; import { zodResolver } from "@hookform/resolvers/zod"; import { useEffect, useMemo, useState } from "react"; import { Controller, type 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 { Textarea } from "@/components/ui/textarea"; import { fetchKnowledgeBase, type CreateKnowledgeBasePayload, type KnowledgeBase, } from "@/lib/api/admin"; import { useI18n } from "@/i18n/provider"; import { KnowledgeAnswerMode, KnowledgeBaseType, KnowledgeChunkProvider, } from "@/lib/generated/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), remark: "", }; type TFunction = (key: string, values?: Record) => string; function createKnowledgeBaseFormSchema(t: TFunction) { return z.object({ name: z.string().trim().min(1, t("knowledge.nameRequired")).max(100, t("knowledge.nameMax")), description: z.string().trim().max(500, t("knowledge.descriptionMax")), knowledgeType: z.string().trim().min(1, t("knowledge.typeRequired")), defaultTopK: z.string().trim().min(1, t("knowledge.topKRequired")), defaultScoreThreshold: z.string().trim().min(1, t("knowledge.scoreRequired")), defaultRerankLimit: z.string().trim().min(1, t("knowledge.rerankRequired")), chunkProvider: z.string().trim().min(1, t("knowledge.chunkProviderRequired")), chunkTargetTokens: z.string().trim().min(1, t("knowledge.targetTokensRequired")), chunkMaxTokens: z.string().trim().min(1, t("knowledge.maxTokensRequired")), chunkOverlapTokens: z.string().trim().min(1, t("knowledge.overlapTokensRequired")), answerMode: z.string().trim().min(1, t("knowledge.answerModeRequired")), remark: z.string().trim().max(500, t("knowledge.remarkMax")), }); } type EditForm = { name: string; description: string; knowledgeType: string; defaultTopK: string; defaultScoreThreshold: string; defaultRerankLimit: string; chunkProvider: string; chunkTargetTokens: string; chunkMaxTokens: string; chunkOverlapTokens: string; answerMode: string; remark: string; }; function getKnowledgeTypeOptions(t: TFunction) { return [ { value: KnowledgeBaseType.Document, label: t("knowledge.typeDocument") }, { value: KnowledgeBaseType.FAQ, label: t("knowledge.typeFAQ") }, ]; } function getChunkProviderOptions(t: TFunction) { return [ { value: KnowledgeChunkProvider.Fixed, label: t("knowledge.chunkFixed") }, { value: KnowledgeChunkProvider.Structured, label: t("knowledge.chunkStructured") }, { value: KnowledgeChunkProvider.Semantic, label: t("knowledge.chunkSemantic") }, ]; } function getAnswerModeOptions(t: TFunction) { return [ { value: String(KnowledgeAnswerMode.Strict), label: t("knowledge.answerStrict") }, { value: String(KnowledgeAnswerMode.Assist), label: t("knowledge.answerAssist") }, ]; } 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), 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), 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 t = useI18n(); const formId = "knowledge-base-edit-form"; const [loading, setLoading] = useState(false); const knowledgeBaseFormSchema = useMemo(() => createKnowledgeBaseFormSchema(t), [t]); const editFormResolver = useMemo( () => zodResolver(knowledgeBaseFormSchema) as Resolver, [knowledgeBaseFormSchema], ); const knowledgeTypeOptions = useMemo(() => getKnowledgeTypeOptions(t), [t]); const chunkProviderOptions = useMemo(() => getChunkProviderOptions(t), [t]); const answerModeOptions = useMemo(() => getAnswerModeOptions(t), [t]); const form = useForm({ 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 ? (
{t("knowledge.loading")}
) : (
{t("knowledge.knowledgeType")} ( )} /> {t("knowledge.name")} {t("knowledge.description")}