"use client"; import { DownloadIcon, FileUpIcon, InfoIcon } from "lucide-react"; import { useMemo, useRef, useState } from "react"; import { toast } from "sonner"; import { ProjectDialog } from "@/components/project-dialog"; import { Button } from "@/components/ui/button"; import { Field, FieldContent, FieldDescription, FieldGroup, FieldLabel, } from "@/components/ui/field"; import { Input } from "@/components/ui/input"; import { ScrollArea } from "@/components/ui/scroll-area"; import { createKnowledgeFAQ, type CreateKnowledgeFAQPayload } from "@/lib/api/admin"; import { useI18n } from "@/i18n/provider"; type FAQImportDialogProps = { open: boolean; knowledgeBaseId: number | null; importing: boolean; onOpenChange: (open: boolean) => void; onImportingChange: (importing: boolean) => void; onImported: () => Promise; }; type ParsedFAQRow = { rowNo: number; question: string; answer: string; similarQuestions: string[]; remark: string; }; type ParseResult = { rows: ParsedFAQRow[]; warnings: string[]; }; type TFunction = (key: string, values?: Record) => string; const acceptedHeaderMap: Record> = { question: "question", answer: "answer", similarquestions: "similarQuestions", "similarQuestions": "similarQuestions", remark: "remark", "\u6807\u51c6\u95ee\u9898": "question", "\u95ee\u9898": "question", "\u7b54\u6848": "answer", "\u76f8\u4f3c\u95ee": "similarQuestions", "\u76f8\u4f3c\u95ee\u9898": "similarQuestions", "\u5907\u6ce8": "remark", }; function normalizeHeader(value: string) { return value.trim().replace(/^\uFEFF/, "").toLowerCase(); } function parseDelimitedText(input: string): string[][] { const text = input.replace(/\r\n/g, "\n").replace(/\r/g, "\n"); const rows: string[][] = []; let row: string[] = []; let cell = ""; let inQuotes = false; let delimiter = ","; function pushCell() { row.push(cell.trim()); cell = ""; } function pushRow() { if (row.length === 1 && row[0] === "" && rows.length === 0) { row = []; return; } if (row.some((item) => item !== "")) { rows.push(row); } row = []; } for (let i = 0; i < text.length; i += 1) { const char = text[i]; const next = text[i + 1]; if (!inQuotes && rows.length === 0 && row.length === 0 && cell.length > 0 && char === "\t") { delimiter = "\t"; } if (char === '"') { if (inQuotes && next === '"') { cell += '"'; i += 1; continue; } inQuotes = !inQuotes; continue; } if (!inQuotes && char === delimiter) { pushCell(); continue; } if (!inQuotes && char === "\n") { pushCell(); pushRow(); continue; } cell += char; } if (cell.length > 0 || row.length > 0) { pushCell(); pushRow(); } return rows; } function parseSimilarQuestions(value: string) { return value .split(/\r?\n|\|/g) .map((item) => item.trim()) .filter(Boolean); } function parseFAQFileContent(input: string, t: TFunction): ParseResult { const table = parseDelimitedText(input); if (table.length === 0) { throw new Error(t("knowledge.fileEmpty")); } const headerRow = table[0]; const headerMap = new Map, number>(); for (let index = 0; index < headerRow.length; index += 1) { const header = acceptedHeaderMap[normalizeHeader(headerRow[index])]; if (header && !headerMap.has(header)) { headerMap.set(header, index); } } if (!headerMap.has("question") || !headerMap.has("answer")) { throw new Error(t("knowledge.missingFAQColumns")); } const rows: ParsedFAQRow[] = []; const warnings: string[] = []; for (let index = 1; index < table.length; index += 1) { const current = table[index]; const rowNo = index + 1; const question = current[headerMap.get("question") ?? -1]?.trim() ?? ""; const answer = current[headerMap.get("answer") ?? -1]?.trim() ?? ""; const similarQuestionsRaw = current[headerMap.get("similarQuestions") ?? -1]?.trim() ?? ""; const remark = current[headerMap.get("remark") ?? -1]?.trim() ?? ""; if (!question && !answer && !similarQuestionsRaw && !remark) { continue; } if (!question || !answer) { warnings.push(t("knowledge.skipRowMissingFAQ", { row: rowNo })); continue; } rows.push({ rowNo, question, answer, similarQuestions: parseSimilarQuestions(similarQuestionsRaw), remark, }); } return { rows, warnings }; } function downloadTemplate() { const templateContent = [ "question,answer,similarQuestions,remark", '"How do I reset my password?","Open profile settings and choose Reset Password.","forgot password|where is reset password","Account FAQ"', '"Which channels are supported?","Web chat and WeCom customer service channels are currently supported.","available channels|supported channels","Channel guide"', ].join("\n"); const blob = new Blob([templateContent], { type: "text/csv;charset=utf-8;" }); const url = URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; link.download = "knowledge-faq-import-template.csv"; link.click(); URL.revokeObjectURL(url); } function buildPayload(row: ParsedFAQRow, knowledgeBaseId: number): CreateKnowledgeFAQPayload { return { knowledgeBaseId, question: row.question, answer: row.answer, similarQuestions: row.similarQuestions, remark: row.remark, }; } export function FAQImportDialog({ open, knowledgeBaseId, importing, onOpenChange, onImportingChange, onImported, }: FAQImportDialogProps) { const t = useI18n(); const fileInputRef = useRef(null); const [fileName, setFileName] = useState(""); const [rows, setRows] = useState([]); const [warnings, setWarnings] = useState([]); const previewRows = useMemo(() => rows.slice(0, 5), [rows]); function resetState() { setFileName(""); setRows([]); setWarnings([]); if (fileInputRef.current) { fileInputRef.current.value = ""; } } async function handleFileChange(event: React.ChangeEvent) { const file = event.target.files?.[0]; if (!file) { return; } try { const content = await file.text(); const parsed = parseFAQFileContent(content, t); setFileName(file.name); setRows(parsed.rows); setWarnings(parsed.warnings); if (parsed.rows.length === 0) { toast.error(t("knowledge.importNoRows")); } else { toast.success(t("knowledge.parsedFAQRows", { count: parsed.rows.length })); } } catch (error) { resetState(); toast.error(error instanceof Error ? error.message : t("knowledge.parseImportFailed")); } } async function handleImport() { if (!knowledgeBaseId || rows.length === 0 || importing) { return; } onImportingChange(true); let successCount = 0; const failedRows: string[] = []; try { for (const row of rows) { try { await createKnowledgeFAQ(buildPayload(row, knowledgeBaseId)); successCount += 1; } catch (error) { failedRows.push( t("knowledge.importRowFailed", { row: row.rowNo, message: error instanceof Error ? error.message : t("knowledge.importFailed"), }) ); } } await onImported(); if (successCount > 0) { toast.success(t("knowledge.importSuccess", { count: successCount })); } if (failedRows.length > 0) { toast.error(t("knowledge.importSomeFailed", { count: failedRows.length })); setWarnings((current) => [...current, ...failedRows]); return; } resetState(); onOpenChange(false); } finally { onImportingChange(false); } } return ( { if (!nextOpen && !importing) { resetState(); } onOpenChange(nextOpen); }} title={t("knowledge.importFAQTitle")} description={t("knowledge.importFAQDescription")} size="lg" footer={ <> } > {t("knowledge.importFile")}
void handleFileChange(event)} />
{t("knowledge.importFileDescription")}
{fileName ? (
{t("knowledge.currentFile", { name: fileName })}
) : null} {warnings.length > 0 ? (
{t("knowledge.importHint")}
    {warnings.map((item, index) => (
  • {item}
  • ))}
) : null}
{t("knowledge.importPreview")}
{previewRows.length > 0 ? (
{previewRows.map((row) => (
{t("knowledge.rowNumber", { row: row.rowNo })}
{row.question}
{row.answer}
{t("knowledge.similarQuestionShort", { value: row.similarQuestions.length > 0 ? row.similarQuestions.join(" / ") : t("knowledge.none"), })}
{t("knowledge.remark")}:{row.remark || t("knowledge.none")}
))}
) : (
{t("knowledge.previewAfterUpload")}
)}
); }