Files
ai-agent/web/app/dashboard/knowledge/_components/faq-edit.tsx
T

290 lines
8.6 KiB
TypeScript
Raw Normal View History

2026-04-09 10:01:23 +08:00
"use client";
2026-05-25 12:06:15 +08:00
import { useEffect, useMemo, useState } from "react";
2026-04-09 10:01:23 +08:00
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm, type Resolver } from "react-hook-form";
import { z } from "zod/v4";
import { ProjectDialog } from "@/components/project-dialog";
import { OptionCombobox } from "@/components/option-combobox";
2026-04-09 10:01:23 +08:00
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 {
fetchKnowledgeFAQ,
fetchKnowledgeDirectories,
2026-04-09 10:01:23 +08:00
type CreateKnowledgeFAQPayload,
type KnowledgeDirectory,
2026-04-09 10:01:23 +08:00
type KnowledgeFAQ,
} from "@/lib/api/admin";
2026-05-25 12:06:15 +08:00
import { useI18n } from "@/i18n/provider";
2026-04-09 10:01:23 +08:00
type FAQEditDialogProps = {
open: boolean;
saving: boolean;
itemId: number | null;
knowledgeBaseId: number | null;
initialDirectoryId?: number;
2026-04-09 10:01:23 +08:00
onOpenChange: (open: boolean) => void;
onSubmit: (payload: CreateKnowledgeFAQPayload) => Promise<void>;
};
2026-05-25 12:06:15 +08:00
type TFunction = (key: string, values?: Record<string, string | number>) => string;
2026-04-09 10:01:23 +08:00
2026-05-25 12:06:15 +08:00
function createFormSchema(t: TFunction) {
return z.object({
directoryId: z.string().trim(),
2026-05-25 12:06:15 +08:00
question: z.string().trim().min(1, t("knowledge.faqQuestionRequired")).max(500, t("knowledge.faqQuestionMax")),
answer: z.string().trim().min(1, t("knowledge.faqAnswerRequired")),
similarQuestionsText: z.string(),
remark: z.string().trim().max(500, t("knowledge.remarkMax")),
});
}
2026-04-09 10:01:23 +08:00
2026-05-25 12:06:15 +08:00
type EditForm = {
directoryId: string;
2026-05-25 12:06:15 +08:00
question: string;
answer: string;
similarQuestionsText: string;
remark: string;
};
2026-04-09 10:01:23 +08:00
const emptyForm: EditForm = {
directoryId: "0",
2026-04-09 10:01:23 +08:00
question: "",
answer: "",
similarQuestionsText: "",
remark: "",
};
type DirectoryOption = { value: string; label: string };
function flattenDirectoryOptions(items: KnowledgeDirectory[], depth = 0): DirectoryOption[] {
return items.flatMap((item) => [
{ value: String(item.id), label: `${depth > 0 ? " " : ""}${item.name}` },
...flattenDirectoryOptions(item.children || [], depth + 1),
]);
}
function buildForm(item: KnowledgeFAQ | null, initialDirectoryId = 0): EditForm {
2026-04-09 10:01:23 +08:00
if (!item) {
return { ...emptyForm, directoryId: String(initialDirectoryId) };
2026-04-09 10:01:23 +08:00
}
return {
directoryId: String(item.directoryId || 0),
2026-04-09 10:01:23 +08:00
question: item.question,
answer: item.answer,
similarQuestionsText: (item.similarQuestions ?? []).join("\n"),
remark: item.remark ?? "",
};
}
function buildPayload(form: EditForm, knowledgeBaseId: number): CreateKnowledgeFAQPayload {
return {
knowledgeBaseId,
directoryId: Number(form.directoryId),
2026-04-09 10:01:23 +08:00
question: form.question.trim(),
answer: form.answer.trim(),
similarQuestions: form.similarQuestionsText
.split("\n")
.map((item) => item.trim())
.filter(Boolean),
remark: form.remark.trim(),
};
}
export function FAQEditDialog({
open,
saving,
itemId,
knowledgeBaseId,
initialDirectoryId = 0,
2026-04-09 10:01:23 +08:00
onOpenChange,
onSubmit,
}: FAQEditDialogProps) {
if (!open || !knowledgeBaseId) {
return null;
}
return (
<FAQEditDialogBody
key={itemId ? `edit-${itemId}` : "create"}
open={open}
saving={saving}
itemId={itemId}
knowledgeBaseId={knowledgeBaseId}
initialDirectoryId={initialDirectoryId}
2026-04-09 10:01:23 +08:00
onOpenChange={onOpenChange}
onSubmit={onSubmit}
/>
);
}
type FAQEditDialogBodyProps = {
open: boolean;
saving: boolean;
itemId: number | null;
knowledgeBaseId: number;
initialDirectoryId: number;
2026-04-09 10:01:23 +08:00
onOpenChange: (open: boolean) => void;
onSubmit: (payload: CreateKnowledgeFAQPayload) => Promise<void>;
};
function FAQEditDialogBody({
open,
saving,
itemId,
knowledgeBaseId,
initialDirectoryId,
2026-04-09 10:01:23 +08:00
onOpenChange,
onSubmit,
}: FAQEditDialogBodyProps) {
2026-05-25 12:06:15 +08:00
const t = useI18n();
2026-04-09 10:01:23 +08:00
const [loading, setLoading] = useState(false);
const [directories, setDirectories] = useState<KnowledgeDirectory[]>([]);
2026-04-09 10:01:23 +08:00
const formId = "knowledge-faq-edit-form";
2026-05-25 12:06:15 +08:00
const formSchema = useMemo(() => createFormSchema(t), [t]);
const resolver = useMemo(
() => zodResolver(formSchema) as Resolver<EditForm>,
[formSchema],
);
const form = useForm<EditForm>({
2026-04-09 10:01:23 +08:00
resolver,
defaultValues: emptyForm,
});
const {
handleSubmit,
reset,
register,
formState: { errors },
} = form;
const directoryOptions = useMemo(
() => [
{ value: "0", label: t("knowledge.rootContent") },
...flattenDirectoryOptions(directories),
],
[directories, t],
);
2026-04-09 10:01:23 +08:00
useEffect(() => {
async function loadDetail() {
if (!itemId) {
reset(buildForm(null, initialDirectoryId));
2026-04-09 10:01:23 +08:00
return;
}
setLoading(true);
try {
const data = await fetchKnowledgeFAQ(itemId);
reset(buildForm(data));
} finally {
setLoading(false);
}
}
if (open) {
void loadDetail();
}
}, [itemId, initialDirectoryId, open, reset]);
useEffect(() => {
if (!open) {
return;
}
let cancelled = false;
async function loadDirectories() {
try {
const data = await fetchKnowledgeDirectories(knowledgeBaseId);
if (!cancelled) {
setDirectories(data);
}
} catch (error) {
console.error("Failed to load knowledge directories:", error);
}
}
void loadDirectories();
return () => {
cancelled = true;
};
}, [knowledgeBaseId, open]);
2026-04-09 10:01:23 +08:00
async function onFormSubmit(values: EditForm) {
await onSubmit(buildPayload(values, knowledgeBaseId));
}
return (
<ProjectDialog
open={open}
onOpenChange={onOpenChange}
2026-05-25 12:06:15 +08:00
title={itemId ? t("knowledge.editFAQTitle") : t("knowledge.createFAQTitle")}
2026-04-09 10:01:23 +08:00
allowFullscreen
size="xl"
footer={
<>
<Button type="button" variant="outline" onClick={() => onOpenChange(false)} disabled={saving}>
2026-05-25 12:06:15 +08:00
{t("knowledge.cancel")}
2026-04-09 10:01:23 +08:00
</Button>
<Button type="submit" form={formId} disabled={saving || loading}>
2026-05-25 12:06:15 +08:00
{saving ? t("knowledge.saving") : itemId ? t("knowledge.save") : t("knowledge.create")}
2026-04-09 10:01:23 +08:00
</Button>
</>
}
>
{loading ? (
2026-05-25 12:06:15 +08:00
<div className="flex items-center justify-center py-12 text-muted-foreground">{t("knowledge.loading")}</div>
2026-04-09 10:01:23 +08:00
) : (
<form id={formId} onSubmit={handleSubmit(onFormSubmit)} className="space-y-4">
<Field data-invalid={!!errors.directoryId}>
<FieldLabel>{t("knowledge.directory")}</FieldLabel>
<FieldContent>
<OptionCombobox
value={form.watch("directoryId")}
onChange={(value) => form.setValue("directoryId", value ?? "0", { shouldDirty: true })}
options={directoryOptions}
placeholder={t("knowledge.selectDirectory")}
searchPlaceholder={t("knowledge.searchDirectory")}
emptyText={t("knowledge.emptyDirectory")}
/>
<FieldError errors={[errors.directoryId]} />
</FieldContent>
</Field>
2026-04-09 10:01:23 +08:00
<Field data-invalid={!!errors.question}>
2026-05-25 12:06:15 +08:00
<FieldLabel htmlFor="faq-question">{t("knowledge.standardQuestion")}</FieldLabel>
2026-04-09 10:01:23 +08:00
<FieldContent>
2026-05-25 12:06:15 +08:00
<Input id="faq-question" placeholder={t("knowledge.questionPlaceholder")} {...register("question")} />
2026-04-09 10:01:23 +08:00
<FieldError errors={[errors.question]} />
</FieldContent>
</Field>
<Field data-invalid={!!errors.answer}>
2026-05-25 12:06:15 +08:00
<FieldLabel htmlFor="faq-answer">{t("knowledge.answer")}</FieldLabel>
2026-04-09 10:01:23 +08:00
<FieldContent>
2026-05-25 12:06:15 +08:00
<Textarea id="faq-answer" rows={8} placeholder={t("knowledge.answerPlaceholder")} {...register("answer")} />
2026-04-09 10:01:23 +08:00
<FieldError errors={[errors.answer]} />
</FieldContent>
</Field>
<Field>
2026-05-25 12:06:15 +08:00
<FieldLabel htmlFor="faq-similar-questions">{t("knowledge.similarQuestions")}</FieldLabel>
2026-04-09 10:01:23 +08:00
<FieldContent>
<Textarea
id="faq-similar-questions"
rows={5}
2026-05-25 12:06:15 +08:00
placeholder={t("knowledge.similarQuestionsPlaceholder")}
2026-04-09 10:01:23 +08:00
{...register("similarQuestionsText")}
/>
</FieldContent>
</Field>
<Field data-invalid={!!errors.remark}>
2026-05-25 12:06:15 +08:00
<FieldLabel htmlFor="faq-remark">{t("knowledge.remark")}</FieldLabel>
2026-04-09 10:01:23 +08:00
<FieldContent>
2026-05-25 12:06:15 +08:00
<Textarea id="faq-remark" rows={3} placeholder={t("knowledge.remarkPlaceholder")} {...register("remark")} />
2026-04-09 10:01:23 +08:00
<FieldError errors={[errors.remark]} />
</FieldContent>
</Field>
</form>
)}
</ProjectDialog>
);
}