Files
ai-agent/web/app/dashboard/knowledge/_components/knowledge-content-detail.tsx
T

214 lines
7.1 KiB
TypeScript
Raw Normal View History

"use client";
import { useEffect, useState } from "react";
import { ProjectDialog } from "@/components/project-dialog";
import { SafeRichHTML } from "@/components/safe-rich-html";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
fetchKnowledgeDocument,
fetchKnowledgeFAQ,
type KnowledgeDocument,
type KnowledgeFAQ,
} from "@/lib/api/admin";
import { markdownToHtml } from "@/components/content-editor/convert";
import { KnowledgeDocumentContentType } from "@/lib/generated/enums";
import { useI18n } from "@/i18n/provider";
import { formatDateTime } from "@/lib/utils";
type KnowledgeContentDetailDialogProps = {
open: boolean;
type: "document" | "faq";
itemId: number | null;
onOpenChange: (open: boolean) => void;
};
type DetailFieldProps = {
label: string;
value: string;
};
function DetailField({ label, value }: DetailFieldProps) {
return (
<div className="min-w-0 space-y-1">
<div className="text-xs text-muted-foreground">{label}</div>
<div className="truncate text-sm">{value || "-"}</div>
</div>
);
}
function getDirectoryLabel(item: KnowledgeDocument | KnowledgeFAQ, rootLabel: string) {
return item.directoryPath || item.directoryName || (item.directoryId === 0 ? rootLabel : "-");
}
function renderDocumentContent(item: KnowledgeDocument) {
if (item.contentType === KnowledgeDocumentContentType.Markdown) {
return markdownToHtml(item.content || "");
}
return item.content || "";
}
export function KnowledgeContentDetailDialog({
open,
type,
itemId,
onOpenChange,
}: KnowledgeContentDetailDialogProps) {
const t = useI18n();
const [loading, setLoading] = useState(false);
const [documentDetail, setDocumentDetail] = useState<KnowledgeDocument | null>(null);
const [faqDetail, setFAQDetail] = useState<KnowledgeFAQ | null>(null);
useEffect(() => {
if (!open || !itemId) {
return;
}
const id = itemId;
let cancelled = false;
async function loadDetail() {
setLoading(true);
try {
if (type === "document") {
const data = await fetchKnowledgeDocument(id);
if (!cancelled) {
setDocumentDetail(data);
setFAQDetail(null);
}
return;
}
const data = await fetchKnowledgeFAQ(id);
if (!cancelled) {
setFAQDetail(data);
setDocumentDetail(null);
}
} finally {
if (!cancelled) {
setLoading(false);
}
}
}
void loadDetail();
return () => {
cancelled = true;
};
}, [itemId, open, type]);
const item = type === "document" ? documentDetail : faqDetail;
const title =
type === "document"
? documentDetail?.title || t("knowledge.viewDocumentTitle")
: faqDetail?.question || t("knowledge.viewFAQTitle");
return (
<ProjectDialog
open={open}
onOpenChange={onOpenChange}
title={title}
size="xl"
allowFullscreen
closeOnEsc
defaultFullscreen
footer={
<Button type="button" variant="outline" onClick={() => onOpenChange(false)}>
{t("common.close")}
</Button>
}
>
{loading || !item ? (
<div className="flex items-center justify-center py-12 text-sm text-muted-foreground">
{t("knowledge.loading")}
</div>
) : (
<div className="space-y-5">
<div className="grid grid-cols-2 gap-4 md:grid-cols-4">
<DetailField
label={t("knowledge.directory")}
value={getDirectoryLabel(item, t("knowledge.rootContent"))}
/>
<div className="space-y-1">
<div className="text-xs text-muted-foreground">{t("knowledge.status")}</div>
<Badge variant="outline">{item.statusName || item.status}</Badge>
</div>
<div className="space-y-1">
<div className="text-xs text-muted-foreground">{t("knowledge.indexStatus")}</div>
<Badge variant="secondary">{item.indexStatusName || item.indexStatus}</Badge>
</div>
<DetailField
label={t("knowledge.createdAt")}
value={formatDateTime(item.createdAt)}
/>
<DetailField
label={t("knowledge.updatedAt")}
value={formatDateTime(item.updatedAt)}
/>
<DetailField
label={t("knowledge.createUser")}
value={item.createUserName || "-"}
/>
<DetailField
label={t("knowledge.updateUser")}
value={item.updateUserName || "-"}
/>
<DetailField
label={t("knowledge.indexedAtLabel")}
value={item.indexedAt ? formatDateTime(item.indexedAt) : "-"}
/>
</div>
{item.indexError ? (
<div className="rounded-md border border-destructive/30 bg-destructive/5 p-3 text-sm text-destructive">
{item.indexError}
</div>
) : null}
{type === "document" && documentDetail ? (
<div className="space-y-2">
<div className="flex items-center gap-2">
<div className="text-sm font-medium">{t("knowledge.content")}</div>
<Badge variant="outline">{documentDetail.contentType || "-"}</Badge>
</div>
<div className="rounded-md border bg-background p-4">
<SafeRichHTML html={renderDocumentContent(documentDetail)} />
</div>
</div>
) : null}
{type === "faq" && faqDetail ? (
<div className="space-y-5">
<div className="space-y-2">
<div className="text-sm font-medium">{t("knowledge.standardQuestion")}</div>
<div className="rounded-md border bg-muted/20 p-3 text-sm">{faqDetail.question}</div>
</div>
<div className="space-y-2">
<div className="text-sm font-medium">{t("knowledge.answer")}</div>
<pre className="whitespace-pre-wrap rounded-md border bg-muted/20 p-4 text-sm leading-6">
{faqDetail.answer || "-"}
</pre>
</div>
{faqDetail.similarQuestions.length > 0 ? (
<div className="space-y-2">
<div className="text-sm font-medium">{t("knowledge.similarQuestions")}</div>
<div className="flex flex-wrap gap-2">
{faqDetail.similarQuestions.map((question) => (
<Badge key={question} variant="outline">
{question}
</Badge>
))}
</div>
</div>
) : null}
{faqDetail.remark ? (
<div className="space-y-2">
<div className="text-sm font-medium">{t("knowledge.remark")}</div>
<div className="rounded-md border bg-muted/20 p-3 text-sm">{faqDetail.remark}</div>
</div>
) : null}
</div>
) : null}
</div>
)}
</ProjectDialog>
);
}