Initial commit: GovAI 政务AI平台

This commit is contained in:
freedakgmail
2026-06-15 23:48:37 +08:00
commit 0f490f72a9
245 changed files with 51669 additions and 0 deletions
+201
View File
@@ -0,0 +1,201 @@
"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";
const mdComponents: Components = {
h1: ({ children }) => (
<h1 className="text-lg font-bold text-primary border-b-2 border-primary/20 pb-2 mb-3 mt-4 first:mt-0">
{children}
</h1>
),
h2: ({ children }) => (
<h2 className="text-base font-bold text-primary/90 border-l-3 border-primary pl-3 mb-2 mt-4">
{children}
</h2>
),
h3: ({ children }) => (
<h3 className="text-sm font-bold text-foreground mb-1.5 mt-3 flex items-center gap-1.5">
<span className="inline-block w-1.5 h-1.5 rounded-full bg-primary shrink-0" />
{children}
</h3>
),
p: ({ children }) => (
<p className="text-sm leading-7 text-foreground/90 my-2">{children}</p>
),
ul: ({ children }) => (
<ul className="my-2 ml-1 space-y-1">{children}</ul>
),
ol: ({ children }) => (
<ol className="my-2 ml-1 space-y-1 list-decimal list-inside">{children}</ol>
),
li: ({ children }) => (
<li className="text-sm leading-6 text-foreground/90 flex items-start gap-1.5">
<span className="inline-block w-1 h-1 rounded-full bg-primary/60 mt-2.5 shrink-0" />
<span className="flex-1">{children}</span>
</li>
),
blockquote: ({ children }) => (
<blockquote className="my-3 border-l-3 border-amber-400 bg-amber-50/60 dark:bg-amber-950/20 px-4 py-2 rounded-r-lg text-sm">
{children}
</blockquote>
),
table: ({ children }) => (
<div className="my-3 overflow-x-auto rounded-lg border">
<table className="w-full text-sm">{children}</table>
</div>
),
thead: ({ children }) => (
<thead className="bg-primary/5 border-b">{children}</thead>
),
th: ({ children }) => (
<th className="px-3 py-2 text-left text-xs font-semibold text-primary/80 uppercase tracking-wider">
{children}
</th>
),
td: ({ children }) => (
<td className="px-3 py-2 text-sm border-b border-muted">{children}</td>
),
hr: () => (
<hr className="my-4 border-t-2 border-dashed border-primary/10" />
),
strong: ({ children }) => (
<strong className="font-semibold text-foreground">{children}</strong>
),
em: ({ children }) => (
<em className="text-primary/80 not-italic font-medium">{children}</em>
),
a: ({ href, children }) => {
if (href === "#cite-kb") {
const label = String(children).replace(/^知识库:/, "");
return (
<span className="inline-flex items-center gap-1 px-2 py-0.5 mx-0.5 text-xs font-medium rounded-md bg-blue-50 text-blue-700 border border-blue-200 align-middle">
<BookOpen className="h-3 w-3 shrink-0" />
<span>{label}</span>
</span>
);
}
if (href === "#cite-ai") {
return (
<span className="inline-flex items-center gap-1 px-2 py-0.5 mx-0.5 text-xs font-medium rounded-md bg-amber-50 text-amber-700 border border-amber-200 align-middle whitespace-nowrap">
<BrainCircuit className="h-3 w-3 shrink-0" />
<span>AI建议</span>
</span>
);
}
// 推荐应用:渲染为可点击的应用跳转卡片
if (href?.startsWith("#cite-app:")) {
const slug = href.replace("#cite-app:", "");
const appName = String(children);
return (
<AppLinkBadge slug={slug} name={appName} />
);
}
return (
<a href={href} target="_blank" rel="noopener noreferrer" className="text-primary underline underline-offset-2 hover:text-primary/80">
{children}
</a>
);
},
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 (
<ReactMarkdown remarkPlugins={[remarkGfm]} components={mdComponents}>
{text}
</ReactMarkdown>
);
}
if (isBlock) {
return (
<div className="my-3 rounded-lg overflow-hidden border bg-slate-50 dark:bg-slate-900">
<div className="px-3 py-1.5 bg-slate-100 dark:bg-slate-800 text-xs text-muted-foreground border-b">
{lang || "代码"}
</div>
<pre className="p-3 overflow-x-auto text-xs leading-5">
<code>{children}</code>
</pre>
</div>
);
}
return (
<code className="text-xs font-mono bg-primary/5 text-primary px-1.5 py-0.5 rounded border border-primary/10">
{children}
</code>
);
},
pre: ({ children }) => <>{children}</>,
};
/**
* 推荐应用跳转徽章组件:点击后跳转到同机构内的其他应用
*/
function AppLinkBadge({ slug, name }: { slug: string; name: string }) {
const router = useRouter();
return (
<button
type="button"
onClick={() => router.push(`/chat/${slug}`)}
className="inline-flex items-center gap-1 px-2.5 py-1 mx-0.5 text-xs font-medium rounded-lg bg-emerald-50 text-emerald-700 border border-emerald-200 align-middle cursor-pointer hover:bg-emerald-100 hover:border-emerald-300 transition-colors whitespace-nowrap"
>
<ArrowRight className="h-3 w-3 shrink-0" />
<span>{name}</span>
</button>
);
}
interface GovMarkdownProps {
content: string;
className?: string;
}
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 组件拦截渲染
*/
function preprocessCitations(content: string): string {
return content
// 知识库引用:[[知识库:文献名称]] 或 [[知识库:文献名称:条款]]
.replace(/\[\[(知识库:[^\]]+)\]\]/g, (_, label) => `[${label}](#cite-kb)`)
// AI建议:标准格式 [[AI建议]]
.replace(/\[\[AI建议\]\]/g, "[AI建议](#cite-ai)")
// AI建议:来源说明块中的 **AI建议:** 或 **AI建议** 标题(仅匹配行首或 > 后)
.replace(/^(\s*>?\s*)\*\*AI建议[:]\*\*/gm, "$1[AI建议](#cite-ai)")
// AI建议:无加粗的 AI建议: 标题行(仅匹配行首或 > 后)
.replace(/^(\s*>?\s*)AI建议[:]\s*$/gm, "$1[AI建议](#cite-ai)")
// 推荐应用:[[推荐应用:应用名称:slug]] → 可点击跳转链接
.replace(/\[\[推荐应用:([^:]+):([^\]]+)\]\]/g, (_, name, slug) => `[${name}](#cite-app:${slug})`);
}
const GovMarkdown = memo(function GovMarkdown({ content, className }: GovMarkdownProps) {
if (!content) {
return (
<div className="flex items-center gap-2 text-sm text-muted-foreground py-1">
<span className="inline-block w-2 h-2 rounded-full bg-primary/60 animate-pulse" />
...
</div>
);
}
const cleaned = preprocessCitations(stripOuterCodeFence(content));
return (
<div className={`gov-markdown ${className || ""}`}>
<ReactMarkdown remarkPlugins={[remarkGfm]} components={mdComponents}>
{cleaned}
</ReactMarkdown>
</div>
);
});
export default GovMarkdown;