From 78f4caa3bf3550a5e5615307eb3b682a6e739b1b Mon Sep 17 00:00:00 2001
From: 202304001 <941456317@qq.com>
Date: Thu, 3 Apr 2025 18:03:07 +0800
Subject: [PATCH] AI写作工具栏功能、写作提示词修改、pdf/word文件内容上传读取

---
 app/components/artifacts.tsx                |   7 +++++--
 app/components/chat.tsx                     |  73 +++++++++++++++++++++++++++++++++++++++++++++++++------------------------
 app/components/writing/write-siderBar.tsx   |  14 +++++++++++++-
 app/components/writing/writie-panel.tsx     |  45 ++++++++++++++++++++++++++++++++++-----------
 app/components/writing/writing.module.scss  |   7 +++++++
 app/components/writing/writing.tsx          | 246 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------------------------------------------------------------------------
 app/icons/HTML.svg                          |   1 +
 app/locales/cn.ts                           |   2 ++
 app/store/index.ts                          |   2 +-
 app/store/message.ts                        |  36 ------------------------------------
 app/store/mindMap.ts                        |  36 ++++++++++++++++++++++++++++++++++++
 app/utils/excelAndWordUtils/export2Excel.ts | 129 ---------------------------------------------------------------------------------------------------------------------------------
 app/utils/excelAndWordUtils/word.ts         |  72 ------------------------------------------------------------------------
 app/utils/fileExport/export2Excel.ts        | 154 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 app/utils/fileExport/toPdf.ts               | 166 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 app/utils/fileExport/word.ts                |  91 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 app/utils/prompt.ts                         |  67 ++++++++++++++++++++++++++++++++++++++++---------------------------
 package.json                                |   4 ++++
 yarn.lock                                   | 271 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
 19 files changed, 1027 insertions(+), 396 deletions(-)
 create mode 100644 app/icons/HTML.svg
 delete mode 100644 app/store/message.ts
 create mode 100644 app/store/mindMap.ts
 delete mode 100644 app/utils/excelAndWordUtils/export2Excel.ts
 delete mode 100644 app/utils/excelAndWordUtils/word.ts
 create mode 100644 app/utils/fileExport/export2Excel.ts
 create mode 100644 app/utils/fileExport/toPdf.ts
 create mode 100644 app/utils/fileExport/word.ts

diff --git a/app/components/artifacts.tsx b/app/components/artifacts.tsx
index ce187fb..00536bf 100644
--- a/app/components/artifacts.tsx
+++ b/app/components/artifacts.tsx
@@ -27,6 +27,7 @@ type HTMLPreviewProps = {
   autoHeight?: boolean;
   height?: number | string;
   onLoad?: (title?: string) => void;
+  width?: number | string; //20250403 新增宽度配置
 };
 
 export type HTMLPreviewHander = {
@@ -78,6 +79,8 @@ export const HTMLPreview = forwardRef<HTMLPreviewHander, HTMLPreviewProps>(
         : iframeHeight + 40;
     }, [props.autoHeight, props.height, iframeHeight]);
 
