"use client"; import { memo } from "react"; import ReactMarkdown from "react-markdown"; import remarkGfm from "remark-gfm"; import type { Components } from "react-markdown"; import { BookOpen, BrainCircuit, ArrowRight } from "lucide-react"; import { useRouter } from "next/navigation"; import type { Chunk } from "@/lib/types"; const mdComponents: Components = { h1: ({ children }) => (

{children}

), h2: ({ children }) => (

{children}

), h3: ({ children }) => (

{children}

), p: ({ children }) =>

{children}

, ul: ({ children }) => , ol: ({ children }) => (
    {children}
), li: ({ children }) => (
  • {children}
  • ), blockquote: ({ children }) => (
    {children}
    ), table: ({ children }) => (
    {children}
    ), thead: ({ children }) => {children}, th: ({ children }) => ( {children} ), td: ({ children }) => {children}, hr: () =>
    , strong: ({ children }) => {children}, em: ({ children }) => {children}, a: ({ href, children }) => { if (href === "#cite-kb") { const label = String(children).replace(/^知识库:/, ""); return ( {label} ); } if (href === "#cite-ai") { return ( AI建议 ); } // 推荐应用:渲染为可点击的应用跳转卡片 if (href?.startsWith("#cite-app:")) { const slug = href.replace("#cite-app:", ""); const appName = String(children); return ; } return ( {children} ); }, code: ({ className, children }) => { const lang = className?.replace("language-", "") || ""; const isBlock = className?.includes("language-"); if (isBlock && (lang === "markdown" || lang === "md")) { const text = String(children).replace(/\n$/, ""); return ( {text} ); } if (isBlock) { return (
    {lang || "代码"}
                {children}
              
    ); } return ( {children} ); }, pre: ({ children }) => <>{children}, }; /** * 推荐应用跳转徽章组件:点击后跳转到同机构内的其他应用 */ function AppLinkBadge({ slug, name }: { slug: string; name: string }) { const router = useRouter(); return ( ); } interface GovMarkdownProps { content: string; className?: string; chunks?: Chunk[]; } function stripOuterCodeFence(text: string): string { const trimmed = text.trim(); const match = trimmed.match(/^```[\w]*\s*\n([\s\S]*?)```\s*$/); if (match) return match[1].trim(); return text; } /** * 预处理 markdown 内容:将来源标注转为特殊链接格式,由 ReactMarkdown 的 a 组件拦截渲染 * * 支持的标注格式: * - [[chunk:N]] → 知识库引用(使用 chunks 映射表解析为文档名) * - [[知识库:文档名]] → 知识库引用徽章 * - [[AI建议]] → AI建议徽章 * - [[推荐应用:名称:slug]] → 可点击跳转链接 */ function preprocessCitations(content: string, chunks?: Chunk[]): string { let result = content; console.log("[preprocessCitations] chunks:", chunks); // 先处理 chunk 编号引用 [[chunk:N]] result = result.replace(/\[\[chunk:(\d+)\]\]/g, (_, idx) => { const i = parseInt(idx, 10); if (chunks && chunks[i]) { // 使用 chunks 映射表,转换为带文档名的知识库引用 const docName = chunks[i].doc_name || chunks[i].content?.slice(0, 20) || "知识库片段"; console.log(`[preprocessCitations] chunk[${i}]:`, docName); return `[知识库:${docName}](#cite-kb)`; } // 无映射时显示后备文本 console.log(`[preprocessCitations] chunk[${i}] not found, chunks length:`, chunks?.length); return `[知识库片段](#cite-kb)`; }); // 知识库引用:[[知识库:文献名称]] 或 [[知识库:文献名称:条款]] result = result.replace(/\[\[(知识库:[^\]]+)\]\]/g, (_, label) => `[${label}](#cite-kb)`); // AI建议:标准格式 [[AI建议]] result = result.replace(/\[\[AI建议\]\]/g, "[AI建议](#cite-ai)"); // AI建议:来源说明块中的 **AI建议:** 或 **AI建议** 标题(仅匹配行首或 > 后) result = result.replace(/^(\s*\>?\s*)\*\*AI建议[::]\*\*/gm, "$1[AI建议](#cite-ai)"); // AI建议:无加粗的 AI建议: 标题行(仅匹配行首或 > 后) result = result.replace(/^(\s*\>?\s*)AI建议[::]\s*$/gm, "$1[AI建议](#cite-ai)"); // 推荐应用:[[推荐应用:应用名称:slug]] → 可点击跳转链接 result = result.replace( /\[\[推荐应用:([^:]+):([^\]]+)\]\]/g, (_, name, slug) => `[${name}](#cite-app:${slug})`, ); return result; } const GovMarkdown = memo(function GovMarkdown({ content, className, chunks }: GovMarkdownProps) { if (!content) { return (
    正在检索知识库并生成回复...
    ); } const cleaned = preprocessCitations(stripOuterCodeFence(content), chunks); return (
    {cleaned}
    ); }); export default GovMarkdown;