From ffd2227e5edd73357a69fa6635cb8b830bf615dc Mon Sep 17 00:00:00 2001 From: mlogclub Date: Tue, 2 Jun 2026 19:55:26 +0800 Subject: [PATCH] feat: add detail view functionality for documents and FAQs with localization support --- .../knowledge/_components/document-list.tsx | 36 +++ .../knowledge/_components/faq-list.tsx | 28 +++ .../_components/knowledge-content-detail.tsx | 211 ++++++++++++++++++ web/components/safe-rich-html.tsx | 12 +- web/messages/en-US.json | 7 + web/messages/zh-CN.json | 7 + 6 files changed, 299 insertions(+), 2 deletions(-) create mode 100644 web/app/dashboard/knowledge/_components/knowledge-content-detail.tsx diff --git a/web/app/dashboard/knowledge/_components/document-list.tsx b/web/app/dashboard/knowledge/_components/document-list.tsx index c740a28..e2824d0 100644 --- a/web/app/dashboard/knowledge/_components/document-list.tsx +++ b/web/app/dashboard/knowledge/_components/document-list.tsx @@ -3,6 +3,7 @@ import { FileTextIcon, FolderInputIcon, + EyeIcon, MoreHorizontalIcon, PencilIcon, SearchIcon, @@ -61,6 +62,7 @@ import { } from "@/lib/generated/enums"; import { cn, formatDateTime } from "@/lib/utils"; import { DocumentEditDialog } from "./document-edit"; +import { KnowledgeContentDetailDialog } from "./knowledge-content-detail"; import { KnowledgeBulkMoveDialog } from "./knowledge-bulk-move-dialog"; import { KnowledgeDirectoryPanel } from "./knowledge-directory-panel"; @@ -154,6 +156,8 @@ export function DocumentList({ knowledgeBaseId, onActionStateChange }: DocumentL const [selectedIds, setSelectedIds] = useState([]); const [moveTargetIds, setMoveTargetIds] = useState([]); const [contextMenuDocumentId, setContextMenuDocumentId] = useState(null); + const [detailItemId, setDetailItemId] = useState(null); + const [detailOpen, setDetailOpen] = useState(false); const [selectedDirectoryId, setSelectedDirectoryId] = useState(null); const [actionLoadingMap, setActionLoadingMap] = useState>({}); const [dialogOpen, setDialogOpen] = useState(false); @@ -302,6 +306,11 @@ export function DocumentList({ knowledgeBaseId, onActionStateChange }: DocumentL setDialogOpen(true); } + function openDetailDialog(item: KnowledgeDocumentListItem) { + setDetailItemId(item.id); + setDetailOpen(true); + } + function handleDialogOpenChange(open: boolean) { if (saving) { return; @@ -544,6 +553,10 @@ export function DocumentList({ knowledgeBaseId, onActionStateChange }: DocumentL + openDetailDialog(item)}> + + {t("knowledge.view")} + openEditDialog(item)}> {t("knowledge.edit")} @@ -569,6 +582,10 @@ export function DocumentList({ knowledgeBaseId, onActionStateChange }: DocumentL + openDetailDialog(item)}> + + {t("knowledge.view")} + openEditDialog(item)}> {t("knowledge.edit")} @@ -638,6 +655,10 @@ export function DocumentList({ knowledgeBaseId, onActionStateChange }: DocumentL + openDetailDialog(item)}> + + {t("knowledge.view")} + openEditDialog(item)}> {t("knowledge.edit")} @@ -662,6 +683,10 @@ export function DocumentList({ knowledgeBaseId, onActionStateChange }: DocumentL + openDetailDialog(item)}> + + {t("knowledge.view")} + openEditDialog(item)}> {t("knowledge.edit")} @@ -757,6 +782,17 @@ export function DocumentList({ knowledgeBaseId, onActionStateChange }: DocumentL onOpenChange={handleDialogOpenChange} onSubmit={handleSubmit} /> + { + setDetailOpen(open); + if (!open) { + setDetailItemId(null); + } + }} + /> ([]); const [moveTargetIds, setMoveTargetIds] = useState([]); const [contextMenuFAQId, setContextMenuFAQId] = useState(null); + const [detailItemId, setDetailItemId] = useState(null); + const [detailOpen, setDetailOpen] = useState(false); const [importing, setImporting] = useState(false); const [importDialogOpen, setImportDialogOpen] = useState(false); const [selectedDirectoryId, setSelectedDirectoryId] = useState(null); @@ -263,6 +267,11 @@ export function FAQList({ setDialogOpen(true); } + function openDetailDialog(item: KnowledgeFAQ) { + setDetailItemId(item.id); + setDetailOpen(true); + } + function handleDialogOpenChange(open: boolean) { if (saving) { return; @@ -475,6 +484,10 @@ export function FAQList({ + openDetailDialog(item)}> + + {t("knowledge.view")} + openEditDialog(item)}> {t("knowledge.edit")} @@ -499,6 +512,10 @@ export function FAQList({ + openDetailDialog(item)}> + + {t("knowledge.view")} + openEditDialog(item)}> {t("knowledge.edit")} @@ -594,6 +611,17 @@ export function FAQList({ onOpenChange={handleDialogOpenChange} onSubmit={handleSubmit} /> + { + setDetailOpen(open); + if (!open) { + setDetailItemId(null); + } + }} + /> void; +}; + +type DetailFieldProps = { + label: string; + value: string; +}; + +function DetailField({ label, value }: DetailFieldProps) { + return ( +
+
{label}
+
{value || "-"}
+
+ ); +} + +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(null); + const [faqDetail, setFAQDetail] = useState(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 ( + onOpenChange(false)}> + {t("common.close")} + + } + > + {loading || !item ? ( +
+ {t("knowledge.loading")} +
+ ) : ( +
+
+ +
+
{t("knowledge.status")}
+ {item.statusName || item.status} +
+
+
{t("knowledge.indexStatus")}
+ {item.indexStatusName || item.indexStatus} +
+ + + + + +
+ + {item.indexError ? ( +
+ {item.indexError} +
+ ) : null} + + {type === "document" && documentDetail ? ( +
+
+
{t("knowledge.content")}
+ {documentDetail.contentType || "-"} +
+
+ +
+
+ ) : null} + + {type === "faq" && faqDetail ? ( +
+
+
{t("knowledge.standardQuestion")}
+
{faqDetail.question}
+
+
+
{t("knowledge.answer")}
+
+                  {faqDetail.answer || "-"}
+                
+
+ {faqDetail.similarQuestions.length > 0 ? ( +
+
{t("knowledge.similarQuestions")}
+
+ {faqDetail.similarQuestions.map((question) => ( + + {question} + + ))} +
+
+ ) : null} + {faqDetail.remark ? ( +
+
{t("knowledge.remark")}
+
{faqDetail.remark}
+
+ ) : null} +
+ ) : null} +
+ )} +
+ ); +} diff --git a/web/components/safe-rich-html.tsx b/web/components/safe-rich-html.tsx index 569f10e..c04acd9 100644 --- a/web/components/safe-rich-html.tsx +++ b/web/components/safe-rich-html.tsx @@ -26,17 +26,24 @@ const allowedTags = new Set([ "h6", "hr", "i", + "img", "li", "ol", "p", "pre", "span", "strong", + "table", + "tbody", + "td", + "th", + "thead", + "tr", "u", "ul", ]) -const allowedAttrs = new Set(["class", "href", "rel", "target", "title"]) +const allowedAttrs = new Set(["alt", "class", "height", "href", "rel", "src", "target", "title", "width"]) function escapeHTML(value: string) { return value @@ -101,7 +108,7 @@ function sanitizeRichHTML(value: string) { element.removeAttribute(attr.name) continue } - if (name === "href" && !isSafeURL(attrValue)) { + if ((name === "href" || name === "src") && !isSafeURL(attrValue)) { element.removeAttribute(attr.name) } } @@ -140,6 +147,7 @@ export function SafeRichHTML({ html, fallback = "-", className }: SafeRichHTMLPr