+    const width = props.width || "100%"; //20250403 新增宽度配置
+
     const srcDoc = useMemo(() => {
       const script = `<script>window.addEventListener("DOMContentLoaded", () => new ResizeObserver((entries) => parent.postMessage({id: '${frameId}', height: entries[0].target.clientHeight}, '*')).observe(document.body))</script>`;
       if (props.code.includes("<!DOCTYPE html>")) {
@@ -98,7 +101,7 @@ export const HTMLPreview = forwardRef<HTMLPreviewHander, HTMLPreviewProps>(
         key={frameId}
         ref={iframeRef}
         sandbox="allow-forms allow-modals allow-scripts"
-        style={{ height }}
+        style={{ height, width }}
         srcDoc={srcDoc}
         onLoad={handleOnLoad}
       />
@@ -253,7 +256,7 @@ export function Artifacts() {
             code={code}
             ref={previewRef}
             autoHeight={false}
-            height={"100%"}
+            height={"100%"} //20250403 新增宽度配置
             onLoad={(title) => {
               setFileName(title as string);
               setLoading(false);
diff --git a/app/components/chat.tsx b/app/components/chat.tsx
index ea54050..4c20051 100644
--- a/app/components/chat.tsx
+++ b/app/components/chat.tsx
@@ -136,10 +136,11 @@ import clsx from "clsx";
 import { getAvailableClientsCount, isMcpEnabled } from "../mcp/actions";
 
 //20250317新增
-import { getExcelData, toExcel } from "../utils/excelAndWordUtils/export2Excel";
-import { exportWord } from "../utils/excelAndWordUtils/word";
+import { getExcelData, toExcel } from "../utils/fileExport/export2Excel";
+import { exportWord, getWordData } from "../utils/fileExport/word";
 import { getMindPrompt } from "../utils/prompt";
 import { message } from "antd";
+import { getPdfData } from "../utils/fileExport/toPdf";
 const localStorage = safeLocalStorage();
 
 const ttsPlayer = createTTSPlayer();
@@ -1774,33 +1775,57 @@ function _Chat() {
   const [fileName, setFileName] = useState("");
 
   async function uploadFile() {
-    // 创建一个隐藏的文件输入框
     const fileInput = document.createElement("input");
     fileInput.type = "file";
-    fileInput.accept = ".xlsx, .xls";
+    fileInput.accept = ".xlsx, .xls, .pdf, .docx, .doc";
     fileInput.multiple = false;
-    fileInput.onchange = (event: Event) => {
-      const target = event.target as HTMLInputElement;
-      const files = target.files;
-      if (files && files.length > 0) {
-        const file = files[0]; // 获取第一个文件
-        getExcelData(file)
-          .then((data) => {
-            const value = `'''filedata
-                          ${file.name}
-                          ${JSON.stringify(data)}
-                          '''filedata
-                        `;
-            setFileData(value);
-            setFileName(file.name);
-            console.log(value);
-          })
-          .catch((error) => {
-            message.error("上传失败");
-          });
+    fileInput.style.display = "none";
+    const handleFileResult = (fileName: string, data: any) => {
+      if (!data) {
+        message.error("未读取到内容");
+        return;
+      }
+      setFileData(`'''filedata
+                  ${fileName}
+                  ${JSON.stringify(data)}
+                  '''filedata`);
+      setFileName(fileName);
+    };
+    const handleError = (error: any, defaultMsg = "上传失败") => {
+      console.error(`${defaultMsg}:`, error);
+      message.error(defaultMsg);
+    };
+    // 文件处理器映射
+    const fileHandlers: Record<string, (file: File) => Promise<any>> = {
+      ".xlsx": getExcelData,
+      ".xls": getExcelData,
+      ".doc": getWordData,
+      ".docx": getWordData,
+      ".pdf": getPdfData,
+    };
+    fileInput.onchange = async (event: Event) => {
+      try {
+        const file = (event.target as HTMLInputElement).files?.[0];
+        if (!file) return;
+        const fileExt = file.name
+          .toLowerCase()
+          .slice(file.name.lastIndexOf("."));
+        // 校验文件类型
+        if (!Object.keys(fileHandlers).includes(fileExt)) {
+          message.error("不支持的文件类型");
+          return;
+        }
+        // 获取处理器并执行
+        const handler = fileHandlers[fileExt];
+        const data = await handler(file);
+        handleFileResult(file.name, data);
+      } catch (error) {
+        handleError(error);
+      } finally {
+        fileInput.remove();
       }
-      fileInput.remove();
     };
+    document.body.appendChild(fileInput);
     fileInput.click();
   }
   return (
diff --git a/app/components/writing/write-siderBar.tsx b/app/components/writing/write-siderBar.tsx
index 95959da..523ea1e 100644
--- a/app/components/writing/write-siderBar.tsx
+++ b/app/components/writing/write-siderBar.tsx
@@ -21,6 +21,8 @@ export interface WriteSiderBarProps {
   setHtmlCode: React.Dispatch<React.SetStateAction<string>>;
   loading: boolean;
   setLoading: React.Dispatch<React.SetStateAction<boolean>>;
+  setWidth: React.Dispatch<React.SetStateAction<string>>;
+  setHtmlheader: React.Dispatch<React.SetStateAction<string>>;
 }
 
 const WritingPanel = dynamic(
@@ -31,7 +33,15 @@ const WritingPanel = dynamic(
 );
 
 export function WriteSiderBar(props: WriteSiderBarProps) {
-  const { className, htmlCode, setHtmlCode, loading, setLoading } = props;
+  const {
+    className,
+    htmlCode,
+    setHtmlCode,
+    loading,
+    setLoading,
+    setWidth,
+    setHtmlheader,
+  } = props;
   const isMobileScreen = useMobileScreen();
   const { onDragStart, shouldNarrow } = useDragSideBar();
   const navigate = useNavigate();
@@ -93,6 +103,8 @@ export function WriteSiderBar(props: WriteSiderBarProps) {
             setHtmlCode={setHtmlCode}
             loading={loading}
             setLoading={setLoading}
+            setWidth={setWidth}
+            setHtmlheader={setHtmlheader}
           />
         </SideBarBody>
       </SideBarContainer>
diff --git a/app/components/writing/writie-panel.tsx b/app/components/writing/writie-panel.tsx
index 81bc449..33b7127 100644
--- a/app/components/writing/writie-panel.tsx
+++ b/app/components/writing/writie-panel.tsx
@@ -16,11 +16,10 @@ const mergedData = [
     type: "select",
     default: "公司官网",
     options: [
-      { name: "公司官网", value: "1" },
-      { name: "小红书", value: "2" },
-      { name: "微信", value: "3" },
-      { name: "公众号", value: "4" },
-      { name: "今日头条", value: "5" },
+      { name: "公司官网", value: "100%" },
+      { name: "小红书", value: "400px" },
+      { name: "微信公众号", value: "300px" },
+      { name: "今日头条", value: "500px" },
     ],
   },
   {
@@ -62,10 +61,10 @@ const mergedData = [
     required: false,
     default: "产品推广文案",
     options: [
-      { name: "产品推广文案", value: "产品推广文案" },
-      { name: "品牌宣传文案", value: "品牌宣传文案" },
-      { name: "产品说明书", value: "产品说明书" },
-      { name: "产品介绍", value: "产品介绍" },
+      { name: "产品推广文案", value: "promotion" },
+      { name: "品牌宣传文案", value: "propagandize" },
+      { name: "产品说明书", value: "instructionBook" },
+      { name: "产品介绍", value: "introduce" },
     ],
   },
   {
@@ -85,9 +84,18 @@ export interface WritePanelProps {
   setHtmlCode: React.Dispatch<React.SetStateAction<string>>;
   loading: boolean;
   setLoading: React.Dispatch<React.SetStateAction<boolean>>;
+  setWidth: React.Dispatch<React.SetStateAction<string>>;
+  setHtmlheader: React.Dispatch<React.SetStateAction<string>>;
 }
 export function WritingPanel(props: WritePanelProps) {
-  const { htmlCode, setHtmlCode, setLoading, loading } = props;
+  const {
+    htmlCode,
+    setHtmlCode,
+    setLoading,
+    loading,
+    setWidth,
+    setHtmlheader,
+  } = props;
   // 为每个选择框单独声明状态,存储name
   const chatStore = useChatStore();
   const [writingPurposeName, setWritingPurposeName] = useState("公司官网"); // 写作用途
@@ -104,10 +112,12 @@ export function WritingPanel(props: WritePanelProps) {
   const handleSelectChange = (index: number, value: string) => {
     const options = mergedData[index].options;
     const selectedName = options.find((opt) => opt.value === value)?.name || "";
-
+    const selectedValue =
+      options.find((opt) => opt.value === value)?.value || "";
     switch (index) {
       case 0:
         setWritingPurposeName(selectedName);
+        setWidth(selectedValue);
         break;
       case 1:
         setImageModeName(selectedName);
@@ -164,6 +174,19 @@ export function WritingPanel(props: WritePanelProps) {
       if (cleanedContent.endsWith("```")) {
         cleanedContent = cleanedContent.substring(0, cleanedContent.length - 4);
       }
+
+      //保存html头部
+      const bodyTagRegex = /<body[^>]*>/i;
+      const bodyTagMatch = cleanedContent.match(bodyTagRegex);
+      if (bodyTagMatch && bodyTagMatch.index !== undefined) {
+        // 截取从文档开头到 <body> 标签的起始位置
+        const contentUpToBody = cleanedContent.slice(
+          0,
+          bodyTagMatch.index + bodyTagMatch[0].length,
+        );
+        setHtmlheader(contentUpToBody); //保存html头部
+      }
+      localStorage.setItem("htmlCode", cleanedContent);
       setHtmlCode(cleanedContent);
     } catch (error) {
       message.error("生成失败,请重试");
diff --git a/app/components/writing/writing.module.scss b/app/components/writing/writing.module.scss
index c1d7e8d..ab16e9f 100644
--- a/app/components/writing/writing.module.scss
+++ b/app/components/writing/writing.module.scss
@@ -1,3 +1,10 @@
+.write-body{
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    align-items: center;
+}
+
 .loading-content {
     display: flex;
     flex-direction: column;
diff --git a/app/components/writing/writing.tsx b/app/components/writing/writing.tsx
index c497085..79bc3d9 100644
--- a/app/components/writing/writing.tsx
+++ b/app/components/writing/writing.tsx
@@ -11,12 +11,11 @@ import Locale from "@/app/locales";
 import { Path } from "@/app/constant";
 import { useNavigate } from "react-router-dom";
 import { getClientConfig } from "@/app/config/client";
-import React, { useMemo, useRef, useState } from "react";
-import { useAppConfig } from "@/app/store";
+import React, { useCallback, useMemo, useRef, useState } from "react";
+import { useAppConfig, useMindMapStore } from "@/app/store";
 import { ChatAction } from "../chat";
 import { useWindowSize } from "@/app/utils";
-import { exportWord } from "@/app/utils/excelAndWordUtils/word";
-import { HTMLPreview } from "../artifacts";
+import { exportHtmlToWord } from "@/app/utils/fileExport/word";
 import ReactQuill from "react-quill";
 import "react-quill/dist/quill.snow.css";
 
@@ -34,7 +33,13 @@ import WordIcon from "@/app/icons/word.svg";
 import MindIcon from "@/app/icons/mind.svg";
 import PptIcon from "@/app/icons/ppt.svg";
 import PdfIcon from "@/app/icons/pdf.svg";
+import HtmlIcon from "@/app/icons/HTML.svg";
+
 import { message } from "antd";
+import { HTMLPreview } from "../artifacts";
+import { getMindPrompt } from "@/app/utils/prompt";
+import { htmlToPdf2 } from "@/app/utils/fileExport/toPdf";
+import { htmlToExcel } from "@/app/utils/fileExport/export2Excel";
 
 export function WritingPage() {
   const isMobileScreen = useMobileScreen();
@@ -45,10 +50,14 @@ export function WritingPage() {
   const scrollRef = useRef<HTMLDivElement>(null);
   const isWriting = location.pathname === Path.Writing;
   const { height } = useWindowSize();
+  const [width, setWidth] = useState("100%");
   const [isEdit, setIsEdit] = useState(false);
   const [loading, setLoading] = useState(false);
   const quillRef = useRef<ReactQuill | null>(null);
-  const [htmlCode, setHtmlCode] = useState("");
+  const [htmlHeader, setHtmlheader] = useState("");
+  const [htmlCode, setHtmlCode] = useState(
+    localStorage.getItem("htmlCode") || "",
+  );
 
   //编辑器
   const toolbarOptions = [
@@ -60,42 +69,66 @@ export function WritingPage() {
     ["link", "image"],
   ];
 
-  const copyToClipboard = () => {
-    // 检查quillRef.current是否为null
-    if (quillRef.current) {
-      const editor = quillRef.current.getEditor(); // 获取编辑器实例
-      const range = editor.getSelection(); // 获取当前选择的范围
-      const text = editor.getText(); // 获取编辑器中的文本内容
+  // 生成完整HTML内容
+  const generateFullHtml = useCallback(
+    () => `${htmlHeader}${htmlCode}</body></html>`,
+    [htmlHeader, htmlCode],
+  );
 
-      if (range && range.length > 0) {
-        // 如果有选中的文本,复制选中的内容
-        const selectedText = text.substring(
-          range.index,
-          range.index + range.length,
-        );
-        navigator.clipboard
-          .writeText(selectedText)
-          .then(() => {
-            message.success("复制成功");
-          })
-          .catch((err) => {
-            message.error("复制失败:");
-          });
-      } else {
-        // 如果没有选择文本,就复制全部内容
-        navigator.clipboard
-          .writeText(text)
-          .then(() => {
-            message.success("复制成功");
-          })
-          .catch((err) => {
-            message.error("复制失败:");
-          });
-      }
-    } else {
-      console.error("Quill编辑器尚未初始化");
+  const handleCopy = async () => {
+    try {
+      const blob = new Blob([htmlCode], { type: "text/html" });
+      const clipboardItem = new ClipboardItem({ "text/html": blob });
+      await navigator.clipboard.write([clipboardItem]);
+      message.success("复制成功!");
+    } catch (error) {
+      message.error("复制失败");
     }
   };
+  //跳转到ppt页面
+  function toPowerpoint(pptMessage: string) {
+    navigate("/powerpoint", { state: { msg: true, pptMessage: pptMessage } });
+  }
+  // 转至思维导图页面
+  const toMind = useCallback(
+    (content: string) => {
+      const { setMindMapData } = useMindMapStore.getState();
+      setMindMapData(
+        [
+          {
+            role: "user",
+            content: getMindPrompt(content, true),
+          },
+        ],
+        content,
+      );
+      navigate("/mind", { state: { msg: true } });
+    },
+    [navigate],
+  );
+
+  //导出html文件
+  const exportHtml = useCallback(() => {
+    try {
+      const htmlContent = generateFullHtml();
+      const blob = new Blob([htmlContent], { type: "text/html" });
+      const url = URL.createObjectURL(blob);
+      const a = document.createElement("a");
+      a.href = url;
+      a.download = "output.html";
+      a.click();
+      URL.revokeObjectURL(url);
+      message.success("导出成功");
+    } catch (error) {
+      message.error("导出失败");
+    }
+  }, [generateFullHtml]);
+
+  function hasTable(htmlContent: string): boolean {
+    const parser = new DOMParser();
+    const doc = parser.parseFromString(htmlContent, "text/html");
+    return doc.querySelector("table") !== null;
+  }
 
   return (
     <>
@@ -105,6 +138,8 @@ export function WritingPage() {
         setHtmlCode={setHtmlCode}
         loading={loading}
         setLoading={setLoading}
+        setWidth={setWidth}
+        setHtmlheader={setHtmlheader}
       />
       <WindowContent>
         <div className={chatStyles.chat} key={"1"}>
@@ -130,66 +165,97 @@ export function WritingPage() {
               <div className={`window-header-main-title`}>AI-Writing</div>
             </div>
             <div className={chatStyles["chat-message-actions"]}>
-              <div className={chatStyles["chat-input-actions"]}>
-                <ChatAction
-                  text={Locale.Chat.Actions.ReWrite}
-                  icon={<ReloadIcon />}
-                  onClick={() => {}}
-                />
-                <ChatAction
-                  text={Locale.Chat.Actions.Copy}
-                  icon={<CopyIcon />}
-                  onClick={copyToClipboard}
-                />
-                {!isEdit ? (
+              {htmlCode && (
+                <div className={chatStyles["chat-input-actions"]}>
                   <ChatAction
-                    text={Locale.Chat.Actions.Edit}
-                    icon={<EditIcon />}
-                    onClick={() => {
-                      setIsEdit(true);
+                    text={Locale.Chat.Actions.ReWrite}
+                    icon={<ReloadIcon />}
+                    onClick={() => {}}
+                    disabled={isEdit}
+                  />
+                  <ChatAction
+                    text={Locale.Chat.Actions.Copy}
+                    icon={<CopyIcon />}
+                    onClick={handleCopy}
+                    disabled={isEdit}
+                  />
+                  {!isEdit ? (
+                    <ChatAction
+                      text={Locale.Chat.Actions.Edit}
+                      icon={<EditIcon />}
+                      onClick={() => {
+                        setIsEdit(true);
+                      }}
+                    />
+                  ) : (
+                    <ChatAction
+                      text={Locale.Chat.Actions.CancelEdit}
+                      icon={<EditIcon />}
+                      onClick={() => {
+                        setIsEdit(false);
+                      }}
+                    />
+                  )}
+                  <ChatAction
+                    text={Locale.Export.Pdf}
+                    icon={<PdfIcon />}
+                    onClick={async () => {
+                      setLoading(true);
+                      await htmlToPdf2(htmlCode);
+                      setLoading(false);
                     }}
+                    disabled={isEdit}
                   />
-                ) : (
                   <ChatAction
-                    text="取消编辑"
-                    icon={<EditIcon />}
+                    text={Locale.Export.Word}
+                    icon={<WordIcon />}
                     onClick={() => {
-                      setIsEdit(false);
+                      const html = `${htmlHeader}
+                                ${htmlCode}
+                                </body>
+                                </html>
+                      `;
+                      exportHtmlToWord(html);
                     }}
+                    disabled={isEdit}
                   />
-                )}
+                  {hasTable(htmlCode) && (
+                    <ChatAction
+                      text={Locale.Export.Excel}
+                      icon={<ExcelIcon />}
+                      onClick={() => {
+                        htmlToExcel(htmlCode);
+                      }}
+                      disabled={isEdit}
+                    />
+                  )}
 
-                <ChatAction
-                  text={Locale.Export.Pdf}
-                  icon={<PdfIcon />}
-                  onClick={() => {}}
-                />
-                {htmlCode && (
                   <ChatAction
-                    text={Locale.Export.Word}
-                    icon={<WordIcon />}
+                    text={Locale.Export.Ppt}
+                    icon={<PptIcon />}
                     onClick={() => {
-                      exportWord(htmlCode);
+                      toPowerpoint(htmlCode);
                     }}
+                    disabled={isEdit}
                   />
-                )}
-                <ChatAction
-                  text={Locale.Export.Excel}
-                  icon={<ExcelIcon />}
-                  onClick={() => {}}
-                  disabled={true}
-                />
-                <ChatAction
-                  text={Locale.Export.Ppt}
-                  icon={<PptIcon />}
-                  onClick={() => {}}
-                />
-                <ChatAction
-                  text={Locale.Export.Mind.title}
-                  icon={<MindIcon />}
-                  onClick={() => {}}
-                />
-              </div>
+                  <ChatAction
+                    text={Locale.Export.Mind.title}
+                    icon={<MindIcon />}
+                    onClick={() => {
+                      toMind(htmlCode);
+                    }}
+                    disabled={isEdit}
+                  />
+                  <ChatAction
+                    text={Locale.Export.Html}
+                    icon={<HtmlIcon />}
+                    onClick={() => {
+                      exportHtml();
+                    }}
+                    disabled={isEdit}
+                  />
+                </div>
+              )}
             </div>
             <div className="window-actions">
               {showMaxIcon && (
@@ -209,7 +275,10 @@ export function WritingPage() {
               {isMobileScreen && <SDIcon width={50} height={50} />}
             </div>
           </div>
-          <div className={chatStyles["chat-body"]} ref={scrollRef}>
+          <div
+            className={`${chatStyles["chat-body"]} ${styles["write-body"]}`}
+            ref={scrollRef}
+          >
             {loading ? (
               <div className={clsx("no-dark", styles["loading-content"])}>
                 <BotIcon />
@@ -231,7 +300,8 @@ export function WritingPage() {
                 <HTMLPreview
                   code={htmlCode}
                   autoHeight={!document.fullscreenElement}
-                  height={!document.fullscreenElement ? 600 : height}
+                  height={!document.fullscreenElement ? "100%" : height}
+                  width={width}
                 />
               ))
             )}
diff --git a/app/icons/HTML.svg b/app/icons/HTML.svg
new file mode 100644
index 0000000..e414e44
--- /dev/null
+++ b/app/icons/HTML.svg
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1743667735062" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4363" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16"><path d="M354.40128 0c-87.04 0-157.44 70.55872-157.44 157.59872v275.68128H78.72c-21.6576 0-39.36256 17.69984-39.36256 39.36256v236.31872c0 21.6576 17.69984 39.35744 39.36256 39.35744h118.24128v118.08256c0 87.04 70.4 157.59872 157.44 157.59872h472.63744c87.04 0 157.59872-70.55872 157.59872-157.59872V315.0336c0-41.74848-38.9888-81.93024-107.52-149.27872l-29.11744-29.12256L818.87744 107.52C751.5392 38.9888 711.39328 0 669.59872 0H354.4064z m0 78.72h287.20128c28.35456 7.0912 27.99616 42.1376 27.99616 76.8v120.16128c0 21.6576 17.69984 39.35744 39.36256 39.35744h118.07744c39.38816 0 78.87872-0.0256 78.87872 39.36256v512c0 43.32032-35.55328 78.87872-78.87872 78.87872H354.4064c-43.32544 0-78.72-35.5584-78.72-78.87872v-118.08256h393.91744c21.66272 0 39.36256-17.69472 39.36256-39.35744V472.64256c0-21.66272-17.69984-39.36256-39.36256-39.36256H275.68128V157.59872c0-43.32032 35.39456-78.87872 78.72-78.87872zM75.03872 493.59872h22.08256v73.92256H176.7936V493.59872h23.04v175.68256h-23.04V587.6736H97.1264v81.60256h-22.08256V493.59872z m151.68 0h121.92256v20.16256h-49.92v155.52H276.6336v-155.52h-49.92v-20.16256z m148.80256 0h32.64l49.92 143.04256h0.95744l48.96256-143.04256h33.59744v175.68256h-22.07744v-106.56256c0-10.88 0.31744-26.55744 0.95744-47.03744h-0.95744l-52.80256 153.6h-19.2l-52.79744-153.6h-0.96256c1.28 22.4 1.92 38.71744 1.92 48.95744v104.64256h-20.15744V493.59872z m214.07744 0h22.08256v155.52h69.12v20.16256h-91.20256V493.59872z" p-id="4364"></path></svg>
\ No newline at end of file
diff --git a/app/locales/cn.ts b/app/locales/cn.ts
index dd48ed8..afae074 100644
--- a/app/locales/cn.ts
+++ b/app/locales/cn.ts
@@ -53,6 +53,7 @@ const cn = {
       PinToastAction: "查看",
       Delete: "删除",
       Edit: "编辑",
+      CancelEdit: "取消编辑",
       FullScreen: "全屏",
       RefreshTitle: "刷新标题",
       RefreshToast: "已发送刷新标题请求",
@@ -148,6 +149,7 @@ const cn = {
     Excel: "下载Excel",
     Pdf: "导出PDF",
     Ppt: "导出PPT",
+    Html: "导出HTML",
   },
   Select: {
     Search: "搜索消息",
diff --git a/app/store/index.ts b/app/store/index.ts
index fb82fc1..8dffb0c 100644
--- a/app/store/index.ts
+++ b/app/store/index.ts
@@ -3,4 +3,4 @@ export * from "./update";
 export * from "./access";
 export * from "./config";
 export * from "./plugin";
-export * from "./message";
+export * from "./mindMap";
diff --git a/app/store/message.ts b/app/store/message.ts
deleted file mode 100644
index 23f2a02..0000000
--- a/app/store/message.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-import { createPersistStore } from "../utils/store";
-
-export const useMindMapStore = createPersistStore<
-  { newMessages: { role: string; content: string }[]; content: string },
-  {
-    setMindMapData: (
-      newMessages: { role: string; content: string }[],
-      content: string,
-    ) => void;
-    clearMindMapData: () => void;
-  }
->(
-  {
-    newMessages: [],
-    content: "",
-  },
-  (set, get) => ({
-    setMindMapData: (newMessages, content) => {
-      set(() => ({
-        newMessages,
-        content,
-      }));
-    },
-
-    clearMindMapData: () => {
-      set(() => ({
-        newMessages: [],
-        content: "",
-      }));
-    },
-  }),
-  {
-    name: "mind-map-store",
-    version: 1,
-  },
-);
diff --git a/app/store/mindMap.ts b/app/store/mindMap.ts
new file mode 100644
index 0000000..23f2a02
--- /dev/null
+++ b/app/store/mindMap.ts
@@ -0,0 +1,36 @@
+import { createPersistStore } from "../utils/store";
+
+export const useMindMapStore = createPersistStore<
+  { newMessages: { role: string; content: string }[]; content: string },
+  {
+    setMindMapData: (
+      newMessages: { role: string; content: string }[],
+      content: string,
+    ) => void;
+    clearMindMapData: () => void;
+  }
+>(
+  {
+    newMessages: [],
+    content: "",
+  },
+  (set, get) => ({
+    setMindMapData: (newMessages, content) => {
+      set(() => ({
+        newMessages,
+        content,
+      }));
+    },
+
+    clearMindMapData: () => {
+      set(() => ({
+        newMessages: [],
+        content: "",
+      }));
+    },
+  }),
+  {
+    name: "mind-map-store",
+    version: 1,
+  },
+);
diff --git a/app/utils/excelAndWordUtils/export2Excel.ts b/app/utils/excelAndWordUtils/export2Excel.ts
deleted file mode 100644
index 8bae2a4..0000000
--- a/app/utils/excelAndWordUtils/export2Excel.ts
+++ /dev/null
@@ -1,129 +0,0 @@
-/* eslint-disable */
-import * as XLSX from "xlsx";
-
-export function toExcel(content: string) {
-  let sheetName = "result"; // 默认表名
-  let tableContent = "";
-
-  // 查找标题内容(以 ** 或 ## 或 ### 开头)
-  const titleMatch = content.match(/^(#{2,3}|\*\*)\s*(.*)/m);
-  if (titleMatch) {
-    sheetName = titleMatch[2].trim().replace(/\s+/g, "_"); // 使用标题作为表名
-    // 提取表格内容(跳过标题部分)
-    tableContent = content.substring(titleMatch[0].length).trim();
-  } else {
-    tableContent = content;
-  }
-
-  // 查找表格的起始位置(第一行以 | 开头且以 | 结尾)
-  let tableStartIndex = -1;
-  const lines = tableContent.split("\n");
-  for (let i = 0; i < lines.length; i++) {
-    if (lines[i].trim().startsWith("|") && lines[i].trim().endsWith("|")) {
-      tableStartIndex = i;
-      break;
-    }
-  }
-
-  if (tableStartIndex === -1) {
-    console.error("表格内容未找到");
-    return;
-  }
-
-  // 查找表格的结束位置(遇到不以 | 开头或者不以 | 结尾的行)
-  let tableEndIndex = -1;
-  for (let i = tableStartIndex; i < lines.length; i++) {
-    if (!lines[i].trim().startsWith("|") || !lines[i].trim().endsWith("|")) {
-      tableEndIndex = i;
-      break;
-    }
-  }
-
-  if (tableEndIndex === -1) {
-    tableEndIndex = lines.length;
-  }
-
-  // 提取表格内容
-  const tableData = lines
-    .slice(tableStartIndex, tableEndIndex)
-    .join("\n")
-    .trim();
-
-  // 解析表格内容
-  const rows = tableData.split("\n");
-  const data: any[] = [];
-  let headers: string[] = [];
-
-  rows.forEach((row, rowIndex) => {
-    // 去掉行首尾的 | 符号,并按 | 分割
-    const cells = row
-      .replace(/^\s*\|/g, "")
-      .replace(/\|\s*$/g, "")
-      .split(/\s*\|\s*/);
-
-    // 跳过分隔线(假设分隔线的每个单元格都是由短横线组成)
-    if (
-      rowIndex > 0 &&
-      cells.every(
-        (cell) => cell.trim().replace(/-/g, "").length === 0, // 检查单元格内容是否只包含短横线
-      )
-    ) {
-      return;
-    }
-
-    if (rowIndex === 0) {
-      // 第一行是表头
-      headers = cells.map((cell) => cell.trim());
-    } else {
-      // 数据行
-      const rowData: any = {};
-      cells.forEach((cell, cellIndex) => {
-        if (cellIndex < headers.length) {
-          rowData[headers[cellIndex]] = cell.trim();
-        }
-      });
-      data.push(rowData);
-    }
-  });
-
-  // 创建工作表
-  const ws = XLSX.utils.json_to_sheet(data);
-  const wb = XLSX.utils.book_new();
-  XLSX.utils.book_append_sheet(wb, ws, sheetName);
-
-  // 生成文件并下载
-  XLSX.writeFile(wb, `${sheetName}.xlsx`);
-}
-
-export function getExcelData(file: File): Promise<any[][]> {
-  return new Promise((resolve, reject) => {
-    const reader = new FileReader();
-
-    reader.onload = (e) => {
-      try {
-        const data = e.target?.result;
-        if (!data) {
-          reject(new Error("Failed to read file data"));
-          return;
-        }
-
-        const workbook = XLSX.read(data, { type: "array" });
-        const firstSheetName = workbook.SheetNames[0];
-        const worksheet = workbook.Sheets[firstSheetName];
-
-        // 使用类型断言将 unknown[] 转换为 any[][]
-        const jsonData = XLSX.utils.sheet_to_json(worksheet, {
-          header: 1,
-        }) as any[][];
-        resolve(jsonData.slice(0, 100));
-      } catch (error) {
-        reject(error);
-      }
-    };
-    reader.onerror = (error) => {
-      reject(error);
-    };
-
-    reader.readAsArrayBuffer(file);
-  });
-}
diff --git a/app/utils/excelAndWordUtils/word.ts b/app/utils/excelAndWordUtils/word.ts
deleted file mode 100644
index 3f293bb..0000000
--- a/app/utils/excelAndWordUtils/word.ts
+++ /dev/null
@@ -1,72 +0,0 @@
-import { Document, Packer, Paragraph, TextRun } from "docx";
-import { saveAs } from "file-saver";
-import * as cheerio from "cheerio";
-
-export function exportWord(content: string) {
-    console.log(content)
-    // 简单类型检测示例
-    const isHTML = (text: string): boolean => {
-        return /<html[\s>]/.test(text) &&
-            /<\/html>/.test(text) &&
-            /<head>/.test(text) &&
-            /<body>/.test(text)
-    }
-    if (isHTML(content)) {
-        exportHtmlToWord(content)
-    } else {
-        exportMarkdownToWord(content)
-    }
-}
-
-function exportHtmlToWord(content: string) {
-    console.log("-----------------------------------"+content)
-    let cleanedContent = content.startsWith('```html') ? content.substring(8) : content;
-    if (cleanedContent.endsWith('```')) {
-        cleanedContent = cleanedContent.substring(0, cleanedContent.length - 4);
-    }
-    const blob = new Blob([cleanedContent], { type: 'application/msword' });
-    const url = URL.createObjectURL(blob);
-    const a = document.createElement('a');
-    a.href = url;
-    a.download = 'htmldemo.docx';
-    // 触发点击事件,开始下载
-    document.body.appendChild(a);
-    a.click();
-    // 下载完成后移除临时链接元素
-    document.body.removeChild(a);
-    // 释放 Blob URL 对象
-    URL.revokeObjectURL(url);
-}
-
-function exportMarkdownToWord(content: string) {
-    // 按换行符拆分内容
-    const lines = content.split(/\r?\n/);
-    const paragraphs: Paragraph[] = [];
-    for (const line of lines) {
-        // 去除 Markdown 标记(#、*等)
-        const cleanedLine = line.replace(/^#+\s*/, "").replace(/^\*\*\s*|\*\s*/g, "").trim();
-        // 处理空行
-        if (cleanedLine === "") {
-            paragraphs.push(new Paragraph(""));
-            continue;
-        }
-        // 添加文本段落
-        paragraphs.push(new Paragraph(cleanedLine));
-    }
-
-    // 创建 Word 文档对象
-    const doc = new Document({
-        sections: [{
-            children: paragraphs,
-        }],
-    });
-
-    // 转换为 Blob 并下载
-    Packer.toBlob(doc)
-        .then((blob) => {
-            saveAs(blob, "demo.docx");
-        })
-        .catch((error) => {
-            console.error("导出 Word 失败:", error);
-        });
-}
diff --git a/app/utils/fileExport/export2Excel.ts b/app/utils/fileExport/export2Excel.ts
new file mode 100644
index 0000000..83ab433
--- /dev/null
+++ b/app/utils/fileExport/export2Excel.ts
@@ -0,0 +1,154 @@
+/* eslint-disable */
+import * as XLSX from "xlsx";
+
+export function toExcel(content: string) {
+  let sheetName = "result"; // 默认表名
+  let tableContent = "";
+
+  // 查找标题内容(以 ** 或 ## 或 ### 开头)
+  const titleMatch = content.match(/^(#{2,3}|\*\*)\s*(.*)/m);
+  if (titleMatch) {
+    sheetName = titleMatch[2].trim().replace(/\s+/g, "_"); // 使用标题作为表名
+    // 提取表格内容(跳过标题部分)
+    tableContent = content.substring(titleMatch[0].length).trim();
+  } else {
+    tableContent = content;
+  }
+
+  // 查找表格的起始位置(第一行以 | 开头且以 | 结尾)
+  let tableStartIndex = -1;
+  const lines = tableContent.split("\n");
+  for (let i = 0; i < lines.length; i++) {
+    if (lines[i].trim().startsWith("|") && lines[i].trim().endsWith("|")) {
+      tableStartIndex = i;
+      break;
+    }
+  }
+
+  if (tableStartIndex === -1) {
+    console.error("表格内容未找到");
+    return;
+  }
+
+  // 查找表格的结束位置(遇到不以 | 开头或者不以 | 结尾的行)
+  let tableEndIndex = -1;
+  for (let i = tableStartIndex; i < lines.length; i++) {
+    if (!lines[i].trim().startsWith("|") || !lines[i].trim().endsWith("|")) {
+      tableEndIndex = i;
+      break;
+    }
+  }
+
+  if (tableEndIndex === -1) {
+    tableEndIndex = lines.length;
+  }
+
+  // 提取表格内容
+  const tableData = lines
+    .slice(tableStartIndex, tableEndIndex)
+    .join("\n")
+    .trim();
+
+  // 解析表格内容
+  const rows = tableData.split("\n");
+  const data: any[] = [];
+  let headers: string[] = [];
+
+  rows.forEach((row, rowIndex) => {
+    // 去掉行首尾的 | 符号,并按 | 分割
+    const cells = row
+      .replace(/^\s*\|/g, "")
+      .replace(/\|\s*$/g, "")
+      .split(/\s*\|\s*/);
+
+    // 跳过分隔线(假设分隔线的每个单元格都是由短横线组成)
+    if (
+      rowIndex > 0 &&
+      cells.every(
+        (cell) => cell.trim().replace(/-/g, "").length === 0, // 检查单元格内容是否只包含短横线
+      )
+    ) {
+      return;
+    }
+
+    if (rowIndex === 0) {
+      // 第一行是表头
+      headers = cells.map((cell) => cell.trim());
+    } else {
+      // 数据行
+      const rowData: any = {};
+      cells.forEach((cell, cellIndex) => {
+        if (cellIndex < headers.length) {
+          rowData[headers[cellIndex]] = cell.trim();
+        }
+      });
+      data.push(rowData);
+    }
+  });
+
+  // 创建工作表
+  const ws = XLSX.utils.json_to_sheet(data);
+  const wb = XLSX.utils.book_new();
+  XLSX.utils.book_append_sheet(wb, ws, sheetName);
+
+  // 生成文件并下载
+  XLSX.writeFile(wb, `${sheetName}.xlsx`);
+}
+
+export function getExcelData(file: File): Promise<any[][]> {
+  return new Promise((resolve, reject) => {
+    const reader = new FileReader();
+
+    reader.onload = (e) => {
+      try {
+        const data = e.target?.result;
+        if (!data) {
+          reject(new Error("Failed to read file data"));
+          return;
+        }
+        const workbook = XLSX.read(data, { type: "array" });
+        const firstSheetName = workbook.SheetNames[0];
+        const worksheet = workbook.Sheets[firstSheetName];
+
+        // 使用类型断言将 unknown[] 转换为 any[][]
+        const jsonData = XLSX.utils.sheet_to_json(worksheet, {
+          header: 1,
+        }) as any[][];
+        resolve(jsonData.slice(0, 100));
+      } catch (error) {
+        reject(error);
+      }
+    };
+    reader.onerror = (error) => {
+      reject(error);
+    };
+
+    reader.readAsArrayBuffer(file);
+  });
+}
+
+export function htmlToExcel(htmlContent: string) {
+  const parser = new DOMParser();
+  const doc = parser.parseFromString(htmlContent, "text/html");
+  const table = doc.querySelector("table");
+  if (!table) {
+    return [];
+  }
+  const rows = table.querySelectorAll("tr");
+  const tableData: string[][] = [];
+  // 提取表格数据(包含表头)
+  rows.forEach((row) => {
+    const cells = row.querySelectorAll("td, th");
+    const rowData: string[] = [];
+    cells.forEach((cell) => {
+      rowData.push(cell.textContent?.trim() || "");
+    });
+    tableData.push(rowData);
+  });
+  // 使用二维数组直接创建工作表
+  const ws = XLSX.utils.aoa_to_sheet(tableData);
+  // 创建工作簿并保存
+  const wb = XLSX.utils.book_new();
+  XLSX.utils.book_append_sheet(wb, ws, "Sheet1");
+  XLSX.writeFile(wb, "export.xlsx");
+}
diff --git a/app/utils/fileExport/toPdf.ts b/app/utils/fileExport/toPdf.ts
new file mode 100644
index 0000000..3254d4a
--- /dev/null
+++ b/app/utils/fileExport/toPdf.ts
@@ -0,0 +1,166 @@
+import html2canvas from "html2canvas";
+import jsPDF from "jspdf";
+import { pdfToText } from "pdf-ts";
+
+export async function htmlToPdf2(htmlCode: string) {
+  const container = document.createElement("div");
+  container.style.cssText = `
+    position: fixed;
+    left: -9999px;
+    top: -9999px;
+    width: 210mm; // 固定 A4 宽度
+    background: white;
+  `;
+  container.innerHTML = htmlCode;
+  document.body.appendChild(container);
+
+  try {
+    // 关键修改 1: 确保所有图片加载完成
+    await waitForImages(container);
+
+    // 关键修改 2: 强制触发布局计算
+    void container.offsetHeight;
+
+    const canvas = await html2canvas(container, {
+      scale: 2,
+      useCORS: true,
+      logging: true,
+      scrollY: -window.scrollY, // 消除滚动偏移
+      windowWidth: container.scrollWidth,
+      windowHeight: container.scrollHeight,
+    });
+
+    const pdf = new jsPDF("p", "mm", "a4");
+    const pageWidth = pdf.internal.pageSize.getWidth();
+    const imgRatio = canvas.width / canvas.height;
+
+    // 关键修改 3: 动态计算高度
+    const imgHeight = (pageWidth * canvas.height) / canvas.width;
+    pdf.addImage(
+      canvas.toDataURL("image/png"),
+      "PNG",
+      0,
+      0,
+      pageWidth,
+      imgHeight,
+    );
+
+    // 关键修改 4: 处理多页内容
+    if (imgHeight > pdf.internal.pageSize.getHeight()) {
+      pdf.addPage();
+      pdf.addImage(
+        canvas.toDataURL("image/png"),
+        "PNG",
+        0,
+        -pdf.internal.pageSize.getHeight(),
+        pageWidth,
+        imgHeight,
+      );
+    }
+
+    pdf.save("document.pdf");
+  } catch (error) {
+    console.error("生成失败:", error);
+  } finally {
+    document.body.removeChild(container);
+  }
+}
+
+// 图片加载等待函数
+const waitForImages = (element: HTMLElement) => {
+  return new Promise<void>((resolve) => {
+    const images = element.getElementsByTagName("img");
+    let loaded = 0;
+
+    const checkDone = () => {
+      if (loaded >= images.length) resolve();
+    };
+
+    if (images.length === 0) return resolve();
+
+    Array.from(images).forEach((img) => {
+      if (img.complete) {
+        loaded++;
+        checkDone();
+      } else {
+        img.onload = () => {
+          loaded++;
+          checkDone();
+        };
+        img.onerror = checkDone;
+      }
+    });
+  });
+};
+
+export function htmlToPdf(htmlCode: string) {
+  // 1. 动态创建一个 div 元素,并配置为隐藏(display: none)
+  const container = document.createElement("div");
+  container.innerHTML = htmlCode;
+  container.style.display = "none";
+  document.body.appendChild(container);
+  // 2. 为了让 html2canvas 能捕获内容,将 display 修改为 block,并移至视野之外
+  container.style.display = "block";
+  container.style.position = "absolute";
+  container.style.top = "-9999px";
+  container.style.left = "-9999px";
+  window.pageYOffset = 0;
+  document.documentElement.scrollTop = 0;
+  document.body.scrollTop = 0;
+  setTimeout(() => {
+    html2canvas(container, {
+      allowTaint: true,
+      useCORS: true,
+      scale: 2, // 提升画面质量,但是会增加文件大小
+      height: container.scrollHeight, // 需要注意,element的 高度 宽度一定要在这里定义一下,不然会存在只下载了当前你能看到的页面   避雷避雷!!!
+      windowHeight: container.scrollHeight,
+    }).then(function (canvas) {
+      var contentWidth = canvas.width;
+      var contentHeight = canvas.height;
+      // console.log('contentWidth', contentWidth)
+      // console.log('contentHeight', contentHeight)
+      // 一页pdf显示html页面生成的canvas高度;
+      var pageHeight = (contentWidth * 841.89) / 592.28;
+      // 未生成pdf的html页面高度
+      var leftHeight = contentHeight;
+      // console.log('pageHeight', pageHeight)
+      // console.log('leftHeight', leftHeight)
+      // 页面偏移
+      var position = 0;
+      // a4纸的尺寸[595.28,841.89],html页面生成的canvas在pdf中图片的宽高  //40是左右页边距
+      var imgWidth = 595.28 - 40;
+      var imgHeight = (592.28 / contentWidth) * contentHeight;
+      var pageData = canvas.toDataURL("image/jpeg", 1.0);
+      var pdf = new jsPDF("p", "pt", "a4");
+      // 有两个高度需要区分,一个是html页面的实际高度,和生成pdf的页面高度(841.89)
+      // 当内容未超过pdf一页显示的范围,无需分页
+      if (leftHeight < pageHeight) {
+        // console.log('没超过1页')
+        pdf.addImage(pageData, "JPEG", 20, 20, imgWidth, imgHeight);
+      } else {
+        while (leftHeight > 0) {
+          // console.log('超过1页')
+          pdf.addImage(pageData, "JPEG", 20, position, imgWidth, imgHeight);
+          leftHeight -= pageHeight;
+          position -= 841.89;
+          // 避免添加空白页
+          if (leftHeight > 0) {
+            pdf.addPage();
+          }
+        }
+      }
+      pdf.save("out.pdf");
+    });
+  }, 1000);
+}
+
+export async function getPdfData(file: File) {
+  try {
+    const arrayBuffer = await file.arrayBuffer();
+    const text = await pdfToText(new Uint8Array(arrayBuffer));
+    return text;
+  } catch (error) {
+    console.error("Error extracting PDF content:", error);
+    throw error;
+  }
+}
diff --git a/app/utils/fileExport/word.ts b/app/utils/fileExport/word.ts
new file mode 100644
index 0000000..c287af7
--- /dev/null
+++ b/app/utils/fileExport/word.ts
@@ -0,0 +1,91 @@
+import { Document, Packer, Paragraph } from "docx";
+import { saveAs } from "file-saver";
+import * as mammoth from "mammoth";
+
+export function exportWord(content: string) {
+  console.log(content);
+  // 简单类型检测示例
+  const isHTML = (text: string): boolean => {
+    return (
+      /<html[\s>]/.test(text) &&
+      /<\/html>/.test(text) &&
+      /<head>/.test(text) &&
+      /<body>/.test(text)
+    );
+  };
+  if (isHTML(content)) {
+    exportHtmlToWord(content);
+  } else {
+    exportMarkdownToWord(content);
+  }
+}
+
+export function exportHtmlToWord(content: string) {
+  let cleanedContent = content.startsWith("```html")
+    ? content.substring(8)
+    : content;
+  if (cleanedContent.endsWith("```")) {
+    cleanedContent = cleanedContent.substring(0, cleanedContent.length - 4);
+  }
+  const blob = new Blob([cleanedContent], { type: "application/msword" });
+  const url = URL.createObjectURL(blob);
+  const a = document.createElement("a");
+  a.href = url;
+  a.download = "htmldemo.docx";
+  // 触发点击事件,开始下载
+  document.body.appendChild(a);
+  a.click();
+  // 下载完成后移除临时链接元素
+  document.body.removeChild(a);
+  // 释放 Blob URL 对象
+  URL.revokeObjectURL(url);
+}
+
+function exportMarkdownToWord(content: string) {
+  // 按换行符拆分内容
+  const lines = content.split(/\r?\n/);
+  const paragraphs: Paragraph[] = [];
+  for (const line of lines) {
+    // 去除 Markdown 标记(#、*等)
+    const cleanedLine = line
+      .replace(/^#+\s*/, "")
+      .replace(/^\*\*\s*|\*\s*/g, "")
+      .trim();
+    // 处理空行
+    if (cleanedLine === "") {
+      paragraphs.push(new Paragraph(""));
+      continue;
+    }
+    // 添加文本段落
+    paragraphs.push(new Paragraph(cleanedLine));
+  }
+
+  // 创建 Word 文档对象
+  const doc = new Document({
+    sections: [
+      {
+        children: paragraphs,
+      },
+    ],
+  });
+
+  // 转换为 Blob 并下载
+  Packer.toBlob(doc)
+    .then((blob) => {
+      saveAs(blob, "demo.docx");
+    })
+    .catch((error) => {
+      console.error("导出 Word 失败:", error);
+    });
+}
+
+export async function getWordData(file: File) {
+  try {
+    const arrayBuffer = await file.arrayBuffer();
+    const { value, messages } = await mammoth.extractRawText({ arrayBuffer });
+    return value;
+  } catch (error) {
+    console.error("Error extracting Word content:", error);
+    throw error;
+  }
+}
diff --git a/app/utils/prompt.ts b/app/utils/prompt.ts
index 970970e..22be8d0 100644
--- a/app/utils/prompt.ts
+++ b/app/utils/prompt.ts
@@ -1,35 +1,48 @@
 import type { writePromptParam } from "@/app/types/prompt";
 
-export function getWrtingPrompt(param: writePromptParam) {
-  let writingPurposeName;
-  switch (param.writingPurposeName) {
-    case "公司官网":
-      writingPurposeName = "公司官网的介绍";
-      break;
-    case "小红书":
-      writingPurposeName = "小红书的介绍";
-      break;
-    case "微信":
-      writingPurposeName = "微信的介绍";
-      break;
-    case "公众号":
-      writingPurposeName = "公众号的介绍";
-      break;
-    case "今日头条":
-      writingPurposeName = "今日头条的介绍";
-      break;
-  }
-  const isImg = `文案要配上图片,实现图文混排,要美观,要符合${writingPurposeName}的排版标准和写作风格,写作风格要${param.writingStyleName},
+export function getWrtingPrompt(param: writePromptParam): string {
+  const {
+    isImgName,
+    writingPurposeName: rawPurpose,
+    writingStyleName: rawStyle,
+    writingTypeName: rawType,
+    writingLanguageName,
+    prompt,
+    writingCount,
+  } = param;
+
+  const purposeMap: Record<string, string> = {
+    公司官网: "公司官网的介绍",
+    小红书: "小红书的介绍",
+    微信: "微信的介绍",
+    公众号: "公众号的介绍",
+    今日头条: "今日头条的介绍",
+  };
+  const styleMap: Record<string, string> = {
+    专业: "专业的风格",
+    活泼: "活泼的风格",
+    严谨: "严谨的风格",
+  };
+  const typeMap: Record<string, string> = {
+    产品推广文案: "产品推广的文案",
+    品牌宣传文案: "品牌宣传的文案",
+    产品说明书: "产品的说明书",
+    产品介绍: "产品的介绍",
+  };
+
+  let isImg = "";
+  if (isImgName === "是") {
+    const purpose = purposeMap[rawPurpose] || "公司官网的介绍";
+    const style = styleMap[rawStyle] || "专业的风格";
+
+    isImg = `文案要配上图片,实现图文混排,要美观,要符合${purpose}的排版标准和写作风格,写作风格要${style},
             你没有图片没关系,把图文混排的效果实现,并在你认为要插入图片的地方将图片的Prompt用英文输出给:![description](https://image.pollinations.ai/prompt/description?nologo=true),记得图片地址后面的?nologo=true一定不能去掉了,
             因为这个语法可以自动按照提示生成并渲染图片。你可以帮我大幅提高生成图片质量和丰富程度,比如增加相机光圈、具体场景描述等内容,注意图片一定要用<img,否则在HTML下图片可能显示不了`;
+  }
+  const writingTypeName = typeMap[rawType] || "产品推广文案";
 
-  const input = `帮我使用${param.writingLanguageName}写一篇主题是${
-    param.prompt
-  }的${param.writingTypeName},
-              ${param.isImgName === "是" ? isImg : ""}
-              ,字数要求不少于${
-                param.writingCount
-              }字,字数不包括html代码和图片Prompt。输出成标准的html,直接给结果,不要做任何解释`;
+  const input = `帮我使用${writingLanguageName}写一篇主题是${prompt}的${writingTypeName},${isImg},字数要求不少于${writingCount}字,
+    字数不包括html代码和图片Prompt。输出成标准的html并且样式必须为内联样式,直接给结果,不要做任何解释`;
   return input;
 }
 
diff --git a/package.json b/package.json
index a843e5a..1fa9808 100644
--- a/package.json
+++ b/package.json
@@ -43,9 +43,12 @@
     "fuse.js": "^7.0.0",
     "heic2any": "^0.0.4",
     "html-to-image": "^1.11.11",
+    "html2canvas": "^1.4.1",
     "idb-keyval": "^6.2.1",
+    "jspdf": "^3.0.1",
     "lodash-es": "^4.17.21",
     "lucide-react": "^0.484.0",
+    "mammoth": "^1.9.0",
     "markdown-to-txt": "^2.0.1",
     "mermaid": "^10.6.1",
     "mind-elixir": "^4.5.0",
@@ -53,6 +56,7 @@
     "next": "^14.1.1",
     "node-fetch": "^3.3.1",
     "openapi-client-axios": "^7.5.5",
+    "pdf-ts": "^0.0.2",
     "rc-tooltip": "^6.4.0",
     "react": "^18.2.0",
     "react-dom": "^18.2.0",
diff --git a/yarn.lock b/yarn.lock
index c2b1a86..21d8422 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1271,6 +1271,13 @@
   dependencies:
     regenerator-runtime "^0.14.0"
 
+"@babel/runtime@^7.26.7":
+  version "7.27.0"
+  resolved "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.27.0.tgz#fbee7cf97c709518ecc1f590984481d5460d4762"
+  integrity sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==
+  dependencies:
+    regenerator-runtime "^0.14.0"
+
 "@babel/template@^7.18.10", "@babel/template@^7.20.7":
   version "7.20.7"
   resolved "https://registry.npmjs.org/@babel/template/-/template-7.20.7.tgz"
@@ -2626,6 +2633,11 @@
   dependencies:
     parchment "^1.1.2"
 
+"@types/raf@^3.4.0":
+  version "3.4.3"
+  resolved "https://registry.npmmirror.com/@types/raf/-/raf-3.4.3.tgz#85f1d1d17569b28b8db45e16e996407a56b0ab04"
+  integrity sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==
+
 "@types/react-dom@^18.2.7":
   version "18.2.7"
   resolved "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.7.tgz"
@@ -2669,6 +2681,11 @@
   resolved "https://registry.npmmirror.com/@types/tough-cookie/-/tough-cookie-4.0.5.tgz"
   integrity sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==
 
+"@types/trusted-types@^2.0.7":
+  version "2.0.7"
+  resolved "https://registry.npmmirror.com/@types/trusted-types/-/trusted-types-2.0.7.tgz#baccb07a970b91707df3a3e8ba6896c57ead2d11"
+  integrity sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==
+
 "@types/unist@*", "@types/unist@^2.0.0":
   version "2.0.6"
   resolved "https://registry.npmjs.org/@types/unist/-/unist-2.0.6.tgz"
@@ -2898,6 +2915,11 @@
     "@webassemblyjs/wast-parser" "1.9.0"
     "@xtuc/long" "4.2.2"
 
+"@xmldom/xmldom@^0.8.6":
+  version "0.8.10"
+  resolved "https://registry.npmmirror.com/@xmldom/xmldom/-/xmldom-0.8.10.tgz#a1337ca426aa61cef9fe15b5b28e340a72f6fa99"
+  integrity sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==
+
 "@xmldom/xmldom@^0.9.7":
   version "0.9.8"
   resolved "https://registry.npmmirror.com/@xmldom/xmldom/-/xmldom-0.9.8.tgz"
@@ -2985,12 +3007,12 @@ aggregate-error@^3.0.0:
     clean-stack "^2.0.0"
     indent-string "^4.0.0"
 
-ajv-keywords@^3.5.2:
+ajv-keywords@^3.1.0, ajv-keywords@^3.5.2:
   version "3.5.2"
   resolved "https://registry.npmmirror.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz"
   integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==
 
-ajv@^6.12.4, ajv@^6.12.5:
+ajv@^6.1.0, ajv@^6.12.4, ajv@^6.12.5:
   version "6.12.6"
   resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz"
   integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==
@@ -3109,7 +3131,7 @@ arg@^4.1.0:
   resolved "https://registry.npmmirror.com/arg/-/arg-4.1.3.tgz"
   integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==
 
-argparse@^1.0.7:
+argparse@^1.0.7, argparse@~1.0.3:
   version "1.0.10"
   resolved "https://registry.npmmirror.com/argparse/-/argparse-1.0.10.tgz"
   integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==
@@ -3198,6 +3220,11 @@ asynckit@^0.4.0:
   resolved "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz"
   integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
 
+atob@^2.1.2:
+  version "2.1.2"
+  resolved "https://registry.npmmirror.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9"
+  integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==
+
 available-typed-arrays@^1.0.5:
   version "1.0.5"
   resolved "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz"
@@ -3318,16 +3345,36 @@ balanced-match@^1.0.0:
   resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz"
   integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
 
+base64-arraybuffer@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.npmmirror.com/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz#1c37589a7c4b0746e34bd1feb951da2df01c1bdc"
+  integrity sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==
+
+base64-js@^1.5.1:
+  version "1.5.1"
+  resolved "https://registry.npmmirror.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
+  integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
+
 bath-es5@^3.0.3:
   version "3.0.3"
   resolved "https://registry.npmjs.org/bath-es5/-/bath-es5-3.0.3.tgz"
   integrity sha512-PdCioDToH3t84lP40kUFCKWCOCH389Dl1kbC8FGoqOwamxsmqxxnJSXdkTOsPoNHXjem4+sJ+bbNoQm5zeCqxg==
 
+big.js@^5.2.2:
+  version "5.2.2"
+  resolved "https://registry.npmmirror.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328"
+  integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==
+
 binary-extensions@^2.0.0:
   version "2.2.0"
   resolved "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz"
   integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==
 
+bluebird@~3.4.0:
+  version "3.4.7"
+  resolved "https://registry.npmmirror.com/bluebird/-/bluebird-3.4.7.tgz#f72d760be09b7f76d08ed8fae98b289a8d05fab3"
+  integrity sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==
+
 boolbase@^1.0.0:
   version "1.0.0"
   resolved "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz"
@@ -3365,6 +3412,11 @@ bser@2.1.1:
   dependencies:
     node-int64 "^0.4.0"
 
+btoa@^1.2.1:
+  version "1.2.1"
+  resolved "https://registry.npmmirror.com/btoa/-/btoa-1.2.1.tgz#01a9909f8b2c93f6bf680ba26131eb30f7fa3d73"
+  integrity sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==
+
 buffer-from@^1.0.0:
   version "1.1.2"
   resolved "https://registry.npmmirror.com/buffer-from/-/buffer-from-1.1.2.tgz"
@@ -3444,6 +3496,20 @@ caniuse-lite@^1.0.30001579, caniuse-lite@^1.0.30001688:
   resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001692.tgz"
   integrity sha512-A95VKan0kdtrsnMubMKxEKUKImOPSuCpYgxSQBo036P5YYgVIcOYJEgt/txJWqObiRQeISNCfef9nvlQ0vbV7A==
 
+canvg@^3.0.11:
+  version "3.0.11"
+  resolved "https://registry.npmmirror.com/canvg/-/canvg-3.0.11.tgz#4b4290a6c7fa36871fac2b14e432eff33b33cf2b"
+  integrity sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==
+  dependencies:
+    "@babel/runtime" "^7.12.5"
+    "@types/raf" "^3.4.0"
+    core-js "^3.8.3"
+    raf "^3.4.1"
+    regenerator-runtime "^0.13.7"
+    rgbcolor "^1.0.1"
+    stackblur-canvas "^2.0.0"
+    svg-pathdata "^6.0.3"
+
 ccount@^2.0.0:
   version "2.0.1"
   resolved "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz"
@@ -3758,6 +3824,11 @@ core-js-compat@^3.25.1:
   dependencies:
     browserslist "^4.21.5"
 
+core-js@^3.6.0, core-js@^3.8.3:
+  version "3.41.0"
+  resolved "https://registry.npmmirror.com/core-js/-/core-js-3.41.0.tgz#57714dafb8c751a6095d028a7428f1fb5834a776"
+  integrity sha512-SJ4/EHwS36QMJd6h/Rg+GyR4A5xE0FSI3eZ+iBVpfqf1x0eTSg1smWLHrA+2jQThZSh97fmSgFSU8B61nxosxA==
+
 core-util-is@~1.0.0:
   version "1.0.3"
   resolved "https://registry.npmmirror.com/core-util-is/-/core-util-is-1.0.3.tgz"
@@ -3834,6 +3905,13 @@ css-box-model@^1.2.1:
   dependencies:
     tiny-invariant "^1.0.6"
 
+css-line-break@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.npmmirror.com/css-line-break/-/css-line-break-2.1.0.tgz#bfef660dfa6f5397ea54116bb3cb4873edbc4fa0"
+  integrity sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==
+  dependencies:
+    utrie "^1.0.2"
+
 css-select@^4.1.3:
   version "4.3.0"
   resolved "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz"
@@ -4388,6 +4466,11 @@ diff@^5.0.0:
   resolved "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz"
   integrity sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==
 
+dingbat-to-unicode@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.npmmirror.com/dingbat-to-unicode/-/dingbat-to-unicode-1.0.1.tgz#5091dd673241453e6b5865e26e5a4452cdef5c83"
+  integrity sha512-98l0sW87ZT58pU4i61wa2OHwxbiYSbuxsCBozaVnYX2iCnr3bLM3fIes1/ej7h1YdOKuKt/MLs706TVnALA65w==
+
 dir-glob@^3.0.1:
   version "3.0.1"
   resolved "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz"
@@ -4487,6 +4570,13 @@ dompurify@^3.0.5:
   resolved "https://registry.npmjs.org/dompurify/-/dompurify-3.0.5.tgz"
   integrity sha512-F9e6wPGtY+8KNMRAVfxeCOHU0/NPWMSENNq4pQctuXRqqdEPW7q3CrLbR5Nse044WwacyjHGOMlvNsBe1y6z9A==
 
+dompurify@^3.2.4:
+  version "3.2.4"
+  resolved "https://registry.npmmirror.com/dompurify/-/dompurify-3.2.4.tgz#af5a5a11407524431456cf18836c55d13441cd8e"
+  integrity sha512-ysFSFEDVduQpyhzAob/kkuJjf5zWkZD8/A9ywSp1byueyuCfHamrCBa14/Oc2iiB0e51B+NpxSl5gmzn+Ms/mg==
+  optionalDependencies:
+    "@types/trusted-types" "^2.0.7"
+
 domutils@^2.8.0:
   version "2.8.0"
   resolved "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz"
@@ -4505,6 +4595,13 @@ domutils@^3.0.1, domutils@^3.1.0:
     domelementtype "^2.3.0"
     domhandler "^5.0.3"
 
+duck@^0.1.12:
+  version "0.1.12"
+  resolved "https://registry.npmmirror.com/duck/-/duck-0.1.12.tgz#de7adf758421230b6d7aee799ce42670586b9efa"
+  integrity sha512-wkctla1O6VfP89gQ+J/yDesM0S7B7XLXjKGzXxMDVFg7uEn706niAtyYovKbyq1oT9YwDcly721/iUWoc8MVRg==
+  dependencies:
+    underscore "^1.13.1"
+
 dunder-proto@^1.0.1:
   version "1.0.1"
   resolved "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz"
@@ -4559,6 +4656,11 @@ emoji-regex@^9.2.2:
   resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz"
   integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==
 
+emojis-list@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.npmmirror.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78"
+  integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==
+
 encoding-sniffer@^0.2.0:
   version "0.2.0"
   resolved "https://registry.npmmirror.com/encoding-sniffer/-/encoding-sniffer-0.2.0.tgz"
@@ -5132,6 +5234,11 @@ fetch-blob@^3.1.2, fetch-blob@^3.1.4:
     node-domexception "^1.0.0"
     web-streams-polyfill "^3.0.3"
 
+fflate@^0.8.1:
+  version "0.8.2"
+  resolved "https://registry.npmmirror.com/fflate/-/fflate-0.8.2.tgz#fc8631f5347812ad6028bbe4a2308b2792aa1dea"
+  integrity sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==
+
 file-entry-cache@^6.0.1:
   version "6.0.1"
   resolved "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz"
@@ -5626,6 +5733,14 @@ html-to-image@^1.11.11:
   resolved "https://registry.npmmirror.com/html-to-image/-/html-to-image-1.11.11.tgz"
   integrity sha512-9gux8QhvjRO/erSnDPv28noDZcPZmYE7e1vFsBLKLlRlKDSqNJYebj6Qz1TGd5lsRV+X+xYyjCKjuZdABinWjA==
 
+html2canvas@^1.0.0-rc.5, html2canvas@^1.4.1:
+  version "1.4.1"
+  resolved "https://registry.npmmirror.com/html2canvas/-/html2canvas-1.4.1.tgz#7cef1888311b5011d507794a066041b14669a543"
+  integrity sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==
+  dependencies:
+    css-line-break "^2.1.0"
+    text-segmentation "^1.0.3"
+
 htmlparser2@^9.1.0:
   version "9.1.0"
   resolved "https://registry.npmmirror.com/htmlparser2/-/htmlparser2-9.1.0.tgz"
@@ -6531,7 +6646,7 @@ json2mq@^0.2.0:
   dependencies:
     string-convert "^0.2.0"
 
-json5@^1.0.2:
+json5@^1.0.1, json5@^1.0.2:
   version "1.0.2"
   resolved "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz"
   integrity sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==
@@ -6543,6 +6658,21 @@ json5@^2.2.2, json5@^2.2.3:
   resolved "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz"
   integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==
 
+jspdf@^3.0.1:
+  version "3.0.1"
+  resolved "https://registry.npmmirror.com/jspdf/-/jspdf-3.0.1.tgz#d81e1964f354f60412516eb2449ea2cccd4d2a3b"
+  integrity sha512-qaGIxqxetdoNnFQQXxTKUD9/Z7AloLaw94fFsOiJMxbfYdBbrBuhWmbzI8TVjrw7s3jBY1PFHofBKMV/wZPapg==
+  dependencies:
+    "@babel/runtime" "^7.26.7"
+    atob "^2.1.2"
+    btoa "^1.2.1"
+    fflate "^0.8.1"
+  optionalDependencies:
+    canvg "^3.0.11"
+    core-js "^3.6.0"
+    dompurify "^3.2.4"
+    html2canvas "^1.0.0-rc.5"
+
 "jsx-ast-utils@^2.4.1 || ^3.0.0", jsx-ast-utils@^3.3.3:
   version "3.3.3"
   resolved "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.3.tgz"
@@ -6551,7 +6681,7 @@ json5@^2.2.2, json5@^2.2.3:
     array-includes "^3.1.5"
     object.assign "^4.1.3"
 
-jszip@*, jszip@^3.10.1:
+jszip@*, jszip@^3.10.1, jszip@^3.7.1:
   version "3.10.1"
   resolved "https://registry.npmmirror.com/jszip/-/jszip-3.10.1.tgz"
   integrity sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==
@@ -6680,6 +6810,15 @@ loader-runner@^4.1.0:
   resolved "https://registry.npmmirror.com/loader-runner/-/loader-runner-4.3.0.tgz"
   integrity sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==
 
+loader-utils@^1.0.0:
+  version "1.4.2"
+  resolved "https://registry.npmmirror.com/loader-utils/-/loader-utils-1.4.2.tgz#29a957f3a63973883eb684f10ffd3d151fec01a3"
+  integrity sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==
+  dependencies:
+    big.js "^5.2.2"
+    emojis-list "^3.0.0"
+    json5 "^1.0.1"
+
 locate-path@^5.0.0:
   version "5.0.0"
   resolved "https://registry.npmmirror.com/locate-path/-/locate-path-5.0.0.tgz"
@@ -6746,6 +6885,15 @@ loose-envify@^1.1.0, loose-envify@^1.4.0:
   dependencies:
     js-tokens "^3.0.0 || ^4.0.0"
 
+lop@^0.4.2:
+  version "0.4.2"
+  resolved "https://registry.npmmirror.com/lop/-/lop-0.4.2.tgz#c9c2f958a39b9da1c2f36ca9ad66891a9fe84640"
+  integrity sha512-RefILVDQ4DKoRZsJ4Pj22TxE3omDO47yFpkIBoDKzkqPRISs5U1cnAdg/5583YPkWPaLIYHOKRMQSvjFsO26cw==
+  dependencies:
+    duck "^0.1.12"
+    option "~0.2.1"
+    underscore "^1.13.1"
+
 lowlight@^2.0.0:
   version "2.8.1"
   resolved "https://registry.npmjs.org/lowlight/-/lowlight-2.8.1.tgz"
@@ -6805,6 +6953,22 @@ makeerror@1.0.12:
   dependencies:
     tmpl "1.0.5"
 
+mammoth@^1.9.0:
+  version "1.9.0"
+  resolved "https://registry.npmmirror.com/mammoth/-/mammoth-1.9.0.tgz#71e34ca280735275788bfe95e653a058dcab4df2"
+  integrity sha512-F+0NxzankQV9XSUAuVKvkdQK0GbtGGuqVnND9aVf9VSeUA82LQa29GjLqYU6Eez8LHqSJG3eGiDW3224OKdpZg==
+  dependencies:
+    "@xmldom/xmldom" "^0.8.6"
+    argparse "~1.0.3"
+    base64-js "^1.5.1"
+    bluebird "~3.4.0"
+    dingbat-to-unicode "^1.0.1"
+    jszip "^3.7.1"
+    lop "^0.4.2"
+    path-is-absolute "^1.0.0"
+    underscore "^1.13.1"
+    xmlbuilder "^10.0.0"
+
 markdown-table@^3.0.0:
   version "3.0.3"
   resolved "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.3.tgz"
@@ -7481,6 +7645,11 @@ node-domexception@^1.0.0:
   resolved "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz"
   integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==
 
+node-ensure@^0.0.0:
+  version "0.0.0"
+  resolved "https://registry.npmmirror.com/node-ensure/-/node-ensure-0.0.0.tgz#ecae764150de99861ec5c810fd5d096b183932a7"
+  integrity sha512-DRI60hzo2oKN1ma0ckc6nQWlHU69RH6xN0sjQTjMpChPfTYvKZdcQFfdYK2RWbJcKyUizSIy/l8OTGxMAM1QDw==
+
 node-fetch@^3.3.1:
   version "3.3.1"
   resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.1.tgz"
@@ -7639,6 +7808,11 @@ openapi-types@^12.1.3:
   resolved "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz"
   integrity sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==
 
+option@~0.2.1:
+  version "0.2.4"
+  resolved "https://registry.npmmirror.com/option/-/option-0.2.4.tgz#fd475cdf98dcabb3cb397a3ba5284feb45edbfe4"
+  integrity sha512-pkEqbDyl8ou5cpq+VsnQbe/WlEy5qS7xPzMS1U55OCG9KPvwFD46zDbxQIj3egJSFc3D+XhYOPUzz49zQAVy7A==
+
 optionator@^0.9.3:
   version "0.9.3"
   resolved "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz"
@@ -7770,6 +7944,26 @@ path-type@^4.0.0:
   resolved "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz"
   integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
 
+pdf-ts@^0.0.2:
+  version "0.0.2"
+  resolved "https://registry.npmmirror.com/pdf-ts/-/pdf-ts-0.0.2.tgz#27fc7842f998fdd679ae1aa2b95e42b8e0736758"
+  integrity sha512-t9VmdLA+8dvX9t3XulCD1hIEWi0N94p2WpfTPwDcvYCW/NElaK+abHj4q5F4XhJcnxzm6dzlileyaH7qcIbnmQ==
+  dependencies:
+    pdfjs-dist "1.10.100"
+
+pdfjs-dist@1.10.100:
+  version "1.10.100"
+  resolved "https://registry.npmmirror.com/pdfjs-dist/-/pdfjs-dist-1.10.100.tgz#d5a250b42482ab6e41d763a795ce7cdebe6b1894"
+  integrity sha512-aCfONGqlBeazYxik3rjd7xaoCKMRYECwZSCC3EC3weqibF2V1Bp/v9WZbF7Lyy5Q6UE4NqOYu126r7U+Le4Uhg==
+  dependencies:
+    node-ensure "^0.0.0"
+    worker-loader "^1.0.0"
+
+performance-now@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.npmmirror.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
+  integrity sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==
+
 picocolors@^1.0.0, picocolors@^1.1.1:
   version "1.1.1"
   resolved "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz"
@@ -7943,6 +8137,13 @@ raf-schd@^4.0.3:
   resolved "https://registry.npmmirror.com/raf-schd/-/raf-schd-4.0.3.tgz"
   integrity sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==
 
+raf@^3.4.1:
+  version "3.4.1"
+  resolved "https://registry.npmmirror.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39"
+  integrity sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==
+  dependencies:
+    performance-now "^2.1.0"
+
 randombytes@^2.1.0:
   version "2.1.0"
   resolved "https://registry.npmmirror.com/randombytes/-/randombytes-2.1.0.tgz"
@@ -8445,6 +8646,11 @@ regenerate@^1.4.2:
   resolved "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz"
   integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==
 
+regenerator-runtime@^0.13.7:
+  version "0.13.11"
+  resolved "https://registry.npmmirror.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9"
+  integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==
+
 regenerator-runtime@^0.14.0:
   version "0.14.1"
   resolved "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz"
@@ -8646,6 +8852,11 @@ rfdc@^1.3.0:
   resolved "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz"
   integrity sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==
 
+rgbcolor@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.npmmirror.com/rgbcolor/-/rgbcolor-1.0.1.tgz#d6505ecdb304a6595da26fa4b43307306775945d"
+  integrity sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==
+
 rimraf@^3.0.2:
   version "3.0.2"
   resolved "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz"
@@ -8745,6 +8956,14 @@ scheduler@^0.23.0:
   dependencies:
     loose-envify "^1.1.0"
 
+schema-utils@^0.4.0:
+  version "0.4.7"
+  resolved "https://registry.npmmirror.com/schema-utils/-/schema-utils-0.4.7.tgz#ba74f597d2be2ea880131746ee17d0a093c68187"
+  integrity sha512-v/iwU6wvwGK8HbU9yi3/nhGzP0yGSuhQMzL6ySiec1FSrZZDkhm4noOSWzrNFo/jEc+SJY6jRTwuwbSXJPDUnQ==
+  dependencies:
+    ajv "^6.1.0"
+    ajv-keywords "^3.1.0"
+
 schema-utils@^3.0.0:
   version "3.3.0"
   resolved "https://registry.npmmirror.com/schema-utils/-/schema-utils-3.3.0.tgz"
@@ -8976,6 +9195,11 @@ stack-utils@^2.0.3:
   dependencies:
     escape-string-regexp "^2.0.0"
 
+stackblur-canvas@^2.0.0:
+  version "2.7.0"
+  resolved "https://registry.npmmirror.com/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz#af931277d0b5096df55e1f91c530043e066989b6"
+  integrity sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==
+
 statuses@2.0.1:
   version "2.0.1"
   resolved "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz"
@@ -9173,6 +9397,11 @@ svg-parser@^2.0.4:
   resolved "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz"
   integrity sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==
 
+svg-pathdata@^6.0.3:
+  version "6.0.3"
+  resolved "https://registry.npmmirror.com/svg-pathdata/-/svg-pathdata-6.0.3.tgz#80b0e0283b652ccbafb69ad4f8f73e8d3fbf2cac"
+  integrity sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==
+
 svgo@^2.8.0:
   version "2.8.0"
   resolved "https://registry.npmjs.org/svgo/-/svgo-2.8.0.tgz"
@@ -9250,6 +9479,13 @@ test-exclude@^6.0.0:
     glob "^7.1.4"
     minimatch "^3.0.4"
 
+text-segmentation@^1.0.3:
+  version "1.0.3"
+  resolved "https://registry.npmmirror.com/text-segmentation/-/text-segmentation-1.0.3.tgz#52a388159efffe746b24a63ba311b6ac9f2d7943"
+  integrity sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==
+  dependencies:
+    utrie "^1.0.2"
+
 text-table@^0.2.0:
   version "0.2.0"
   resolved "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz"
@@ -9444,6 +9680,11 @@ unbox-primitive@^1.0.2:
     has-symbols "^1.0.3"
     which-boxed-primitive "^1.0.2"
 
+underscore@^1.13.1:
+  version "1.13.7"
+  resolved "https://registry.npmmirror.com/underscore/-/underscore-1.13.7.tgz#970e33963af9a7dda228f17ebe8399e5fbe63a10"
+  integrity sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==
+
 undici-types@~6.19.2:
   version "6.19.8"
   resolved "https://registry.npmmirror.com/undici-types/-/undici-types-6.19.8.tgz"
@@ -9621,6 +9862,13 @@ util-deprecate@~1.0.1:
   resolved "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz"
   integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==
 
+utrie@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.npmmirror.com/utrie/-/utrie-1.0.2.tgz#d42fe44de9bc0119c25de7f564a6ed1b2c87a645"
+  integrity sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==
+  dependencies:
+    base64-arraybuffer "^1.0.2"
+
 uuid@^9.0.0:
   version "9.0.0"
   resolved "https://registry.npmmirror.com/uuid/-/uuid-9.0.0.tgz"
@@ -9854,6 +10102,14 @@ word@~0.3.0:
   resolved "https://registry.npmmirror.com/word/-/word-0.3.0.tgz"
   integrity sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==
 
+worker-loader@^1.0.0:
+  version "1.1.1"
+  resolved "https://registry.npmmirror.com/worker-loader/-/worker-loader-1.1.1.tgz#920d74ddac6816fc635392653ed8b4af1929fd92"
+  integrity sha512-qJZLVS/jMCBITDzPo/RuweYSIG8VJP5P67mP/71alGyTZRe1LYJFdwLjLalY3T5ifx0bMDRD3OB6P2p1escvlg==
+  dependencies:
+    loader-utils "^1.0.0"
+    schema-utils "^0.4.0"
+
 wrap-ansi@^6.2.0:
   version "6.2.0"
   resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz"
@@ -9920,6 +10176,11 @@ xml@^1.0.1:
   resolved "https://registry.npmmirror.com/xml/-/xml-1.0.1.tgz"
   integrity sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==
 
+xmlbuilder@^10.0.0:
+  version "10.1.1"
+  resolved "https://registry.npmmirror.com/xmlbuilder/-/xmlbuilder-10.1.1.tgz#8cae6688cc9b38d850b7c8d3c0a4161dcaf475b0"
+  integrity sha512-OyzrcFLL/nb6fMGHbiRDuPup9ljBycsdCypwuyg5AAHvyWzGfChJpCXMG88AGTIMFhGZ9RccFN1e6lhg3hkwKg==
+
 xmlchars@^2.2.0:
   version "2.2.0"
   resolved "https://registry.npmmirror.com/xmlchars/-/xmlchars-2.2.0.tgz"
--
libgit2 0.24.0