Files
deerflow-code/web-code/components/MarkdownRenderer.tsx
T
2026-05-27 19:43:15 +08:00

127 lines
6.1 KiB
TypeScript

import React, { useState } from 'react';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import rehypeRaw from 'rehype-raw';
interface MarkdownRendererProps {
content: string;
className?: string;
onCitationClick?: (index: number) => void;
}
const extractTextFromNode = (node: React.ReactNode): string => {
if (typeof node === 'string' || typeof node === 'number') return String(node);
if (Array.isArray(node)) return node.map(extractTextFromNode).join('');
if (React.isValidElement<{ children?: React.ReactNode }>(node)) return extractTextFromNode(node.props.children);
return '';
};
const CodeBlock: React.FC<{ children: React.ReactNode; className?: string }> = ({ children, className = '' }) => {
const [copied, setCopied] = useState(false);
const codeText = extractTextFromNode(children).replace(/\n$/, '');
const language = className?.match(/language-(\w+)/)?.[1];
const handleCopy = async () => {
try {
await navigator.clipboard.writeText(codeText);
setCopied(true);
window.setTimeout(() => setCopied(false), 1500);
} catch (error) {
console.error('复制代码失败:', error);
}
};
return (
<div className="group relative my-4 overflow-hidden rounded-lg border border-gray-200 bg-gray-50 shadow-sm dark:border-slate-700 dark:bg-slate-900">
<div className="flex items-center justify-between border-b border-gray-200 bg-gray-100 px-3 py-2 text-xs text-gray-500 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-400">
<span>{language || 'code'}</span>
<button
type="button"
onClick={handleCopy}
className="rounded border border-gray-300 bg-white px-2 py-1 text-xs text-gray-600 transition-colors hover:bg-gray-50 dark:border-slate-600 dark:bg-slate-700 dark:text-slate-200 dark:hover:bg-slate-600"
>
{copied ? '已复制' : '复制'}
</button>
</div>
<pre className="m-0 overflow-x-auto p-4 text-sm leading-relaxed text-gray-800 dark:text-slate-100">
<code className={className}>{children}</code>
</pre>
</div>
);
};
const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({ content, className = '', onCitationClick }) => {
const contentWithCitationBadge = content.replace(
/\[\[(\d+)\]\]/g,
'<button type="button" data-citation-index="$1" class="inline-flex items-center justify-center min-w-[1.5rem] h-6 px-2 mx-0.5 rounded-md border text-xs font-semibold align-middle bg-red-50 text-red-600 border-red-200 dark:bg-slate-700 dark:text-slate-100 dark:border-slate-500 cursor-pointer transition-all hover:opacity-85">[$1]</button>',
);
return (
<div
className={`markdown-content ${className}`}
onClick={(event) => {
if (!onCitationClick) return;
const target = event.target as HTMLElement;
const button = target.closest('[data-citation-index]') as HTMLElement | null;
if (!button) return;
const index = Number(button.dataset.citationIndex || '0');
if (Number.isNaN(index) || index <= 0) return;
onCitationClick(index);
}}
>
<ReactMarkdown
remarkPlugins={[remarkGfm]}
rehypePlugins={[rehypeRaw]}
components={{
h1: ({ node, ...props }) => <h1 className="text-2xl font-bold mb-4" {...props} />,
h2: ({ node, ...props }) => <h2 className="text-xl font-bold mb-3" {...props} />,
h3: ({ node, ...props }) => <h3 className="text-lg font-bold mb-2" {...props} />,
h4: ({ node, ...props }) => <h4 className="text-base font-bold mb-2" {...props} />,
h5: ({ node, ...props }) => <h5 className="text-sm font-bold mb-2" {...props} />,
h6: ({ node, ...props }) => <h6 className="text-xs font-bold mb-2" {...props} />,
p: ({ node, ...props }) => <p className="mb-4 leading-relaxed" {...props} />,
ul: ({ node, ...props }) => <ul className="list-disc list-inside mb-4 ml-4" {...props} />,
ol: ({ node, ...props }) => <ol className="list-decimal list-inside mb-4 ml-4" {...props} />,
li: ({ node, ...props }) => <li className="mb-1" {...props} />,
// code: ({ node, inline, className, children, ...props }: any) => {
// if (inline) {
// return (
// <code
// className={`rounded bg-gray-100 px-1 py-0.5 text-sm text-gray-800 dark:bg-slate-800 dark:text-slate-100 ${className || ''}`}
// {...props}
// >
// {children}
// </code>
// );
// }
// return <CodeBlock className={className}>{children}</CodeBlock>;
// },
// pre: ({ node, children }) => <>{children}</>,
blockquote: ({ node, ...props }) => (
<blockquote className="border-l-4 border-gray-300 dark:border-gray-600 pl-4 italic my-4" {...props} />
),
a: ({ node, ...props }) => (
<a className="text-blue-600 dark:text-blue-400 hover:underline" {...props} />
),
table: ({ node, ...props }) => (
<div className="overflow-x-auto mb-4">
<table className="min-w-full border-collapse" {...props} />
</div>
),
thead: ({ node, ...props }) => <thead className="bg-gray-100 dark:bg-gray-800" {...props} />,
tbody: ({ node, ...props }) => <tbody className="divide-y divide-gray-200 dark:divide-gray-700" {...props} />,
tr: ({ node, ...props }) => <tr className="hover:bg-gray-50 dark:hover:bg-gray-700" {...props} />,
th: ({ node, ...props }) => <th className="px-4 py-2 text-left font-semibold" {...props} />,
td: ({ node, ...props }) => <td className="px-4 py-2" {...props} />,
hr: ({ node, ...props }) => <hr className="my-4 border-gray-300 dark:border-gray-600" {...props} />,
strong: ({ node, ...props }) => <strong className="font-bold" {...props} />,
em: ({ node, ...props }) => <em className="italic" {...props} />,
}}
>
{contentWithCitationBadge}
</ReactMarkdown>
</div>
);
};
export default MarkdownRenderer;