diff --git a/web/package.json b/web/package.json
index 6b57b314d..e44d90896 100644
--- a/web/package.json
+++ b/web/package.json
@@ -36,6 +36,7 @@
"@radix-ui/react-tabs": "^1.1.4",
"@radix-ui/react-tooltip": "^1.2.0",
"@rc-component/mentions": "^1.2.0",
+ "@react-pdf/renderer": "^4.3.0",
"@t3-oss/env-nextjs": "^0.11.0",
"@tailwindcss/typography": "^0.5.16",
"@tiptap/extension-document": "^2.12.0",
@@ -51,10 +52,14 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
+ "docx": "^9.5.0",
+ "file-saver": "^2.0.5",
"framer-motion": "^12.6.5",
"hast": "^1.0.0",
"highlight.js": "^11.11.1",
+ "html2canvas": "^1.4.1",
"immer": "^10.1.1",
+ "jspdf": "^3.0.1",
"katex": "^0.16.21",
"lowlight": "^3.3.0",
"lru-cache": "^11.1.0",
@@ -86,6 +91,7 @@
"devDependencies": {
"@eslint/eslintrc": "^3.3.1",
"@tailwindcss/postcss": "^4.0.15",
+ "@types/file-saver": "^2.0.7",
"@types/hast": "^3.0.4",
"@types/node": "^20.14.10",
"@types/react": "^19.0.0",
diff --git a/web/src/app/chat/components/research-block.tsx b/web/src/app/chat/components/research-block.tsx
index 4897d6a9a..0deef32a2 100644
--- a/web/src/app/chat/components/research-block.tsx
+++ b/web/src/app/chat/components/research-block.tsx
@@ -1,13 +1,14 @@
// Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
// SPDX-License-Identifier: MIT
-import { Check, Copy, Headphones, Pencil, Undo2, X, Download } from "lucide-react";
+import { Check, Copy, Headphones, Pencil, Undo2, X } from "lucide-react";
import { useCallback, useEffect, useState } from "react";
import { ScrollContainer } from "~/components/deer-flow/scroll-container";
import { Tooltip } from "~/components/deer-flow/tooltip";
import { Button } from "~/components/ui/button";
import { Card } from "~/components/ui/card";
+import { DownloadDropdown } from "~/components/ui/download-dropdown";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/components/ui/tabs";
import { useReplay } from "~/core/replay";
import { closeResearch, listenToPodcast, useStore } from "~/core/store";
@@ -49,6 +50,11 @@ export function ResearchBlock({
const [editing, setEditing] = useState(false);
const [copied, setCopied] = useState(false);
+
+ // 获取报告内容用于下载
+ const reportContent = useStore((state) =>
+ reportId ? (state.messages.get(reportId)?.content ?? "") : ""
+ );
const handleCopy = useCallback(() => {
if (!reportId) {
return;
@@ -64,31 +70,7 @@ export function ResearchBlock({
}, 1000);
}, [reportId]);
- // Download report as markdown
- const handleDownload = useCallback(() => {
- if (!reportId) {
- return;
- }
- const report = useStore.getState().messages.get(reportId);
- if (!report) {
- return;
- }
- const now = new Date();
- const pad = (n: number) => n.toString().padStart(2, '0');
- const timestamp = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}_${pad(now.getHours())}-${pad(now.getMinutes())}-${pad(now.getSeconds())}`;
- const filename = `research-report-${timestamp}.md`;
- const blob = new Blob([report.content], { type: 'text/markdown' });
- const url = URL.createObjectURL(blob);
- const a = document.createElement('a');
- a.href = url;
- a.download = filename;
- document.body.appendChild(a);
- a.click();
- setTimeout(() => {
- document.body.removeChild(a);
- URL.revokeObjectURL(url);
- }, 0);
- }, [reportId]);
+
const handleEdit = useCallback(() => {
@@ -140,16 +122,10 @@ export function ResearchBlock({
{copied ? : }
-
-
-
+
>
)}
diff --git a/web/src/components/ui/download-dropdown.tsx b/web/src/components/ui/download-dropdown.tsx
new file mode 100644
index 000000000..b90d29a59
--- /dev/null
+++ b/web/src/components/ui/download-dropdown.tsx
@@ -0,0 +1,134 @@
+// Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+// SPDX-License-Identifier: MIT
+
+"use client";
+
+import { Download, FileText, FileImage, FileDown } from "lucide-react";
+import { useCallback, useState } from "react";
+
+import { Button } from "~/components/ui/button";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+} from "~/components/ui/dropdown-menu";
+import { Tooltip } from "~/components/deer-flow/tooltip";
+import {
+ downloadAsMarkdown,
+ downloadAsPDF,
+ downloadAsImage,
+ downloadAsWord,
+ createTempElementForScreenshot,
+ cleanupTempElement,
+} from "~/lib/download-utils";
+
+export interface DownloadDropdownProps {
+ content: string;
+ className?: string;
+ disabled?: boolean;
+}
+
+export function DownloadDropdown({
+ content,
+ className,
+ disabled = false
+}: DownloadDropdownProps) {
+ const [isDownloading, setIsDownloading] = useState(false);
+
+ const handleDownloadMarkdown = useCallback(() => {
+ downloadAsMarkdown(content);
+ }, [content]);
+
+ const handleDownloadPDF = useCallback(async () => {
+ if (isDownloading) return;
+ setIsDownloading(true);
+ try {
+ await downloadAsPDF(content);
+ } catch (error) {
+ console.error('PDF download failed:', error);
+ } finally {
+ setIsDownloading(false);
+ }
+ }, [content, isDownloading]);
+
+ const handleDownloadImage = useCallback(async () => {
+ if (isDownloading) return;
+ setIsDownloading(true);
+ try {
+ // 创建临时元素用于截图
+ const tempId = createTempElementForScreenshot(content);
+ // 等待DOM更新
+ await new Promise(resolve => setTimeout(resolve, 100));
+ await downloadAsImage(tempId);
+ cleanupTempElement(tempId);
+ } catch (error) {
+ console.error('Image download failed:', error);
+ } finally {
+ setIsDownloading(false);
+ }
+ }, [content, isDownloading]);
+
+ const handleDownloadWord = useCallback(async () => {
+ if (isDownloading) return;
+ setIsDownloading(true);
+ try {
+ await downloadAsWord(content);
+ } catch (error) {
+ console.error('Word download failed:', error);
+ } finally {
+ setIsDownloading(false);
+ }
+ }, [content, isDownloading]);
+
+ return (
+
+
+
+
+
+
+
+
+
+ Download as Markdown
+
+
+
+ Download as PDF
+
+
+
+ Download as Image
+
+
+
+ Download as Word
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/web/src/lib/download-utils.ts b/web/src/lib/download-utils.ts
new file mode 100644
index 000000000..c420a9565
--- /dev/null
+++ b/web/src/lib/download-utils.ts
@@ -0,0 +1,637 @@
+// Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+// SPDX-License-Identifier: MIT
+
+import { Document, Packer, Paragraph, TextRun } from "docx";
+import { saveAs } from "file-saver";
+import html2canvas from "html2canvas";
+import jsPDF from "jspdf";
+import React from "react";
+import { renderToStaticMarkup } from "react-dom/server";
+import ReactMarkdown from "react-markdown";
+import rehypeKatex from "rehype-katex";
+import remarkGfm from "remark-gfm";
+import remarkMath from "remark-math";
+
+// 生成带时间戳的文件名
+export function generateFilename(baseName: string, extension: string): string {
+ const now = new Date();
+ const pad = (n: number) => n.toString().padStart(2, '0');
+ const timestamp = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}_${pad(now.getHours())}-${pad(now.getMinutes())}-${pad(now.getSeconds())}`;
+ return `${baseName}-${timestamp}.${extension}`;
+}
+
+// 清理HTML元素中的现代CSS函数,确保html2canvas兼容性
+function sanitizeElementForCanvas(element: HTMLElement): void {
+ // 移除所有子元素的style属性中可能包含现代CSS函数的内容
+ const allElements = element.querySelectorAll('*');
+ allElements.forEach((el) => {
+ if (el instanceof HTMLElement) {
+ // 清理可能包含oklch等现代颜色函数的样式
+ const style = el.style;
+ const propertiesToRemove: string[] = [];
+
+ for (const property of Array.from(style)) {
+ const value = style.getPropertyValue(property);
+ if (value && (value.includes('oklch') || value.includes('var(--'))) {
+ propertiesToRemove.push(property);
+ }
+ }
+
+ propertiesToRemove.forEach(property => {
+ style.removeProperty(property);
+ });
+ }
+ });
+}
+
+// 使用ReactMarkdown将Markdown渲染为HTML
+function renderMarkdownToHTML(markdown: string): string {
+ // 处理KaTeX和引用(参考项目中的处理方式)
+ const processedMarkdown = markdown
+ .replace(/\\\\\[/g, "$$$$") // Replace '\\[' with '$$'
+ .replace(/\\\\\]/g, "$$$$") // Replace '\\]' with '$$'
+ .replace(/\\\\\(/g, "$$$$") // Replace '\\(' with '$$'
+ .replace(/\\\\\)/g, "$$$$") // Replace '\\)' with '$$'
+ .replace(/\\\[/g, "$$$$") // Replace '\[' with '$$'
+ .replace(/\\\]/g, "$$$$") // Replace '\]' with '$$'
+ .replace(/\\\(/g, "$$$$") // Replace '\(' with '$$'
+ .replace(/\\\)/g, "$$$$") // Replace '\)' with '$$'
+ .replace(/^```markdown\n/gm, "")
+ .replace(/^```text\n/gm, "")
+ .replace(/^```\n/gm, "")
+ .replace(/\n```$/gm, "");
+
+ // 使用ReactMarkdown渲染为HTML字符串
+ const reactElement = React.createElement(ReactMarkdown, {
+ remarkPlugins: [remarkGfm, remarkMath],
+ rehypePlugins: [rehypeKatex],
+ components: {
+ // 自定义组件,确保在服务端渲染时正常工作
+ img: ({ src, alt }) =>
+ React.createElement('img', {
+ src: src as string,
+ alt: alt ?? '',
+ style: { maxWidth: '100%', height: 'auto', borderRadius: '4px' }
+ }),
+ a: ({ href, children }) =>
+ React.createElement('a', {
+ href: href!,
+ target: '_blank',
+ rel: 'noopener noreferrer',
+ style: { color: '#0066cc', textDecoration: 'underline' }
+ }, children),
+ table: ({ children }) =>
+ React.createElement('table', {
+ style: {
+ width: '100%',
+ borderCollapse: 'collapse',
+ margin: '16px 0',
+ border: '1px solid #e5e7eb'
+ }
+ }, children),
+ th: ({ children }) =>
+ React.createElement('th', {
+ style: {
+ padding: '8px 12px',
+ backgroundColor: '#f9fafb',
+ border: '1px solid #e5e7eb',
+ fontWeight: 'bold',
+ textAlign: 'left'
+ }
+ }, children),
+ td: ({ children }) =>
+ React.createElement('td', {
+ style: {
+ padding: '8px 12px',
+ border: '1px solid #e5e7eb'
+ }
+ }, children),
+ code: ({ children, ...props }) => {
+ const inline = 'inline' in props ? props.inline : false;
+ if (inline) {
+ return React.createElement('code', {
+ style: {
+ backgroundColor: '#f3f4f6',
+ padding: '2px 4px',
+ borderRadius: '3px',
+ fontSize: '0.9em',
+ fontFamily: 'monospace'
+ }
+ }, children);
+ }
+ return React.createElement('pre', {
+ style: {
+ backgroundColor: '#f8f9fa',
+ padding: '16px',
+ borderRadius: '6px',
+ overflow: 'auto',
+ fontSize: '14px',
+ fontFamily: 'monospace',
+ margin: '16px 0'
+ }
+ }, React.createElement('code', {}, children));
+ },
+ blockquote: ({ children }) =>
+ React.createElement('blockquote', {
+ style: {
+ borderLeft: '4px solid #cccccc',
+ paddingLeft: '16px',
+ margin: '16px 0',
+ fontStyle: 'italic',
+ color: '#666666'
+ }
+ }, children)
+ }
+ }, processedMarkdown);
+
+ return renderToStaticMarkup(reactElement);
+}
+
+// 下载Markdown格式
+export function downloadAsMarkdown(content: string, filename?: string): void {
+ const finalFilename = filename ?? generateFilename('research-report', 'md');
+ const blob = new Blob([content], { type: 'text/markdown' });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = finalFilename;
+ document.body.appendChild(a);
+ a.click();
+ setTimeout(() => {
+ document.body.removeChild(a);
+ URL.revokeObjectURL(url);
+ }, 0);
+}
+
+// 下载PDF格式
+export async function downloadAsPDF(content: string, filename?: string): Promise {
+ const finalFilename = filename ?? generateFilename('research-report', 'pdf');
+
+ // 创建一个临时的div来渲染markdown内容
+ const tempDiv = document.createElement('div');
+ tempDiv.style.position = 'absolute';
+ tempDiv.style.left = '-9999px';
+ tempDiv.style.top = '-9999px';
+ tempDiv.style.width = '800px';
+ tempDiv.style.padding = '20px';
+ tempDiv.style.fontFamily = 'Arial, sans-serif';
+ tempDiv.style.fontSize = '14px';
+ tempDiv.style.lineHeight = '1.6';
+ tempDiv.style.color = '#000000';
+ tempDiv.style.backgroundColor = '#ffffff';
+ // 添加CSS变量覆盖,避免oklch颜色函数
+ tempDiv.style.setProperty('--color', '#000000');
+ tempDiv.style.setProperty('--background', '#ffffff');
+
+ // 使用ReactMarkdown正确渲染Markdown内容
+ const htmlContent = renderMarkdownToHTML(content);
+
+ tempDiv.innerHTML = `
+ ${htmlContent}
+
`;
+ document.body.appendChild(tempDiv);
+
+ // 清理现代CSS函数
+ sanitizeElementForCanvas(tempDiv);
+
+ try {
+ const canvas = await html2canvas(tempDiv, {
+ scale: 2,
+ useCORS: true,
+ allowTaint: false,
+ backgroundColor: '#ffffff',
+ ignoreElements: (element) => {
+ // 忽略可能包含不支持样式的元素
+ return element.tagName === 'SCRIPT' || element.tagName === 'STYLE';
+ },
+ onclone: (clonedDoc) => {
+ // 移除所有现有的样式表以避免oklch等现代CSS函数
+ const existingStyles = clonedDoc.querySelectorAll('style, link[rel="stylesheet"]');
+ existingStyles.forEach(style => style.remove());
+
+ // 添加KaTeX CSS
+ const katexLink = clonedDoc.createElement('link');
+ katexLink.rel = 'stylesheet';
+ katexLink.href = 'https://cdn.jsdelivr.net/npm/katex@0.16.21/dist/katex.min.css';
+ clonedDoc.head.appendChild(katexLink);
+
+ // 添加安全的样式
+ const styleElement = clonedDoc.createElement('style');
+ styleElement.textContent = `
+ * {
+ color: #000000 !important;
+ background-color: #ffffff !important;
+ border-color: #cccccc !important;
+ font-family: Arial, sans-serif !important;
+ }
+ h1 {
+ color: #1a1a1a !important;
+ font-size: 28px !important;
+ font-weight: bold !important;
+ margin: 24px 0 12px 0 !important;
+ line-height: 1.2 !important;
+ border-bottom: 2px solid #e5e5e5 !important;
+ padding-bottom: 8px !important;
+ }
+ h2 {
+ color: #2a2a2a !important;
+ font-size: 24px !important;
+ font-weight: bold !important;
+ margin: 20px 0 10px 0 !important;
+ line-height: 1.3 !important;
+ border-bottom: 1px solid #e5e5e5 !important;
+ padding-bottom: 4px !important;
+ }
+ h3 {
+ color: #3a3a3a !important;
+ font-size: 20px !important;
+ font-weight: bold !important;
+ margin: 18px 0 8px 0 !important;
+ line-height: 1.3 !important;
+ }
+ h4, h5, h6 {
+ color: #4a4a4a !important;
+ font-weight: bold !important;
+ margin: 16px 0 6px 0 !important;
+ }
+ p {
+ margin: 12px 0 !important;
+ line-height: 1.6 !important;
+ color: #333333 !important;
+ }
+ strong {
+ color: #000000 !important;
+ font-weight: bold !important;
+ }
+ em {
+ color: #555555 !important;
+ font-style: italic !important;
+ }
+ ul, ol {
+ margin: 12px 0 !important;
+ padding-left: 20px !important;
+ list-style-position: outside !important;
+ }
+ ul {
+ list-style-type: disc !important;
+ }
+ ol {
+ list-style-type: decimal !important;
+ }
+ li {
+ margin: 4px 0 !important;
+ line-height: 1.6 !important;
+ padding-left: 6px !important;
+ text-align: left !important;
+ }
+ table {
+ width: 100% !important;
+ border-collapse: collapse !important;
+ margin: 16px 0 !important;
+ border: 1px solid #e5e7eb !important;
+ }
+ th {
+ padding: 10px 12px !important;
+ background-color: #f9fafb !important;
+ border: 1px solid #e5e7eb !important;
+ font-weight: bold !important;
+ text-align: left !important;
+ }
+ td {
+ padding: 8px 12px !important;
+ border: 1px solid #e5e7eb !important;
+ }
+ code {
+ background-color: #f3f4f6 !important;
+ padding: 2px 6px !important;
+ border-radius: 4px !important;
+ font-size: 0.9em !important;
+ font-family: 'Courier New', monospace !important;
+ }
+ pre {
+ background-color: #f8f9fa !important;
+ padding: 16px !important;
+ border-radius: 8px !important;
+ overflow: auto !important;
+ margin: 16px 0 !important;
+ border: 1px solid #e9ecef !important;
+ }
+ pre code {
+ background-color: transparent !important;
+ padding: 0 !important;
+ font-size: 14px !important;
+ }
+ blockquote {
+ border-left: 4px solid #cccccc !important;
+ padding-left: 16px !important;
+ margin: 16px 0 !important;
+ font-style: italic !important;
+ color: #666666 !important;
+ background-color: #f9f9f9 !important;
+ padding: 12px 16px !important;
+ border-radius: 0 6px 6px 0 !important;
+ }
+ img {
+ max-width: 100% !important;
+ height: auto !important;
+ border-radius: 6px !important;
+ margin: 12px 0 !important;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1) !important;
+ }
+ a {
+ color: #0066cc !important;
+ text-decoration: underline !important;
+ }
+ a:hover {
+ color: #004499 !important;
+ }
+ /* KaTeX样式支持 */
+ .katex {
+ font-size: 1em !important;
+ }
+ .katex-display {
+ margin: 16px 0 !important;
+ text-align: center !important;
+ }
+ `;
+ clonedDoc.head.appendChild(styleElement);
+ }
+ });
+
+ const imgData = canvas.toDataURL('image/png');
+ const pdf = new jsPDF();
+ const imgWidth = 210;
+ const pageHeight = 295;
+ const imgHeight = (canvas.height * imgWidth) / canvas.width;
+ let heightLeft = imgHeight;
+ let position = 0;
+
+ pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight);
+ heightLeft -= pageHeight;
+
+ while (heightLeft >= 0) {
+ position = heightLeft - imgHeight;
+ pdf.addPage();
+ pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight);
+ heightLeft -= pageHeight;
+ }
+
+ pdf.save(finalFilename);
+ } finally {
+ document.body.removeChild(tempDiv);
+ }
+}
+
+// 下载为图片格式
+export async function downloadAsImage(elementId: string, filename?: string): Promise {
+ const finalFilename = filename ?? generateFilename('research-report', 'png');
+ const element = document.getElementById(elementId);
+
+ if (!element) {
+ throw new Error('找不到要截图的元素');
+ }
+
+ // 清理现代CSS函数
+ sanitizeElementForCanvas(element);
+
+ const canvas = await html2canvas(element, {
+ scale: 2,
+ useCORS: true,
+ allowTaint: false,
+ backgroundColor: '#ffffff',
+ ignoreElements: (element) => {
+ // 忽略可能包含不支持样式的元素
+ return element.tagName === 'SCRIPT' || element.tagName === 'STYLE';
+ },
+ onclone: (clonedDoc) => {
+ // 移除所有现有的样式表以避免oklch等现代CSS函数
+ const existingStyles = clonedDoc.querySelectorAll('style, link[rel="stylesheet"]');
+ existingStyles.forEach(style => style.remove());
+
+ // 添加KaTeX CSS
+ const katexLink = clonedDoc.createElement('link');
+ katexLink.rel = 'stylesheet';
+ katexLink.href = 'https://cdn.jsdelivr.net/npm/katex@0.16.21/dist/katex.min.css';
+ clonedDoc.head.appendChild(katexLink);
+
+ // 添加安全的样式
+ const styleElement = clonedDoc.createElement('style');
+ styleElement.textContent = `
+ * {
+ color: #000000 !important;
+ background-color: #ffffff !important;
+ border-color: #cccccc !important;
+ font-family: Arial, sans-serif !important;
+ }
+ h1 {
+ color: #1a1a1a !important;
+ font-size: 28px !important;
+ font-weight: bold !important;
+ margin: 24px 0 12px 0 !important;
+ line-height: 1.2 !important;
+ border-bottom: 2px solid #e5e5e5 !important;
+ padding-bottom: 8px !important;
+ }
+ h2 {
+ color: #2a2a2a !important;
+ font-size: 24px !important;
+ font-weight: bold !important;
+ margin: 20px 0 10px 0 !important;
+ line-height: 1.3 !important;
+ border-bottom: 1px solid #e5e5e5 !important;
+ padding-bottom: 4px !important;
+ }
+ h3 {
+ color: #3a3a3a !important;
+ font-size: 20px !important;
+ font-weight: bold !important;
+ margin: 18px 0 8px 0 !important;
+ line-height: 1.3 !important;
+ }
+ h4, h5, h6 {
+ color: #4a4a4a !important;
+ font-weight: bold !important;
+ margin: 16px 0 6px 0 !important;
+ }
+ p {
+ margin: 12px 0 !important;
+ line-height: 1.6 !important;
+ color: #333333 !important;
+ }
+ strong {
+ color: #000000 !important;
+ font-weight: bold !important;
+ }
+ em {
+ color: #555555 !important;
+ font-style: italic !important;
+ }
+ ul, ol {
+ margin: 12px 0 !important;
+ padding-left: 20px !important;
+ list-style-position: outside !important;
+ }
+ ul {
+ list-style-type: disc !important;
+ }
+ ol {
+ list-style-type: decimal !important;
+ }
+ li {
+ margin: 4px 0 !important;
+ line-height: 1.6 !important;
+ padding-left: 6px !important;
+ text-align: left !important;
+ }
+ table {
+ width: 100% !important;
+ border-collapse: collapse !important;
+ margin: 16px 0 !important;
+ border: 1px solid #e5e7eb !important;
+ }
+ th {
+ padding: 10px 12px !important;
+ background-color: #f9fafb !important;
+ border: 1px solid #e5e7eb !important;
+ font-weight: bold !important;
+ text-align: left !important;
+ }
+ td {
+ padding: 8px 12px !important;
+ border: 1px solid #e5e7eb !important;
+ }
+ code {
+ background-color: #f3f4f6 !important;
+ padding: 2px 6px !important;
+ border-radius: 4px !important;
+ font-size: 0.9em !important;
+ font-family: 'Courier New', monospace !important;
+ }
+ pre {
+ background-color: #f8f9fa !important;
+ padding: 16px !important;
+ border-radius: 8px !important;
+ overflow: auto !important;
+ margin: 16px 0 !important;
+ border: 1px solid #e9ecef !important;
+ }
+ pre code {
+ background-color: transparent !important;
+ padding: 0 !important;
+ font-size: 14px !important;
+ }
+ blockquote {
+ border-left: 4px solid #cccccc !important;
+ padding-left: 16px !important;
+ margin: 16px 0 !important;
+ font-style: italic !important;
+ color: #666666 !important;
+ background-color: #f9f9f9 !important;
+ padding: 12px 16px !important;
+ border-radius: 0 6px 6px 0 !important;
+ }
+ img {
+ max-width: 100% !important;
+ height: auto !important;
+ border-radius: 6px !important;
+ margin: 12px 0 !important;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1) !important;
+ }
+ a {
+ color: #0066cc !important;
+ text-decoration: underline !important;
+ }
+ a:hover {
+ color: #004499 !important;
+ }
+ /* KaTeX样式支持 */
+ .katex {
+ font-size: 1em !important;
+ }
+ .katex-display {
+ margin: 16px 0 !important;
+ text-align: center !important;
+ }
+ `;
+ clonedDoc.head.appendChild(styleElement);
+ }
+ });
+
+ canvas.toBlob((blob) => {
+ if (blob) {
+ saveAs(blob, finalFilename);
+ }
+ }, 'image/png', 1.0);
+}
+
+// 下载Word格式
+export async function downloadAsWord(content: string, filename?: string): Promise {
+ const finalFilename = filename ?? generateFilename('research-report', 'docx');
+
+ // 简单的markdown转文本处理
+ const textContent = content
+ .replace(/^#+\s*/gm, '') // 移除标题标记
+ .replace(/\*\*(.*?)\*\*/g, '$1') // 移除粗体标记
+ .replace(/\*(.*?)\*/g, '$1') // 移除斜体标记
+ .replace(/^\- /gm, '• ') // 转换列表项
+ .trim();
+
+ const doc = new Document({
+ sections: [
+ {
+ properties: {},
+ children: textContent.split('\n\n').map(paragraph =>
+ new Paragraph({
+ children: [new TextRun(paragraph.trim())],
+ spacing: {
+ after: 200,
+ },
+ })
+ ),
+ },
+ ],
+ });
+
+ const blob = await Packer.toBlob(doc);
+ saveAs(blob, finalFilename);
+}
+
+// 根据内容创建一个可以截图的临时元素
+export function createTempElementForScreenshot(content: string): string {
+ const tempId = `temp-screenshot-${Date.now()}`;
+ const tempDiv = document.createElement('div');
+ tempDiv.id = tempId;
+ tempDiv.style.position = 'absolute';
+ tempDiv.style.left = '-9999px';
+ tempDiv.style.top = '-9999px';
+ tempDiv.style.width = '800px';
+ tempDiv.style.padding = '20px';
+ tempDiv.style.fontFamily = 'Arial, sans-serif';
+ tempDiv.style.fontSize = '14px';
+ tempDiv.style.lineHeight = '1.6';
+ tempDiv.style.color = '#000000';
+ tempDiv.style.backgroundColor = '#ffffff';
+ tempDiv.style.border = '1px solid #dddddd';
+ tempDiv.style.borderRadius = '8px';
+ // 添加CSS变量覆盖,避免oklch颜色函数
+ tempDiv.style.setProperty('--color', '#000000');
+ tempDiv.style.setProperty('--background', '#ffffff');
+
+ // 使用ReactMarkdown正确渲染Markdown内容
+ const htmlContent = renderMarkdownToHTML(content);
+
+ tempDiv.innerHTML = `
+ ${htmlContent}
+
`;
+ document.body.appendChild(tempDiv);
+
+ return tempId;
+}
+
+// 清理临时元素
+export function cleanupTempElement(elementId: string): void {
+ const element = document.getElementById(elementId);
+ if (element) {
+ document.body.removeChild(element);
+ }
+}
\ No newline at end of file