From 9ff51ac2f30ec1d9a4bcef7ffc00a33a927a2b94 Mon Sep 17 00:00:00 2001 From: ydy0615 Date: Sat, 4 Apr 2026 23:56:18 +0800 Subject: [PATCH] =?UTF-8?q?feat(plugin):=20add=20document=20export,=20doc?= =?UTF-8?q?=E2=80=91block,=20and=20TTS/ASR=20support?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a DocBlock component that renders embedded documents, new export buttons for DOCX and PDF, and updates the file‑upload picker to accept *.txt, *.docx, *.pptx, and *.pdf. Introduces a DOCX→PDF conversion bridge in the backend and new /tts and /asr endpoints that expose TTS and speech‑recognition functionality. The README is rewritten to describe the new features and clean up legacy documentation. All changes are backward‑compatible and do not introduce breaking API changes. --- .kilo/plans/1775304798427-lucky-panda.md | 52 + README.md | 293 +--- README.md.fixed | Bin 10274 -> 0 bytes README.md.original | Bin 10274 -> 0 bytes README.md.tmp | 265 ---- README_clean.md | Bin 10274 -> 0 bytes README_correct.md | Bin 10274 -> 0 bytes README_fixed.md | Bin 10274 -> 0 bytes README_original.txt | 265 ---- backend/docx2pdf_bridge.cjs | 20 + backend/main.py | 126 +- backend/tts_asr.py | 255 ++++ backend/tts_stt.py | 141 -- original_readme.md | Bin 10274 -> 0 bytes package-lock.json | 1543 ++++++++++++++++++++++ package.json | 4 + src/components/DocBlockCrepe.vue | 315 +++-- src/components/MilkdownEditor.vue | 401 ++++-- src/plugins/docBlockPlugin.ts | 249 ++++ src/style.css | 8 +- src/utils/config.js | 1 + src/utils/convert.js | 1 + src/utils/docBlock.js | 180 +++ test.txt | 0 update_readme.py | Bin 2440 -> 0 bytes 25 files changed, 2995 insertions(+), 1124 deletions(-) create mode 100644 .kilo/plans/1775304798427-lucky-panda.md delete mode 100644 README.md.fixed delete mode 100644 README.md.original delete mode 100644 README.md.tmp delete mode 100644 README_clean.md delete mode 100644 README_correct.md delete mode 100644 README_fixed.md delete mode 100644 README_original.txt create mode 100644 backend/docx2pdf_bridge.cjs create mode 100644 backend/tts_asr.py delete mode 100644 backend/tts_stt.py delete mode 100644 original_readme.md create mode 100644 src/plugins/docBlockPlugin.ts create mode 100644 src/utils/docBlock.js delete mode 100644 test.txt delete mode 100644 update_readme.py diff --git a/.kilo/plans/1775304798427-lucky-panda.md b/.kilo/plans/1775304798427-lucky-panda.md new file mode 100644 index 0000000..a86e7fe --- /dev/null +++ b/.kilo/plans/1775304798427-lucky-panda.md @@ -0,0 +1,52 @@ +# 导出按钮缺失修复计划 + +## 问题分析 +当前 `action-buttons` 区域只有以下按钮可见: +- 上传文件 +- 导入 Markdown +- 导出 Markdown +- 上传图片 +- AI 切换按钮 + +**缺失功能**:DOCX 和 PDF 导出按钮 + +## 调查结果 +1. ✅ 翻译文件中已存在 `exportDocx` 和 `exportPdf` 键名(src/utils/i18n.js) +2. ❌ 模板中**完全缺失**这两个按钮的 HTML 代码 +3. ❓ 导出功能后端已实现,前端只需要添加调用接口的按钮 +4. ✅ 相关 CSS 样式已存在,按钮外观无需额外调整 + +## 实施计划 + +### 1. 添加 UI 按钮 +在 `src/components/MilkdownEditor.vue:79` 之后添加两个新按钮: +- DOCX 导出按钮 +- PDF 导出按钮 + +按钮位置: +``` +导出 Markdown → 导出 DOCX → 导出 PDF → 上传图片 +``` + +### 2. 实现前端导出功能 +使用已安装的依赖库: +- `docx` 库:用于 DOCX 导出 +- `html2pdf.js` 库:用于 PDF 导出 + +需要添加的函数: +```javascript +const exportDocx = async () => { + // 使用 docx 库实现导出 +} + +const exportPdf = async () => { + // 使用 html2pdf.js 实现导出 +} +``` + +### 3. 按钮图标 +- DOCX:使用文档图标 +- PDF:使用 PDF 专用图标 + +### 4. 状态管理 +添加加载状态和错误处理,与现有按钮保持一致风格 diff --git a/README.md b/README.md index 405a3f0..c4ed81c 100644 --- a/README.md +++ b/README.md @@ -1,264 +1,93 @@ -# LLM in Text - 智能写作助手 +# LLM in Text - 智能写作助手 -基于 Vue3 和 FastAPI 的智能 Markdown 编辑器,集成大语言模型(LLM)实时补全建议功能,提供类似 GitHub Copilot 的 Ghost Text 体验。 +基于 Vue3 和 FastAPI 的智能 Markdown 编辑器,集成大语言模型(LLM)实时补全建议功能。 ## 功能特性 ### Markdown 编辑器 - 基于 Milkdown Crepe 的所见即所得编辑体验 -- 支持完整 Markdown 语法和 LaTeX 公式 +- 支持 Markdown 语法和 LaTeX 公式 +- 支持 Mermaid 图表渲染 - 导入/导出 Markdown 文件 +- 导出 DOCX 和 PDF 格式 ### AI 智能补全 - 实时生成文本补全建议(灰色显示) - 流式响应,低延迟体验 -- 多种交互方式: - - **Tab 键**:接受建议 - - **Esc 键**:拒绝建议 - - **点击灰色文本**:接受建议 - - **继续输入**:自动拒绝建议 +- 多种交互方式:Tab接受、Esc拒绝、点击接受 -### AI 开关控制 -- 右下角 AI 开关按钮 -- 白色 = AI 启用,黑色 = AI 禁用 -- 禁用时自动清除灰色文本并停止 API 调用 +### 文档处理 +- OCR 图片识别:上传图片自动识别文字 +- 文档转换:PDF、DOCX、PPTX、TXT 转 Markdown +- 文档块嵌入:可折叠的文档预览块 +- 智能大小限制:32KB自动禁用AI + +### 设置面板 +- 外观主题:亮色/暗色/跟随系统 +- 背景模式:默认/暖色/阅读灯/自定义图片 +- 模型智能:低/中/高思考级别 +- 隐私控制:隐私模式防止发送IP +- 多语言界面:中英日韩德法 + +### 语音功能 +- TTS文字转语音(macOS) +- STT语音转文字 ## 技术架构 -```mermaid -flowchart TB - subgraph Frontend["前端 (Vue3 + Vite)"] - A[App.vue] --> B[MilkdownEditor.vue] - B --> C[Crepe Editor] - C --> D[ProseMirror] - D --> E[copilotPlugin.ts] - E --> F[copilotGhostMark] - E --> G[api.js] - end - - subgraph Backend["后端 (FastAPI + Python)"] - H[main.py
FastAPI Server] --> I[prompt.py
Prompt 构建] - H --> J[llm.py
Ollama 调用] - J --> K[Ollama API] - end - - G -->|POST /v1/completions
SSE 流式响应| H - K -->|LLM 响应| J -``` - -## 项目结构 - -``` -llm-in-text/ -├── src/ -│ ├── components/ -│ │ └── MilkdownEditor.vue # 主编辑器组件 -│ ├── plugins/ -│ │ ├── copilotPlugin.ts # ProseMirror AI 补全插件 -│ │ ├── types.ts # 类型定义 -│ │ └── index.ts # 插件导出 -│ ├── utils/ -│ │ ├── api.js # API 调用封装 -│ │ ├── config.js # 配置文件 -│ │ └── ocrCache.js # OCR 缓存管理 -│ ├── App.vue -│ └── main.js -├── backend/ -│ ├── main.py # FastAPI 服务器 -│ ├── llm.py # LLM API 调用 -│ ├── prompt.py # Prompt 构建 -│ └── requirements.txt -└── README.md -``` +前端: Vue3 + Vite + Milkdown + ProseMirror +后端: FastAPI + Python + Ollama ## 快速开始 -### 环境要求 -- Node.js 18+ -- Python 3.8+ -- Ollama 服务(或其他兼容 OpenAI API 的服务) +环境: Node.js 18+、Python 3.8+、Ollama -### 安装 +安装: +- 前端: npm install +- 后端: pip install -r backend/requirements.txt -```bash -# 前端 -npm install +启动: +- 后端: python backend/main.py (端口8001) +- 前端: npm run dev (端口5173) -# 后端 -cd backend -pip install -r requirements.txt -``` +## API接口 -### 配置 - -在 `backend/.env` 中配置: - -```env -OLLAMA_MODEL=gpt-oss:20b -OLLAMA_HOST=http://localhost:11434 -``` - -### 启动 - -```bash -# 后端(端口 8000) -cd backend -python main.py - -# 前端(端口 5173) -npm run dev -``` - -访问 http://localhost:5173 - -## API 接口 - -### POST /v1/completions - -流式获取补全建议 - -**请求:** -```json -{ - "prefix": "# Title\n\nContent ", - "suffix": "", - "languageId": "markdown" -} -``` - -**响应(SSE):** -``` -data: {"content": "here"} -data: {"content": "here is"} -data: {"done": true} -``` +- POST /v1/completions 流式补全建议 +- POST /v1/ocr 图片文字识别 +- POST /v1/convert 文档转换 +- POST /v1/completions/cancel 取消请求 ## 核心实现 -### 后端设计 +### 后端 +- main.py: FastAPI服务器、SSE流式响应 +- llm.py: 异步Ollama调用、超时控制 +- prompt.py: 7条Prompt规则 +- tts_asr.py: macOS 语音处理 -#### main.py - FastAPI 服务器 -- 定义 `/v1/completions` 端点 -- 使用 `StreamingResponse` 返回 SSE 流式响应 -- CORS 配置允许跨域请求 - -#### llm.py - LLM 调用封装 -- 使用 `ollama.AsyncClient` 异步调用 -- 支持 `think='high'` 思考模式 -- 返回 `content` 和 `thinking` 字段 - -#### prompt.py - Prompt 工程 -精心设计的 Prompt 模板,包含 7 条核心规则: - -| 规则 | 说明 | -|------|------| -| RULE #1 | 无缝连接 - 不重复 suffix 内容,避免"复读机"错误 | -| RULE #2 | 空白处理 - 避免双空格,正确对接标点 | -| RULE #3 | 缩进对齐 - 匹配当前缩进级别和类型 | -| RULE #4 | 列表维护 - 识别并继续任务列表、有序列表、无序列表 | -| RULE #5 | 语法闭合 - 自动闭合未完成的 Markdown 语法 | -| RULE #6 | 输出格式 - 仅输出续写文本,无解释无注释 | -| RULE #7 | 必须输出 - 始终提供有用的续写建议 | - -### 前端设计 - -#### ProseMirror Mark 系统 - -使用 ProseMirror 的 Mark 系统实现灰色建议文本: - -```typescript -// 定义 ghost mark -export const copilotGhostMark = $markSchema('copilot_ghost', () => ({ - excludes: '_', - inclusive: true, - toDOM: () => ['span', { - 'data-copilot-ghost': '', - class: 'copilot-ghost-text' - }, 0] -})) - -// CSS 样式 -.copilot-ghost-text { - color: #999; - opacity: 0.6; -} -``` - -#### copilotPlugin 核心逻辑 - -```mermaid -flowchart LR - A[用户输入] --> B{文档变化?} - B -->|是| C[清除旧建议] - C --> D[防抖 1000ms] - D --> E[发送 API 请求] - E --> F[收到建议] - F --> G[插入 Ghost Text] - - G --> H{用户操作} - H -->|Tab| I[接受建议
移除 mark] - H -->|Esc| J[拒绝建议
删除文本] - H -->|点击 Ghost| I - H -->|继续输入| J -``` - -#### 关键函数 - -| 函数 | 作用 | -|------|------| -| `scheduleFetch` | 防抖调度 API 请求 | -| `insertGhostText` | 插入带 mark 的建议文本 | -| `acceptSuggestion` | Tab 接受建议 | -| `rejectSuggestion` | Esc 拒绝建议 | -| `clearGhostText` | 清除当前建议 | - -### 数据流 - -```mermaid -sequenceDiagram - participant U as 用户 - participant E as Editor (ProseMirror) - participant P as copilotPlugin - participant A as api.js - participant B as Backend - participant L as LLM - - U->>E: 输入文本 - E->>P: view.update() - P->>P: 清除旧建议 - P->>P: 防抖 1000ms - P->>A: fetchSuggestion(prefix, suffix) - A->>B: POST /v1/completions - B->>B: build_prompt() - B->>L: ollama.chat() - L-->>B: {content, thinking} - B-->>A: SSE stream - A-->>P: suggestion text - P->>E: insertGhostText() - E-->>U: 显示灰色建议 - - alt Tab 键 - U->>P: Tab - P->>E: acceptSuggestion() - E-->>U: 建议变为正常文本 - else Esc 键 - U->>P: Esc - P->>E: rejectSuggestion() - E-->>U: 建议消失 - else 继续输入 - U->>E: 输入其他字符 - E->>P: handleKeyDown() - P->>E: clearGhostText() - end -``` +### 前端 +- copilotPlugin.ts: ProseMirror Mark系统 +- 关键函数: scheduleFetch、insertGhostText +- Pinia Store状态管理 ## 设计亮点 -1. **前后端分离**:前端只负责渲染和数据回传,后端负责 LLM 调用、Prompt 构建和数据解析 -2. **低延迟优化**:防抖机制 (1000ms) + SSE 流式响应 + AbortController 取消过期请求 -3. **ProseMirror Mark 系统**:与编辑器状态完美集成,支持 Undo/Redo -4. **多种交互方式**:Tab/Esc/点击/输入,用户体验友好 -5. **智能大小限制**:文档超过 32KB 自动禁用 AI 功能 +1. 前后端分离 +2. 低延迟优化:防抖+SSE+AbortController +3. ProseMirror Mark系统 +4. 多种交互方式 +5. 智能大小限制 +6. 隐私保护 +7. 多语言支持 +8. 主题定制 +9. 文档处理 +10. 语音功能 + +## 开发指南 + +代码风格: Python(4空格,snake_case) JS/TS(2空格,camelCase) +测试: pytest +构建: npm run build ## 许可证 diff --git a/README.md.fixed b/README.md.fixed deleted file mode 100644 index 20c37416155c5710ee41a9bc24ec7561e88cdd45..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10274 zcmb7KeQ;FO6~Ab;P=h$r4y{Zbo?uN7NdiSw1Zxt4C}fk}4Pa&=o9yPX0g|xY-K^$= z1Qkcp1Orv5+8G^Yq}qRM|EZPfj2*|Xj+U84YF<>TI9kpI{sx+|bRExR~WBaqjbU?RLrPOZKrS_-?HU0Uc zhbOzYROH@1dR?rt?d-Vv*^Rr#x6aNPF6QK(i=21e1Kq(3?Jti%ANporMeZ;0Up5a7 zuW5GFz2GsUHelu$=4b%TBu0AhPk#4j3DJ!^a$-voZk~Adidx}+v7*$ZJ&>yGutzyw zj$^`6BS!irJ+CcS1LSnUj|-Un*?VHIkEO%+#C{Q_yqsU~Yus7!iAcsRHPhMgzx7a? zo1it?;Q`R<3C@cO?$;4FSfjpYUmam zt&o5=sy@QI1-aw#zlUj2Qnuk5VXL?WcPjSJky3Cck{5&Ij~q`=@zasBBTr7cK?C*q z!!Lzwnw+2Zr=^WF5 zDb)+@?!=nn_zvy=4?>IG7)|9pzg|D1rg^0Gib>#UCp@$pBkOU!2kS8ddVQc2QHxYl z=-i;As;rg06p&l=nse0>Ky3F>73y`st^?hqs#aMY6}AI!8IHU0UyrAStECY$awYEp z7ak6!5t{N}Z&7LhC3}_GR7!K!U}3KVc8iue4VlSNO5df*NVU$wZ3J!{+S1Q}?!GzX0qmGwnbyl5p+=h9T}1r=IxC+6K%G_SFytx5a3*bnMKD~|Z=dBL@< z#tRA~I(?x<4M0{#*uH7}VFNfc9!lLLfwKm&Af;;H4~102+Ml!f*Y=wGKqIN`(~JL1 z5glVm^9=DeaiP7m<~CURzaKPvaP$^YYu4kPnrlaKEtaNszeQ~$sQK~CTJ~d3Z;yMY z=nX@+%_@wz=|E3ewH&|nI7-+9?POqYYCUb~G7Nb7mqh7dTyOBL(2W*-$?}ZnguGuv zny5k_;c4RHz|&u3KNu;DJbZ4nw@iEeGVo+7g&K|f!yhRej5R56ZpN;_Jr8*%5>XOr zhU>-;DHz|&Q=3WTkXW%8Gu;(1^~IYbCKn1l;egtdbz5$3fxXmGD3A}OU;ZXd1Qq_dkZbm zNWb>Op0ZXGI6FQS@?CgOIqWPrYuj4~?70hhpjG;i^>!lqnh01((Yq(@ro(-sQW*&? z`4=7SfybthTNn$wp{wcWl4dhtL-_yh=mU{sLsw^PenkiVjA%p1tX_;{94Ca}#xy4q zmHjJ3U_5L_SWo8ZuE={~1fH8X7V6$&^CvUdv2r@9_A|~Pk;b-aDf-3n-KJFfn)Hif zzfE1V%WC|mSnJI5F5;-f8?FNGZ-^tXA;i%-wGLH5BW&NROl4InHvT)h@Q{kW*G6of zQcM0O{pbS^kJa~Zj5TloUdmM`p0r%up_Xb4KDG0xgFRHW@2Nkl&_1(s^JCfIhk@bX zi3q)qF`^F~bEhR*-k&9;soibP#v*1tam+|W?@GhZ4HQ-g>ONlgfo?A-GGcHC)v4n> zp9myt1761=n}eKg*A5(KRgx^Cz*;bc(V1AJSm(4j*Rb$Wwbg+f{<&+QL24w6Czvfj zc(W%VmZ52$^S)VoXdSFwr=s|6hUFVEwiXeEwZ<~oE{$WgTA^w&Q!&m4;4oLOg%&e7 zl7Owj!JTMa>rHl{tC7iJHUYtZJ~{*`PsR1gJ;4He5F<&#@6L+ z;nivm?8+U3)MK27r`9d3FO+WsueXC&Ls|SGg;m7P0R1^zLW)0*e{sZ_fhL5yH#_GS z|2-Z*6Skl5*-UF9!DA)#`$sI9Vp;5zZ^VnugN z%}ATsy^}?n@?QPx_ajQhx^8yP)P!?#g_2UjF3{turaVKK#bi?9UF2l`qx6fEnd$Pd z7(LQuaoGi8^36N=alY(L1`|h&~tjZCdOo(Uz;>wee%Ro4{vgd@*U8< z9KibPb)nUOXcEFR6X)uJua19q@RP0dSHIkp&!3aYiDX;uTBLTeIozXh%-*INt1XRc zp%1oi!1zw^$10pWUJ-S7OBaIp>{C%f?)NC!ZbZ9EeVxE>rN+6T3@{6>`izz(1v;>KO0iHh7+;iZ`W zcHnm==u>ZwT_5uD19w9laTKq<9KUu4ugKFdZ0#L|cLu_N?KZ;-+=(ql{4`WfyElw} zJb4@%B-e8TClAkUGkq9|2ab9S*}Nh*dZ;OgmECr6e05@xy8g(yac_1%?G<_2gM)js z-)g%$V`vlq&*8zrHQg1t`0l=kzKR@=f4locvK$q5Y$@lG(SJ;S6=`|iP`NYQbT}Ms z3UTzsk)@%DR_-mz(O8+M@%>QufiFj1h#4Ahk1q{n+u47+7&sQa=UF z%F(X1u#4F%h{gM*B8lt=Uo0QN$=mbyc3~lam*MYr5w}4b`^- zygxhgOpyJ=iri`U%-Fk!PrIM>ynDDDC3+9__EvQ0X<{2n@3sADg#GC`iK_nnSyE*8 zQfSaaBxbS7Djlxmel-Be64y;J7N!fF9 z55)S-)W)o5Bsy}%OIKa868Yi&pOMaV9Zfzly?0)F-Lm37m+7J2q~kTM8Ai-t<{|Ye zL>E7P6meBJs6>WkmvRYy*Xj;qm6f~6Q5=!2Uso%v;T%wl;`H(|V~>GJLtgft2qoFb zgJXYk9b$8nes0eg0X@ce=5E8!5GYV1PX4Hz%C^f2WLf5u#qein*!bj+Rk}#v*N08R z>%&x9JnYq4s?B#L_d;7sG=H4KShhjuBSb~43XWD!!>2a~D+l3|Jh9?UY5go#ck1J= zsps?n?hsfr4QQBJ%(6-uh;muqWPZQAcDEJvFCzF|hYnAY51z)4XVw|tglH}N(^l+V zSM}c>;~N9J|5y(>A9v&3ABE?Kf0D0I#J2UoGu`cBRkMRJft_h~_dj*H%C`GvXqbX_ z;7t*=;^ zw?w4F9!4Te)A$~(*XQ+& zbky}uz%hq1GjKn+87Jd7G8Uo?=aVQ>4uk92VA$`snDI*Id=u9%MYUy$YWc`I6YlD! z=-55pnP4(%8oBE6@4a)H3GSvTT(jG;aqR2Oh|725Xw=z;F`ZGthG@(~v}lMdY~|v+ zglgD0na|j+t}Sew79Wlm=gY$s$M{mPb()7A;J~c!QtPS;d~wy7UIZ&2yQko{eO;c< zdQJCodMDyN^*#zQXoX)_bfJDC7v^nLrbs`6wMozG-*1|?(#1HNFq5&B;dTff!QOy$ zBPgl2^EA6ff1cs2hJw+f?5-J;WJhjmo!T+*#`e@>VHD$>CPLbnVt?aRPO+# zf%xt_+6&j13;wAg2kTJ2?kL5XUjhSL3eGRVrE`Cg7F zXujQP3bDU-b?6TR<{KLBTt5#lJh=GaioOpL6Rpgh=w~%ox|`hv5Ug?@TUCNKcJ#T#T%xb86w<+wgX6{@wB`saYKpktas^1zSbV^vO1BUGKr( zDug-N=cGg6EBQ=T@YK>yfs>rcc;5TFMh4*UzylsjOzvQZHo42jy z=&F#R!(GZ_2j7|Gd!!ZcDdt&rP59jrD@48{;4YF<>TI9kpI{sx+|bRExR~WBaqjbU?RLrPOZKrS_-?HU0Uc zhbOzYROH@1dR?rt?d-Vv*^Rr#x6aNPF6QK(i=21e1Kq(3?Jti%ANporMeZ;0Up5a7 zuW5GFz2GsUHelu$=4b%TBu0AhPk#4j3DJ!^a$-voZk~Adidx}+v7*$ZJ&>yGutzyw zj$^`6BS!irJ+CcS1LSnUj|-Un*?VHIkEO%+#C{Q_yqsU~Yus7!iAcsRHPhMgzx7a? zo1it?;Q`R<3C@cO?$;4FSfjpYUmam zt&o5=sy@QI1-aw#zlUj2Qnuk5VXL?WcPjSJky3Cck{5&Ij~q`=@zasBBTr7cK?C*q z!!Lzwnw+2Zr=^WF5 zDb)+@?!=nn_zvy=4?>IG7)|9pzg|D1rg^0Gib>#UCp@$pBkOU!2kS8ddVQc2QHxYl z=-i;As;rg06p&l=nse0>Ky3F>73y`st^?hqs#aMY6}AI!8IHU0UyrAStECY$awYEp z7ak6!5t{N}Z&7LhC3}_GR7!K!U}3KVc8iue4VlSNO5df*NVU$wZ3J!{+S1Q}?!GzX0qmGwnbyl5p+=h9T}1r=IxC+6K%G_SFytx5a3*bnMKD~|Z=dBL@< z#tRA~I(?x<4M0{#*uH7}VFNfc9!lLLfwKm&Af;;H4~102+Ml!f*Y=wGKqIN`(~JL1 z5glVm^9=DeaiP7m<~CURzaKPvaP$^YYu4kPnrlaKEtaNszeQ~$sQK~CTJ~d3Z;yMY z=nX@+%_@wz=|E3ewH&|nI7-+9?POqYYCUb~G7Nb7mqh7dTyOBL(2W*-$?}ZnguGuv zny5k_;c4RHz|&u3KNu;DJbZ4nw@iEeGVo+7g&K|f!yhRej5R56ZpN;_Jr8*%5>XOr zhU>-;DHz|&Q=3WTkXW%8Gu;(1^~IYbCKn1l;egtdbz5$3fxXmGD3A}OU;ZXd1Qq_dkZbm zNWb>Op0ZXGI6FQS@?CgOIqWPrYuj4~?70hhpjG;i^>!lqnh01((Yq(@ro(-sQW*&? z`4=7SfybthTNn$wp{wcWl4dhtL-_yh=mU{sLsw^PenkiVjA%p1tX_;{94Ca}#xy4q zmHjJ3U_5L_SWo8ZuE={~1fH8X7V6$&^CvUdv2r@9_A|~Pk;b-aDf-3n-KJFfn)Hif zzfE1V%WC|mSnJI5F5;-f8?FNGZ-^tXA;i%-wGLH5BW&NROl4InHvT)h@Q{kW*G6of zQcM0O{pbS^kJa~Zj5TloUdmM`p0r%up_Xb4KDG0xgFRHW@2Nkl&_1(s^JCfIhk@bX zi3q)qF`^F~bEhR*-k&9;soibP#v*1tam+|W?@GhZ4HQ-g>ONlgfo?A-GGcHC)v4n> zp9myt1761=n}eKg*A5(KRgx^Cz*;bc(V1AJSm(4j*Rb$Wwbg+f{<&+QL24w6Czvfj zc(W%VmZ52$^S)VoXdSFwr=s|6hUFVEwiXeEwZ<~oE{$WgTA^w&Q!&m4;4oLOg%&e7 zl7Owj!JTMa>rHl{tC7iJHUYtZJ~{*`PsR1gJ;4He5F<&#@6L+ z;nivm?8+U3)MK27r`9d3FO+WsueXC&Ls|SGg;m7P0R1^zLW)0*e{sZ_fhL5yH#_GS z|2-Z*6Skl5*-UF9!DA)#`$sI9Vp;5zZ^VnugN z%}ATsy^}?n@?QPx_ajQhx^8yP)P!?#g_2UjF3{turaVKK#bi?9UF2l`qx6fEnd$Pd z7(LQuaoGi8^36N=alY(L1`|h&~tjZCdOo(Uz;>wee%Ro4{vgd@*U8< z9KibPb)nUOXcEFR6X)uJua19q@RP0dSHIkp&!3aYiDX;uTBLTeIozXh%-*INt1XRc zp%1oi!1zw^$10pWUJ-S7OBaIp>{C%f?)NC!ZbZ9EeVxE>rN+6T3@{6>`izz(1v;>KO0iHh7+;iZ`W zcHnm==u>ZwT_5uD19w9laTKq<9KUu4ugKFdZ0#L|cLu_N?KZ;-+=(ql{4`WfyElw} zJb4@%B-e8TClAkUGkq9|2ab9S*}Nh*dZ;OgmECr6e05@xy8g(yac_1%?G<_2gM)js z-)g%$V`vlq&*8zrHQg1t`0l=kzKR@=f4locvK$q5Y$@lG(SJ;S6=`|iP`NYQbT}Ms z3UTzsk)@%DR_-mz(O8+M@%>QufiFj1h#4Ahk1q{n+u47+7&sQa=UF z%F(X1u#4F%h{gM*B8lt=Uo0QN$=mbyc3~lam*MYr5w}4b`^- zygxhgOpyJ=iri`U%-Fk!PrIM>ynDDDC3+9__EvQ0X<{2n@3sADg#GC`iK_nnSyE*8 zQfSaaBxbS7Djlxmel-Be64y;J7N!fF9 z55)S-)W)o5Bsy}%OIKa868Yi&pOMaV9Zfzly?0)F-Lm37m+7J2q~kTM8Ai-t<{|Ye zL>E7P6meBJs6>WkmvRYy*Xj;qm6f~6Q5=!2Uso%v;T%wl;`H(|V~>GJLtgft2qoFb zgJXYk9b$8nes0eg0X@ce=5E8!5GYV1PX4Hz%C^f2WLf5u#qein*!bj+Rk}#v*N08R z>%&x9JnYq4s?B#L_d;7sG=H4KShhjuBSb~43XWD!!>2a~D+l3|Jh9?UY5go#ck1J= zsps?n?hsfr4QQBJ%(6-uh;muqWPZQAcDEJvFCzF|hYnAY51z)4XVw|tglH}N(^l+V zSM}c>;~N9J|5y(>A9v&3ABE?Kf0D0I#J2UoGu`cBRkMRJft_h~_dj*H%C`GvXqbX_ z;7t*=;^ zw?w4F9!4Te)A$~(*XQ+& zbky}uz%hq1GjKn+87Jd7G8Uo?=aVQ>4uk92VA$`snDI*Id=u9%MYUy$YWc`I6YlD! z=-55pnP4(%8oBE6@4a)H3GSvTT(jG;aqR2Oh|725Xw=z;F`ZGthG@(~v}lMdY~|v+ zglgD0na|j+t}Sew79Wlm=gY$s$M{mPb()7A;J~c!QtPS;d~wy7UIZ&2yQko{eO;c< zdQJCodMDyN^*#zQXoX)_bfJDC7v^nLrbs`6wMozG-*1|?(#1HNFq5&B;dTff!QOy$ zBPgl2^EA6ff1cs2hJw+f?5-J;WJhjmo!T+*#`e@>VHD$>CPLbnVt?aRPO+# zf%xt_+6&j13;wAg2kTJ2?kL5XUjhSL3eGRVrE`Cg7F zXujQP3bDU-b?6TR<{KLBTt5#lJh=GaioOpL6Rpgh=w~%ox|`hv5Ug?@TUCNKcJ#T#T%xb86w<+wgX6{@wB`saYKpktas^1zSbV^vO1BUGKr( zDug-N=cGg6EBQ=T@YK>yfs>rcc;5TFMh4*UzylsjOzvQZHo42jy z=&F#R!(GZ_2j7|Gd!!ZcDdt&rP59jrD@48{; B[MilkdownEditor.vue] - B --> C[Crepe Editor] - C --> D[ProseMirror] - D --> E[copilotPlugin.ts] - E --> F[copilotGhostMark] - E --> G[api.js] - end - - subgraph Backend["后端 (FastAPI + Python)"] - H[main.py
FastAPI Server] --> I[prompt.py
Prompt 构建] - H --> J[llm.py
Ollama 调用] - J --> K[Ollama API] - end - - G -->|POST /v1/completions
SSE 流式响应| H - K -->|LLM 响应| J -``` - -## 项目结构 - -``` -llm-in-text/ -├── src/ -│ ├── components/ -│ │ └── MilkdownEditor.vue # 主编辑器组件 -│ ├── plugins/ -│ │ ├── copilotPlugin.ts # ProseMirror AI 补全插件 -│ │ ├── types.ts # 类型定义 -│ │ └── index.ts # 插件导出 -│ ├── utils/ -│ │ ├── api.js # API 调用封装 -│ │ ├── config.js # 配置文件 -│ │ └── ocrCache.js # OCR 缓存管理 -│ ├── App.vue -│ └── main.js -├── backend/ -│ ├── main.py # FastAPI 服务器 -│ ├── llm.py # LLM API 调用 -│ ├── prompt.py # Prompt 构建 -│ └── requirements.txt -└── README.md -``` - -## 快速开始 - -### 环境要求 -- Node.js 18+ -- Python 3.8+ -- Ollama 服务(或其他兼容 OpenAI API 的服务) - -### 安装 - -```bash -# 前端 -npm install - -# 后端 -cd backend -pip install -r requirements.txt -``` - -### 配置 - -在 `backend/.env` 中配置: - -```env -OLLAMA_MODEL=gpt-oss:20b -OLLAMA_HOST=http://localhost:11434 -``` - -### 启动 - -```bash -# 后端(端口 8000) -cd backend -python main.py - -# 前端(端口 5173) -npm run dev -``` - -访问 http://localhost:5173 - -## API 接口 - -### POST /v1/completions - -流式获取补全建议 - -**请求:** -```json -{ - "prefix": "# Title\n\nContent ", - "suffix": "", - "languageId": "markdown" -} -``` - -**响应(SSE):** -``` -data: {"content": "here"} -data: {"content": "here is"} -data: {"done": true} -``` - -## 核心实现 - -### 后端设计 - -#### main.py - FastAPI 服务器 -- 定义 `/v1/completions` 端点 -- 使用 `StreamingResponse` 返回 SSE 流式响应 -- CORS 配置允许跨域请求 - -#### llm.py - LLM 调用封装 -- 使用 `ollama.AsyncClient` 异步调用 -- 支持 `think='high'` 思考模式 -- 返回 `content` 和 `thinking` 字段 - -#### prompt.py - Prompt 工程 -精心设计的 Prompt 模板,包含 7 条核心规则: - -| 规则 | 说明 | -|------|------| -| RULE #1 | 无缝连接 - 不重复 suffix 内容,避免"复读机"错误 | -| RULE #2 | 空白处理 - 避免双空格,正确对接标点 | -| RULE #3 | 缩进对齐 - 匹配当前缩进级别和类型 | -| RULE #4 | 列表维护 - 识别并继续任务列表、有序列表、无序列表 | -| RULE #5 | 语法闭合 - 自动闭合未完成的 Markdown 语法 | -| RULE #6 | 输出格式 - 仅输出续写文本,无解释无注释 | -| RULE #7 | 必须输出 - 始终提供有用的续写建议 | - -### 前端设计 - -#### ProseMirror Mark 系统 - -使用 ProseMirror 的 Mark 系统实现灰色建议文本: - -```typescript -// 定义 ghost mark -export const copilotGhostMark = $markSchema('copilot_ghost', () => ({ - excludes: '_', - inclusive: true, - toDOM: () => ['span', { - 'data-copilot-ghost': '', - class: 'copilot-ghost-text' - }, 0] -})) - -// CSS 样式 -.copilot-ghost-text { - color: #999; - opacity: 0.6; -} -``` - -#### copilotPlugin 核心逻辑 - -```mermaid -flowchart LR - A[用户输入] --> B{文档变化?} - B -->|是| C[清除旧建议] - C --> D[防抖 1000ms] - D --> E[发送 API 请求] - E --> F[收到建议] - F --> G[插入 Ghost Text] - - G --> H{用户操作} - H -->|Tab| I[接受建议
移除 mark] - H -->|Esc| J[拒绝建议
删除文本] - H -->|点击 Ghost| I - H -->|继续输入| J -``` - -#### 关键函数 - -| 函数 | 作用 | -|------|------| -| `scheduleFetch` | 防抖调度 API 请求 | -| `insertGhostText` | 插入带 mark 的建议文本 | -| `acceptSuggestion` | Tab 接受建议 | -| `rejectSuggestion` | Esc 拒绝建议 | -| `clearGhostText` | 清除当前建议 | - -### 数据流 - -```mermaid -sequenceDiagram - participant U as 用户 - participant E as Editor (ProseMirror) - participant P as copilotPlugin - participant A as api.js - participant B as Backend - participant L as LLM - - U->>E: 输入文本 - E->>P: view.update() - P->>P: 清除旧建议 - P->>P: 防抖 1000ms - P->>A: fetchSuggestion(prefix, suffix) - A->>B: POST /v1/completions - B->>B: build_prompt() - B->>L: ollama.chat() - L-->>B: {content, thinking} - B-->>A: SSE stream - A-->>P: suggestion text - P->>E: insertGhostText() - E-->>U: 显示灰色建议 - - alt Tab 键 - U->>P: Tab - P->>E: acceptSuggestion() - E-->>U: 建议变为正常文本 - else Esc 键 - U->>P: Esc - P->>E: rejectSuggestion() - E-->>U: 建议消失 - else 继续输入 - U->>E: 输入其他字符 - E->>P: handleKeyDown() - P->>E: clearGhostText() - end -``` - -## 设计亮点 - -1. **前后端分离**:前端只负责渲染和数据回传,后端负责 LLM 调用、Prompt 构建和数据解析 -2. **低延迟优化**:防抖机制 (1000ms) + SSE 流式响应 + AbortController 取消过期请求 -3. **ProseMirror Mark 系统**:与编辑器状态完美集成,支持 Undo/Redo -4. **多种交互方式**:Tab/Esc/点击/输入,用户体验友好 -5. **智能大小限制**:文档超过 32KB 自动禁用 AI 功能 - -## 许可证 - -MIT License diff --git a/README_clean.md b/README_clean.md deleted file mode 100644 index 20c37416155c5710ee41a9bc24ec7561e88cdd45..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10274 zcmb7KeQ;FO6~Ab;P=h$r4y{Zbo?uN7NdiSw1Zxt4C}fk}4Pa&=o9yPX0g|xY-K^$= z1Qkcp1Orv5+8G^Yq}qRM|EZPfj2*|Xj+U84YF<>TI9kpI{sx+|bRExR~WBaqjbU?RLrPOZKrS_-?HU0Uc zhbOzYROH@1dR?rt?d-Vv*^Rr#x6aNPF6QK(i=21e1Kq(3?Jti%ANporMeZ;0Up5a7 zuW5GFz2GsUHelu$=4b%TBu0AhPk#4j3DJ!^a$-voZk~Adidx}+v7*$ZJ&>yGutzyw zj$^`6BS!irJ+CcS1LSnUj|-Un*?VHIkEO%+#C{Q_yqsU~Yus7!iAcsRHPhMgzx7a? zo1it?;Q`R<3C@cO?$;4FSfjpYUmam zt&o5=sy@QI1-aw#zlUj2Qnuk5VXL?WcPjSJky3Cck{5&Ij~q`=@zasBBTr7cK?C*q z!!Lzwnw+2Zr=^WF5 zDb)+@?!=nn_zvy=4?>IG7)|9pzg|D1rg^0Gib>#UCp@$pBkOU!2kS8ddVQc2QHxYl z=-i;As;rg06p&l=nse0>Ky3F>73y`st^?hqs#aMY6}AI!8IHU0UyrAStECY$awYEp z7ak6!5t{N}Z&7LhC3}_GR7!K!U}3KVc8iue4VlSNO5df*NVU$wZ3J!{+S1Q}?!GzX0qmGwnbyl5p+=h9T}1r=IxC+6K%G_SFytx5a3*bnMKD~|Z=dBL@< z#tRA~I(?x<4M0{#*uH7}VFNfc9!lLLfwKm&Af;;H4~102+Ml!f*Y=wGKqIN`(~JL1 z5glVm^9=DeaiP7m<~CURzaKPvaP$^YYu4kPnrlaKEtaNszeQ~$sQK~CTJ~d3Z;yMY z=nX@+%_@wz=|E3ewH&|nI7-+9?POqYYCUb~G7Nb7mqh7dTyOBL(2W*-$?}ZnguGuv zny5k_;c4RHz|&u3KNu;DJbZ4nw@iEeGVo+7g&K|f!yhRej5R56ZpN;_Jr8*%5>XOr zhU>-;DHz|&Q=3WTkXW%8Gu;(1^~IYbCKn1l;egtdbz5$3fxXmGD3A}OU;ZXd1Qq_dkZbm zNWb>Op0ZXGI6FQS@?CgOIqWPrYuj4~?70hhpjG;i^>!lqnh01((Yq(@ro(-sQW*&? z`4=7SfybthTNn$wp{wcWl4dhtL-_yh=mU{sLsw^PenkiVjA%p1tX_;{94Ca}#xy4q zmHjJ3U_5L_SWo8ZuE={~1fH8X7V6$&^CvUdv2r@9_A|~Pk;b-aDf-3n-KJFfn)Hif zzfE1V%WC|mSnJI5F5;-f8?FNGZ-^tXA;i%-wGLH5BW&NROl4InHvT)h@Q{kW*G6of zQcM0O{pbS^kJa~Zj5TloUdmM`p0r%up_Xb4KDG0xgFRHW@2Nkl&_1(s^JCfIhk@bX zi3q)qF`^F~bEhR*-k&9;soibP#v*1tam+|W?@GhZ4HQ-g>ONlgfo?A-GGcHC)v4n> zp9myt1761=n}eKg*A5(KRgx^Cz*;bc(V1AJSm(4j*Rb$Wwbg+f{<&+QL24w6Czvfj zc(W%VmZ52$^S)VoXdSFwr=s|6hUFVEwiXeEwZ<~oE{$WgTA^w&Q!&m4;4oLOg%&e7 zl7Owj!JTMa>rHl{tC7iJHUYtZJ~{*`PsR1gJ;4He5F<&#@6L+ z;nivm?8+U3)MK27r`9d3FO+WsueXC&Ls|SGg;m7P0R1^zLW)0*e{sZ_fhL5yH#_GS z|2-Z*6Skl5*-UF9!DA)#`$sI9Vp;5zZ^VnugN z%}ATsy^}?n@?QPx_ajQhx^8yP)P!?#g_2UjF3{turaVKK#bi?9UF2l`qx6fEnd$Pd z7(LQuaoGi8^36N=alY(L1`|h&~tjZCdOo(Uz;>wee%Ro4{vgd@*U8< z9KibPb)nUOXcEFR6X)uJua19q@RP0dSHIkp&!3aYiDX;uTBLTeIozXh%-*INt1XRc zp%1oi!1zw^$10pWUJ-S7OBaIp>{C%f?)NC!ZbZ9EeVxE>rN+6T3@{6>`izz(1v;>KO0iHh7+;iZ`W zcHnm==u>ZwT_5uD19w9laTKq<9KUu4ugKFdZ0#L|cLu_N?KZ;-+=(ql{4`WfyElw} zJb4@%B-e8TClAkUGkq9|2ab9S*}Nh*dZ;OgmECr6e05@xy8g(yac_1%?G<_2gM)js z-)g%$V`vlq&*8zrHQg1t`0l=kzKR@=f4locvK$q5Y$@lG(SJ;S6=`|iP`NYQbT}Ms z3UTzsk)@%DR_-mz(O8+M@%>QufiFj1h#4Ahk1q{n+u47+7&sQa=UF z%F(X1u#4F%h{gM*B8lt=Uo0QN$=mbyc3~lam*MYr5w}4b`^- zygxhgOpyJ=iri`U%-Fk!PrIM>ynDDDC3+9__EvQ0X<{2n@3sADg#GC`iK_nnSyE*8 zQfSaaBxbS7Djlxmel-Be64y;J7N!fF9 z55)S-)W)o5Bsy}%OIKa868Yi&pOMaV9Zfzly?0)F-Lm37m+7J2q~kTM8Ai-t<{|Ye zL>E7P6meBJs6>WkmvRYy*Xj;qm6f~6Q5=!2Uso%v;T%wl;`H(|V~>GJLtgft2qoFb zgJXYk9b$8nes0eg0X@ce=5E8!5GYV1PX4Hz%C^f2WLf5u#qein*!bj+Rk}#v*N08R z>%&x9JnYq4s?B#L_d;7sG=H4KShhjuBSb~43XWD!!>2a~D+l3|Jh9?UY5go#ck1J= zsps?n?hsfr4QQBJ%(6-uh;muqWPZQAcDEJvFCzF|hYnAY51z)4XVw|tglH}N(^l+V zSM}c>;~N9J|5y(>A9v&3ABE?Kf0D0I#J2UoGu`cBRkMRJft_h~_dj*H%C`GvXqbX_ z;7t*=;^ zw?w4F9!4Te)A$~(*XQ+& zbky}uz%hq1GjKn+87Jd7G8Uo?=aVQ>4uk92VA$`snDI*Id=u9%MYUy$YWc`I6YlD! z=-55pnP4(%8oBE6@4a)H3GSvTT(jG;aqR2Oh|725Xw=z;F`ZGthG@(~v}lMdY~|v+ zglgD0na|j+t}Sew79Wlm=gY$s$M{mPb()7A;J~c!QtPS;d~wy7UIZ&2yQko{eO;c< zdQJCodMDyN^*#zQXoX)_bfJDC7v^nLrbs`6wMozG-*1|?(#1HNFq5&B;dTff!QOy$ zBPgl2^EA6ff1cs2hJw+f?5-J;WJhjmo!T+*#`e@>VHD$>CPLbnVt?aRPO+# zf%xt_+6&j13;wAg2kTJ2?kL5XUjhSL3eGRVrE`Cg7F zXujQP3bDU-b?6TR<{KLBTt5#lJh=GaioOpL6Rpgh=w~%ox|`hv5Ug?@TUCNKcJ#T#T%xb86w<+wgX6{@wB`saYKpktas^1zSbV^vO1BUGKr( zDug-N=cGg6EBQ=T@YK>yfs>rcc;5TFMh4*UzylsjOzvQZHo42jy z=&F#R!(GZ_2j7|Gd!!ZcDdt&rP59jrD@48{;4YF<>TI9kpI{sx+|bRExR~WBaqjbU?RLrPOZKrS_-?HU0Uc zhbOzYROH@1dR?rt?d-Vv*^Rr#x6aNPF6QK(i=21e1Kq(3?Jti%ANporMeZ;0Up5a7 zuW5GFz2GsUHelu$=4b%TBu0AhPk#4j3DJ!^a$-voZk~Adidx}+v7*$ZJ&>yGutzyw zj$^`6BS!irJ+CcS1LSnUj|-Un*?VHIkEO%+#C{Q_yqsU~Yus7!iAcsRHPhMgzx7a? zo1it?;Q`R<3C@cO?$;4FSfjpYUmam zt&o5=sy@QI1-aw#zlUj2Qnuk5VXL?WcPjSJky3Cck{5&Ij~q`=@zasBBTr7cK?C*q z!!Lzwnw+2Zr=^WF5 zDb)+@?!=nn_zvy=4?>IG7)|9pzg|D1rg^0Gib>#UCp@$pBkOU!2kS8ddVQc2QHxYl z=-i;As;rg06p&l=nse0>Ky3F>73y`st^?hqs#aMY6}AI!8IHU0UyrAStECY$awYEp z7ak6!5t{N}Z&7LhC3}_GR7!K!U}3KVc8iue4VlSNO5df*NVU$wZ3J!{+S1Q}?!GzX0qmGwnbyl5p+=h9T}1r=IxC+6K%G_SFytx5a3*bnMKD~|Z=dBL@< z#tRA~I(?x<4M0{#*uH7}VFNfc9!lLLfwKm&Af;;H4~102+Ml!f*Y=wGKqIN`(~JL1 z5glVm^9=DeaiP7m<~CURzaKPvaP$^YYu4kPnrlaKEtaNszeQ~$sQK~CTJ~d3Z;yMY z=nX@+%_@wz=|E3ewH&|nI7-+9?POqYYCUb~G7Nb7mqh7dTyOBL(2W*-$?}ZnguGuv zny5k_;c4RHz|&u3KNu;DJbZ4nw@iEeGVo+7g&K|f!yhRej5R56ZpN;_Jr8*%5>XOr zhU>-;DHz|&Q=3WTkXW%8Gu;(1^~IYbCKn1l;egtdbz5$3fxXmGD3A}OU;ZXd1Qq_dkZbm zNWb>Op0ZXGI6FQS@?CgOIqWPrYuj4~?70hhpjG;i^>!lqnh01((Yq(@ro(-sQW*&? z`4=7SfybthTNn$wp{wcWl4dhtL-_yh=mU{sLsw^PenkiVjA%p1tX_;{94Ca}#xy4q zmHjJ3U_5L_SWo8ZuE={~1fH8X7V6$&^CvUdv2r@9_A|~Pk;b-aDf-3n-KJFfn)Hif zzfE1V%WC|mSnJI5F5;-f8?FNGZ-^tXA;i%-wGLH5BW&NROl4InHvT)h@Q{kW*G6of zQcM0O{pbS^kJa~Zj5TloUdmM`p0r%up_Xb4KDG0xgFRHW@2Nkl&_1(s^JCfIhk@bX zi3q)qF`^F~bEhR*-k&9;soibP#v*1tam+|W?@GhZ4HQ-g>ONlgfo?A-GGcHC)v4n> zp9myt1761=n}eKg*A5(KRgx^Cz*;bc(V1AJSm(4j*Rb$Wwbg+f{<&+QL24w6Czvfj zc(W%VmZ52$^S)VoXdSFwr=s|6hUFVEwiXeEwZ<~oE{$WgTA^w&Q!&m4;4oLOg%&e7 zl7Owj!JTMa>rHl{tC7iJHUYtZJ~{*`PsR1gJ;4He5F<&#@6L+ z;nivm?8+U3)MK27r`9d3FO+WsueXC&Ls|SGg;m7P0R1^zLW)0*e{sZ_fhL5yH#_GS z|2-Z*6Skl5*-UF9!DA)#`$sI9Vp;5zZ^VnugN z%}ATsy^}?n@?QPx_ajQhx^8yP)P!?#g_2UjF3{turaVKK#bi?9UF2l`qx6fEnd$Pd z7(LQuaoGi8^36N=alY(L1`|h&~tjZCdOo(Uz;>wee%Ro4{vgd@*U8< z9KibPb)nUOXcEFR6X)uJua19q@RP0dSHIkp&!3aYiDX;uTBLTeIozXh%-*INt1XRc zp%1oi!1zw^$10pWUJ-S7OBaIp>{C%f?)NC!ZbZ9EeVxE>rN+6T3@{6>`izz(1v;>KO0iHh7+;iZ`W zcHnm==u>ZwT_5uD19w9laTKq<9KUu4ugKFdZ0#L|cLu_N?KZ;-+=(ql{4`WfyElw} zJb4@%B-e8TClAkUGkq9|2ab9S*}Nh*dZ;OgmECr6e05@xy8g(yac_1%?G<_2gM)js z-)g%$V`vlq&*8zrHQg1t`0l=kzKR@=f4locvK$q5Y$@lG(SJ;S6=`|iP`NYQbT}Ms z3UTzsk)@%DR_-mz(O8+M@%>QufiFj1h#4Ahk1q{n+u47+7&sQa=UF z%F(X1u#4F%h{gM*B8lt=Uo0QN$=mbyc3~lam*MYr5w}4b`^- zygxhgOpyJ=iri`U%-Fk!PrIM>ynDDDC3+9__EvQ0X<{2n@3sADg#GC`iK_nnSyE*8 zQfSaaBxbS7Djlxmel-Be64y;J7N!fF9 z55)S-)W)o5Bsy}%OIKa868Yi&pOMaV9Zfzly?0)F-Lm37m+7J2q~kTM8Ai-t<{|Ye zL>E7P6meBJs6>WkmvRYy*Xj;qm6f~6Q5=!2Uso%v;T%wl;`H(|V~>GJLtgft2qoFb zgJXYk9b$8nes0eg0X@ce=5E8!5GYV1PX4Hz%C^f2WLf5u#qein*!bj+Rk}#v*N08R z>%&x9JnYq4s?B#L_d;7sG=H4KShhjuBSb~43XWD!!>2a~D+l3|Jh9?UY5go#ck1J= zsps?n?hsfr4QQBJ%(6-uh;muqWPZQAcDEJvFCzF|hYnAY51z)4XVw|tglH}N(^l+V zSM}c>;~N9J|5y(>A9v&3ABE?Kf0D0I#J2UoGu`cBRkMRJft_h~_dj*H%C`GvXqbX_ z;7t*=;^ zw?w4F9!4Te)A$~(*XQ+& zbky}uz%hq1GjKn+87Jd7G8Uo?=aVQ>4uk92VA$`snDI*Id=u9%MYUy$YWc`I6YlD! z=-55pnP4(%8oBE6@4a)H3GSvTT(jG;aqR2Oh|725Xw=z;F`ZGthG@(~v}lMdY~|v+ zglgD0na|j+t}Sew79Wlm=gY$s$M{mPb()7A;J~c!QtPS;d~wy7UIZ&2yQko{eO;c< zdQJCodMDyN^*#zQXoX)_bfJDC7v^nLrbs`6wMozG-*1|?(#1HNFq5&B;dTff!QOy$ zBPgl2^EA6ff1cs2hJw+f?5-J;WJhjmo!T+*#`e@>VHD$>CPLbnVt?aRPO+# zf%xt_+6&j13;wAg2kTJ2?kL5XUjhSL3eGRVrE`Cg7F zXujQP3bDU-b?6TR<{KLBTt5#lJh=GaioOpL6Rpgh=w~%ox|`hv5Ug?@TUCNKcJ#T#T%xb86w<+wgX6{@wB`saYKpktas^1zSbV^vO1BUGKr( zDug-N=cGg6EBQ=T@YK>yfs>rcc;5TFMh4*UzylsjOzvQZHo42jy z=&F#R!(GZ_2j7|Gd!!ZcDdt&rP59jrD@48{;4YF<>TI9kpI{sx+|bRExR~WBaqjbU?RLrPOZKrS_-?HU0Uc zhbOzYROH@1dR?rt?d-Vv*^Rr#x6aNPF6QK(i=21e1Kq(3?Jti%ANporMeZ;0Up5a7 zuW5GFz2GsUHelu$=4b%TBu0AhPk#4j3DJ!^a$-voZk~Adidx}+v7*$ZJ&>yGutzyw zj$^`6BS!irJ+CcS1LSnUj|-Un*?VHIkEO%+#C{Q_yqsU~Yus7!iAcsRHPhMgzx7a? zo1it?;Q`R<3C@cO?$;4FSfjpYUmam zt&o5=sy@QI1-aw#zlUj2Qnuk5VXL?WcPjSJky3Cck{5&Ij~q`=@zasBBTr7cK?C*q z!!Lzwnw+2Zr=^WF5 zDb)+@?!=nn_zvy=4?>IG7)|9pzg|D1rg^0Gib>#UCp@$pBkOU!2kS8ddVQc2QHxYl z=-i;As;rg06p&l=nse0>Ky3F>73y`st^?hqs#aMY6}AI!8IHU0UyrAStECY$awYEp z7ak6!5t{N}Z&7LhC3}_GR7!K!U}3KVc8iue4VlSNO5df*NVU$wZ3J!{+S1Q}?!GzX0qmGwnbyl5p+=h9T}1r=IxC+6K%G_SFytx5a3*bnMKD~|Z=dBL@< z#tRA~I(?x<4M0{#*uH7}VFNfc9!lLLfwKm&Af;;H4~102+Ml!f*Y=wGKqIN`(~JL1 z5glVm^9=DeaiP7m<~CURzaKPvaP$^YYu4kPnrlaKEtaNszeQ~$sQK~CTJ~d3Z;yMY z=nX@+%_@wz=|E3ewH&|nI7-+9?POqYYCUb~G7Nb7mqh7dTyOBL(2W*-$?}ZnguGuv zny5k_;c4RHz|&u3KNu;DJbZ4nw@iEeGVo+7g&K|f!yhRej5R56ZpN;_Jr8*%5>XOr zhU>-;DHz|&Q=3WTkXW%8Gu;(1^~IYbCKn1l;egtdbz5$3fxXmGD3A}OU;ZXd1Qq_dkZbm zNWb>Op0ZXGI6FQS@?CgOIqWPrYuj4~?70hhpjG;i^>!lqnh01((Yq(@ro(-sQW*&? z`4=7SfybthTNn$wp{wcWl4dhtL-_yh=mU{sLsw^PenkiVjA%p1tX_;{94Ca}#xy4q zmHjJ3U_5L_SWo8ZuE={~1fH8X7V6$&^CvUdv2r@9_A|~Pk;b-aDf-3n-KJFfn)Hif zzfE1V%WC|mSnJI5F5;-f8?FNGZ-^tXA;i%-wGLH5BW&NROl4InHvT)h@Q{kW*G6of zQcM0O{pbS^kJa~Zj5TloUdmM`p0r%up_Xb4KDG0xgFRHW@2Nkl&_1(s^JCfIhk@bX zi3q)qF`^F~bEhR*-k&9;soibP#v*1tam+|W?@GhZ4HQ-g>ONlgfo?A-GGcHC)v4n> zp9myt1761=n}eKg*A5(KRgx^Cz*;bc(V1AJSm(4j*Rb$Wwbg+f{<&+QL24w6Czvfj zc(W%VmZ52$^S)VoXdSFwr=s|6hUFVEwiXeEwZ<~oE{$WgTA^w&Q!&m4;4oLOg%&e7 zl7Owj!JTMa>rHl{tC7iJHUYtZJ~{*`PsR1gJ;4He5F<&#@6L+ z;nivm?8+U3)MK27r`9d3FO+WsueXC&Ls|SGg;m7P0R1^zLW)0*e{sZ_fhL5yH#_GS z|2-Z*6Skl5*-UF9!DA)#`$sI9Vp;5zZ^VnugN z%}ATsy^}?n@?QPx_ajQhx^8yP)P!?#g_2UjF3{turaVKK#bi?9UF2l`qx6fEnd$Pd z7(LQuaoGi8^36N=alY(L1`|h&~tjZCdOo(Uz;>wee%Ro4{vgd@*U8< z9KibPb)nUOXcEFR6X)uJua19q@RP0dSHIkp&!3aYiDX;uTBLTeIozXh%-*INt1XRc zp%1oi!1zw^$10pWUJ-S7OBaIp>{C%f?)NC!ZbZ9EeVxE>rN+6T3@{6>`izz(1v;>KO0iHh7+;iZ`W zcHnm==u>ZwT_5uD19w9laTKq<9KUu4ugKFdZ0#L|cLu_N?KZ;-+=(ql{4`WfyElw} zJb4@%B-e8TClAkUGkq9|2ab9S*}Nh*dZ;OgmECr6e05@xy8g(yac_1%?G<_2gM)js z-)g%$V`vlq&*8zrHQg1t`0l=kzKR@=f4locvK$q5Y$@lG(SJ;S6=`|iP`NYQbT}Ms z3UTzsk)@%DR_-mz(O8+M@%>QufiFj1h#4Ahk1q{n+u47+7&sQa=UF z%F(X1u#4F%h{gM*B8lt=Uo0QN$=mbyc3~lam*MYr5w}4b`^- zygxhgOpyJ=iri`U%-Fk!PrIM>ynDDDC3+9__EvQ0X<{2n@3sADg#GC`iK_nnSyE*8 zQfSaaBxbS7Djlxmel-Be64y;J7N!fF9 z55)S-)W)o5Bsy}%OIKa868Yi&pOMaV9Zfzly?0)F-Lm37m+7J2q~kTM8Ai-t<{|Ye zL>E7P6meBJs6>WkmvRYy*Xj;qm6f~6Q5=!2Uso%v;T%wl;`H(|V~>GJLtgft2qoFb zgJXYk9b$8nes0eg0X@ce=5E8!5GYV1PX4Hz%C^f2WLf5u#qein*!bj+Rk}#v*N08R z>%&x9JnYq4s?B#L_d;7sG=H4KShhjuBSb~43XWD!!>2a~D+l3|Jh9?UY5go#ck1J= zsps?n?hsfr4QQBJ%(6-uh;muqWPZQAcDEJvFCzF|hYnAY51z)4XVw|tglH}N(^l+V zSM}c>;~N9J|5y(>A9v&3ABE?Kf0D0I#J2UoGu`cBRkMRJft_h~_dj*H%C`GvXqbX_ z;7t*=;^ zw?w4F9!4Te)A$~(*XQ+& zbky}uz%hq1GjKn+87Jd7G8Uo?=aVQ>4uk92VA$`snDI*Id=u9%MYUy$YWc`I6YlD! z=-55pnP4(%8oBE6@4a)H3GSvTT(jG;aqR2Oh|725Xw=z;F`ZGthG@(~v}lMdY~|v+ zglgD0na|j+t}Sew79Wlm=gY$s$M{mPb()7A;J~c!QtPS;d~wy7UIZ&2yQko{eO;c< zdQJCodMDyN^*#zQXoX)_bfJDC7v^nLrbs`6wMozG-*1|?(#1HNFq5&B;dTff!QOy$ zBPgl2^EA6ff1cs2hJw+f?5-J;WJhjmo!T+*#`e@>VHD$>CPLbnVt?aRPO+# zf%xt_+6&j13;wAg2kTJ2?kL5XUjhSL3eGRVrE`Cg7F zXujQP3bDU-b?6TR<{KLBTt5#lJh=GaioOpL6Rpgh=w~%ox|`hv5Ug?@TUCNKcJ#T#T%xb86w<+wgX6{@wB`saYKpktas^1zSbV^vO1BUGKr( zDug-N=cGg6EBQ=T@YK>yfs>rcc;5TFMh4*UzylsjOzvQZHo42jy z=&F#R!(GZ_2j7|Gd!!ZcDdt&rP59jrD@48{; B[MilkdownEditor.vue] - B --> C[Crepe Editor] - C --> D[ProseMirror] - D --> E[copilotPlugin.ts] - E --> F[copilotGhostMark] - E --> G[api.js] - end - - subgraph Backend["后端 (FastAPI + Python)"] - H[main.py
FastAPI Server] --> I[prompt.py
Prompt 构建] - H --> J[llm.py
Ollama 调用] - J --> K[Ollama API] - end - - G -->|POST /v1/completions
SSE 流式响应| H - K -->|LLM 响应| J -``` - -## 项目结构 - -``` -llm-in-text/ -├── src/ -│ ├── components/ -│ │ └── MilkdownEditor.vue # 主编辑器组件 -│ ├── plugins/ -│ │ ├── copilotPlugin.ts # ProseMirror AI 补全插件 -│ │ ├── types.ts # 类型定义 -│ │ └── index.ts # 插件导出 -│ ├── utils/ -│ │ ├── api.js # API 调用封装 -│ │ ├── config.js # 配置文件 -│ │ └── ocrCache.js # OCR 缓存管理 -│ ├── App.vue -│ └── main.js -├── backend/ -│ ├── main.py # FastAPI 服务器 -│ ├── llm.py # LLM API 调用 -│ ├── prompt.py # Prompt 构建 -│ └── requirements.txt -└── README.md -``` - -## 快速开始 - -### 环境要求 -- Node.js 18+ -- Python 3.8+ -- Ollama 服务(或其他兼容 OpenAI API 的服务) - -### 安装 - -```bash -# 前端 -npm install - -# 后端 -cd backend -pip install -r requirements.txt -``` - -### 配置 - -在 `backend/.env` 中配置: - -```env -OLLAMA_MODEL=gpt-oss:20b -OLLAMA_HOST=http://localhost:11434 -``` - -### 启动 - -```bash -# 后端(端口 8000) -cd backend -python main.py - -# 前端(端口 5173) -npm run dev -``` - -访问 http://localhost:5173 - -## API 接口 - -### POST /v1/completions - -流式获取补全建议 - -**请求:** -```json -{ - "prefix": "# Title\n\nContent ", - "suffix": "", - "languageId": "markdown" -} -``` - -**响应(SSE):** -``` -data: {"content": "here"} -data: {"content": "here is"} -data: {"done": true} -``` - -## 核心实现 - -### 后端设计 - -#### main.py - FastAPI 服务器 -- 定义 `/v1/completions` 端点 -- 使用 `StreamingResponse` 返回 SSE 流式响应 -- CORS 配置允许跨域请求 - -#### llm.py - LLM 调用封装 -- 使用 `ollama.AsyncClient` 异步调用 -- 支持 `think='high'` 思考模式 -- 返回 `content` 和 `thinking` 字段 - -#### prompt.py - Prompt 工程 -精心设计的 Prompt 模板,包含 7 条核心规则: - -| 规则 | 说明 | -|------|------| -| RULE #1 | 无缝连接 - 不重复 suffix 内容,避免"复读机"错误 | -| RULE #2 | 空白处理 - 避免双空格,正确对接标点 | -| RULE #3 | 缩进对齐 - 匹配当前缩进级别和类型 | -| RULE #4 | 列表维护 - 识别并继续任务列表、有序列表、无序列表 | -| RULE #5 | 语法闭合 - 自动闭合未完成的 Markdown 语法 | -| RULE #6 | 输出格式 - 仅输出续写文本,无解释无注释 | -| RULE #7 | 必须输出 - 始终提供有用的续写建议 | - -### 前端设计 - -#### ProseMirror Mark 系统 - -使用 ProseMirror 的 Mark 系统实现灰色建议文本: - -```typescript -// 定义 ghost mark -export const copilotGhostMark = $markSchema('copilot_ghost', () => ({ - excludes: '_', - inclusive: true, - toDOM: () => ['span', { - 'data-copilot-ghost': '', - class: 'copilot-ghost-text' - }, 0] -})) - -// CSS 样式 -.copilot-ghost-text { - color: #999; - opacity: 0.6; -} -``` - -#### copilotPlugin 核心逻辑 - -```mermaid -flowchart LR - A[用户输入] --> B{文档变化?} - B -->|是| C[清除旧建议] - C --> D[防抖 1000ms] - D --> E[发送 API 请求] - E --> F[收到建议] - F --> G[插入 Ghost Text] - - G --> H{用户操作} - H -->|Tab| I[接受建议
移除 mark] - H -->|Esc| J[拒绝建议
删除文本] - H -->|点击 Ghost| I - H -->|继续输入| J -``` - -#### 关键函数 - -| 函数 | 作用 | -|------|------| -| `scheduleFetch` | 防抖调度 API 请求 | -| `insertGhostText` | 插入带 mark 的建议文本 | -| `acceptSuggestion` | Tab 接受建议 | -| `rejectSuggestion` | Esc 拒绝建议 | -| `clearGhostText` | 清除当前建议 | - -### 数据流 - -```mermaid -sequenceDiagram - participant U as 用户 - participant E as Editor (ProseMirror) - participant P as copilotPlugin - participant A as api.js - participant B as Backend - participant L as LLM - - U->>E: 输入文本 - E->>P: view.update() - P->>P: 清除旧建议 - P->>P: 防抖 1000ms - P->>A: fetchSuggestion(prefix, suffix) - A->>B: POST /v1/completions - B->>B: build_prompt() - B->>L: ollama.chat() - L-->>B: {content, thinking} - B-->>A: SSE stream - A-->>P: suggestion text - P->>E: insertGhostText() - E-->>U: 显示灰色建议 - - alt Tab 键 - U->>P: Tab - P->>E: acceptSuggestion() - E-->>U: 建议变为正常文本 - else Esc 键 - U->>P: Esc - P->>E: rejectSuggestion() - E-->>U: 建议消失 - else 继续输入 - U->>E: 输入其他字符 - E->>P: handleKeyDown() - P->>E: clearGhostText() - end -``` - -## 设计亮点 - -1. **前后端分离**:前端只负责渲染和数据回传,后端负责 LLM 调用、Prompt 构建和数据解析 -2. **低延迟优化**:防抖机制 (1000ms) + SSE 流式响应 + AbortController 取消过期请求 -3. **ProseMirror Mark 系统**:与编辑器状态完美集成,支持 Undo/Redo -4. **多种交互方式**:Tab/Esc/点击/输入,用户体验友好 -5. **智能大小限制**:文档超过 32KB 自动禁用 AI 功能 - -## 许可证 - -MIT License diff --git a/backend/docx2pdf_bridge.cjs b/backend/docx2pdf_bridge.cjs new file mode 100644 index 0000000..063c6b1 --- /dev/null +++ b/backend/docx2pdf_bridge.cjs @@ -0,0 +1,20 @@ +const path = require('path') +const { convert } = require('docx2pdf-converter') + +function main() { + const inputPath = process.argv[2] + const outputPath = process.argv[3] + + if (!inputPath || !outputPath) { + throw new Error('缺少 DOCX 或 PDF 路径') + } + + convert(path.resolve(inputPath), path.resolve(outputPath)) +} + +try { + main() +} catch (error) { + console.error(error instanceof Error ? error.message : String(error)) + process.exit(1) +} diff --git a/backend/main.py b/backend/main.py index 14e68ac..f3bded9 100644 --- a/backend/main.py +++ b/backend/main.py @@ -3,13 +3,16 @@ import base64 import json import logging import os +import re +import shutil +import subprocess import tempfile import uuid from typing import Optional -from fastapi import FastAPI, HTTPException, Request, Security +from fastapi import FastAPI, HTTPException, Request, Security, File, UploadFile from fastapi.middleware.cors import CORSMiddleware -from fastapi.responses import JSONResponse, StreamingResponse +from fastapi.responses import JSONResponse, StreamingResponse, Response from fastapi.security import APIKeyHeader from pydantic import BaseModel @@ -81,6 +84,32 @@ class ConvertRequest(BaseModel): filename: str = "document.pdf" +ALLOWED_CONVERT_EXTENSIONS = {".txt", ".docx", ".pptx", ".pdf"} +IMAGE_MARKDOWN_RE = re.compile(r"!\[[^\]]*]\([^)]+\)") +IMAGE_HTML_RE = re.compile(r"]*>", re.IGNORECASE) + + +def _convert_docx_to_pdf(input_path: str, output_path: str) -> None: + node_executable = shutil.which("node") + if not node_executable: + raise RuntimeError("未找到 Node.js,无法转换 DOCX 为 PDF") + + bridge_path = os.path.join(os.path.dirname(__file__), "docx2pdf_bridge.cjs") + if not os.path.exists(bridge_path): + raise RuntimeError("缺少 DOCX 转 PDF 桥接脚本") + + result = subprocess.run( + [node_executable, bridge_path, input_path, output_path], + cwd=os.path.dirname(os.path.dirname(__file__)), + capture_output=True, + text=True, + ) + + if result.returncode != 0: + error_text = (result.stderr or result.stdout or "DOCX 转 PDF 失败").strip() + raise RuntimeError(error_text) + + def _preview(text: str, limit: int = 80) -> str: value = (text or "").replace("\n", "\\n") if len(value) <= limit: @@ -88,6 +117,14 @@ def _preview(text: str, limit: int = 80) -> str: return value[:limit] + "..." +def _sanitize_converted_markdown(text: str) -> str: + value = (text or "").replace("\r\n", "\n").replace("\r", "\n") + value = IMAGE_MARKDOWN_RE.sub("", value) + value = IMAGE_HTML_RE.sub("", value) + value = re.sub(r"\n{3,}", "\n\n", value) + return value.strip() + + def _sse_payload(payload: dict) -> str: return f"data: {json.dumps(payload)}\n\n" @@ -253,9 +290,9 @@ async def ocr_image(request: OCRRequest, api_key: str = Security(get_api_key)): @app.post("/v1/convert") async def convert_to_markdown(request: ConvertRequest, api_key: str = Security(get_api_key)): - """鐏忓棙鏋冩禒鎯版祮閹诡澀璐烳arkdown閺嶇厧绱?"" + """Convert file to markdown""" request_id = str(uuid.uuid4())[:8] - + try: logger.info( "[%s] /v1/convert filename=%s file_base64_chars=%d", @@ -263,53 +300,106 @@ async def convert_to_markdown(request: ConvertRequest, api_key: str = Security(g request.filename, len(request.file or ""), ) - - # 鐟欙絿鐖淏ase64閺傚洣娆㈤崘鍛啇 + + # Decode base64 file_bytes = base64.b64decode(request.file) logger.info("[%s] /v1/convert decoded file_bytes=%d", request_id, len(file_bytes)) - - # 閼惧嘲褰囬弬鍥︽閹碘晛鐫嶉崥? + + # Get file extension ext = os.path.splitext(request.filename)[1].lower() - - # 閸掓稑缂撴稉瀛樻閺傚洣娆? + + if ext not in ALLOWED_CONVERT_EXTENSIONS: + raise ValueError("仅支持 txt、docx、pptx、pdf 格式") + + if ext == ".txt": + markdown_text = _sanitize_converted_markdown(file_bytes.decode("utf-8", errors="ignore")) + return { + "markdown": markdown_text, + "filename": request.filename + } + + # Create temporary file with tempfile.NamedTemporaryFile(delete=False, suffix=ext) as tmp: tmp.write(file_bytes) tmp_path = tmp.name - + try: - # 娴h法鏁arkItDown鏉烆剚宕叉稉绡梐rkdown + # Convert using MarkItDown md = markitdown.MarkItDown() result = md.convert(tmp_path) - markdown_text = result.text_content - + markdown_text = _sanitize_converted_markdown(result.text_content) + logger.info( "[%s] /v1/convert success text_chars=%d text_preview='%s'", request_id, len(markdown_text or ""), _preview(markdown_text, 120), ) - + return { "markdown": markdown_text, "filename": request.filename } finally: - # 濞撳懐鎮婃稉瀛樻閺傚洣娆? + # Clean up temporary file if os.path.exists(tmp_path): os.unlink(tmp_path) - + except Exception as e: logger.exception("[%s] /v1/convert failed: %s", request_id, e) return JSONResponse(content={"error": str(e)}, status_code=500) + + + +@app.post("/v1/export/pdf") +async def export_pdf(file: UploadFile = File(...), api_key: str = Security(get_api_key)): + request_id = str(uuid.uuid4())[:8] + original_name = file.filename or "document.docx" + base_name = os.path.splitext(original_name)[0] or "document" + + try: + file_bytes = await file.read() + logger.info( + "[%s] /v1/export/pdf filename=%s file_bytes=%d", + request_id, + original_name, + len(file_bytes), + ) + + with tempfile.TemporaryDirectory() as temp_dir: + input_path = os.path.join(temp_dir, f"{base_name}.docx") + output_path = os.path.join(temp_dir, f"{base_name}.pdf") + + with open(input_path, "wb") as tmp_file: + tmp_file.write(file_bytes) + + await asyncio.to_thread(_convert_docx_to_pdf, input_path, output_path) + + if not os.path.exists(output_path): + raise RuntimeError("PDF 转换后未生成输出文件") + + with open(output_path, "rb") as pdf_file: + pdf_bytes = pdf_file.read() + + logger.info("[%s] /v1/export/pdf success pdf_bytes=%d", request_id, len(pdf_bytes)) + headers = { + "Content-Disposition": f'attachment; filename="{base_name}.pdf"', + } + return Response(content=pdf_bytes, media_type="application/pdf", headers=headers) + except Exception as e: + logger.exception("[%s] /v1/export/pdf failed: %s", request_id, e) + return JSONResponse(content={"error": str(e)}, status_code=500) + + if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=8001) -# TTS and STT routes +# TTS and ASR routes from tts_asr import register_tts_asr_routes register_tts_asr_routes(app) diff --git a/backend/tts_asr.py b/backend/tts_asr.py new file mode 100644 index 0000000..0acc06f --- /dev/null +++ b/backend/tts_asr.py @@ -0,0 +1,255 @@ +# TTS and ASR API for macOS Silicon with HuggingFace transformers +import asyncio +import base64 +import logging +import os +import platform + +from fastapi import APIRouter, HTTPException, Security +from pydantic import BaseModel +import numpy as np + +router = APIRouter() +logger = logging.getLogger("tts_asr") + +_tts_pipeline = None +_asr_pipeline = None +_device = None + + +def _get_device(): + global _device + if _device is not None: + return _device + + import torch + + if platform.system() == "Darwin" and hasattr(torch.backends, "mps") and torch.backends.mps.is_available(): + _device = "mps" + logger.info("[Device] 使用 MPS 加速") + elif torch.cuda.is_available(): + _device = "cuda" + logger.info("[Device] 使用 CUDA 加速") + else: + _device = "cpu" + logger.info("[Device] 使用 CPU") + return _device + + +def _device_arg(): + device = _get_device() + if device == "cuda": + return "cuda:0" + return device + + +def _get_tts_pipeline(): + global _tts_pipeline + if _tts_pipeline is not None: + return _tts_pipeline + + import torch + from transformers import pipeline + + logger.info("[TTS] 加载 Kokoro-82M 模型...") + _tts_pipeline = pipeline( + "text-to-speech", + model="hexgrad/Kokoro-82M", + trust_remote_code=True, + device=_device_arg(), + torch_dtype=torch.float16 if _get_device() != "cpu" else torch.float32, + ) + logger.info("[TTS] Kokoro-82M 模型加载完成") + return _tts_pipeline + + +def _get_asr_pipeline(): + global _asr_pipeline + if _asr_pipeline is not None: + return _asr_pipeline + + import torch + from transformers import AutoModelForSpeechSeq2Seq, AutoProcessor, pipeline + + logger.info("[ASR] 加载 Whisper large-v3-turbo 模型...") + model_id = "openai/whisper-large-v3-turbo" + model = AutoModelForSpeechSeq2Seq.from_pretrained( + model_id, + torch_dtype=torch.float16 if _get_device() != "cpu" else torch.float32, + low_cpu_mem_usage=True, + use_safetensors=True, + ) + processor = AutoProcessor.from_pretrained(model_id) + _asr_pipeline = pipeline( + "automatic-speech-recognition", + model=model, + tokenizer=processor.tokenizer, + feature_extractor=processor.feature_extractor, + torch_dtype=torch.float16 if _get_device() != "cpu" else torch.float32, + device=_device_arg(), + ) + logger.info("[ASR] Whisper large-v3-turbo 模型加载完成") + return _asr_pipeline + + +def _save_audio_to_wav(audio_data: bytes, sample_rate: int = 16000) -> str: + import tempfile + import wave + + with tempfile.NamedTemporaryFile(suffix=".wav", delete=False, mode="wb") as tmp: + with wave.open(tmp.name, "wb") as wf: + wf.setnchannels(1) + wf.setsampwidth(2) + wf.setframerate(sample_rate) + wf.writeframes(audio_data) + return tmp.name + + +def _tts_sync(text: str, voice: str = "af_bella", rate: float = 1.0) -> tuple[bytes, int]: + tts = _get_tts_pipeline() + result = tts(text, voice=voice) + audio = None + sample_rate = 24000 + if isinstance(result, dict): + audio = result.get("audio") + sample_rate = int(result.get("sampling_rate", sample_rate)) + elif isinstance(result, (list, tuple)) and result: + audio = result[0] + + if audio is None: + raise RuntimeError("Kokoro 未返回音频数据") + + if hasattr(audio, "cpu"): + audio = audio.cpu().numpy() + + duration_ms = int(len(audio) * 1000 / sample_rate) + + if audio.dtype != np.int16: + audio = (audio * 32767).astype(np.int16) + + import tempfile + import wave + + with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tmp: + output_path = tmp.name + try: + with wave.open(output_path, "wb") as wf: + wf.setnchannels(1) + wf.setsampwidth(2) + wf.setframerate(sample_rate) + wf.writeframes(audio.tobytes()) + with open(output_path, "rb") as f: + return f.read(), duration_ms + finally: + if os.path.exists(output_path): + os.unlink(output_path) + + +async def _text_to_speech(text: str, voice: str = "af_bella", rate: float = 1.0) -> tuple[bytes, int]: + return await asyncio.to_thread(_tts_sync, text, voice, rate) + + +def _asr_sync(audio_data: bytes, language: str = "zh") -> str: + import soundfile as sf + + asr = _get_asr_pipeline() + audio_path = _save_audio_to_wav(audio_data) + try: + audio_array, sample_rate = sf.read(audio_path) + result = asr( + audio_array, + sampling_rate=sample_rate, + generate_kwargs={"language": language, "task": "transcribe"}, + ) + if isinstance(result, dict): + return result.get("text", "").strip() + return str(result).strip() + finally: + if os.path.exists(audio_path): + os.unlink(audio_path) + + +async def _speech_to_text(audio_data: bytes, language: str = "zh") -> str: + return await asyncio.to_thread(_asr_sync, audio_data, language) + + +class TTSRequest(BaseModel): + text: str + voice: str = "af_bella" + rate: float = 1.0 + format: str = "wav" + + +class TTSResponse(BaseModel): + audio_base64: str + format: str + duration_ms: int + + +class ASRRequest(BaseModel): + audio_base64: str + language: str = "zh-CN" + + +class ASRResponse(BaseModel): + text: str + language: str + + +def get_api_key(api_key: str): + from backend.main import API_KEY + + if api_key != API_KEY: + raise HTTPException(status_code=403, detail="API Key 无效") + return api_key + + +@router.post("/tts", response_model=TTSResponse) +async def text_to_speech(req: TTSRequest, api_key: str = Security(get_api_key)): + request_id = str(hash(req.text))[:8] + try: + logger.info("[TTS][%s] text_chars=%d voice=%s format=%s", request_id, len(req.text), req.voice, req.format) + audio_data, duration_ms = await _text_to_speech(req.text, req.voice, req.rate) + if req.format.lower() == "mp3": + import subprocess + import tempfile + + with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tmp_in: + tmp_in.write(audio_data) + input_path = tmp_in.name + with tempfile.NamedTemporaryFile(suffix=".mp3", delete=False) as tmp_out: + output_path = tmp_out.name + try: + cmd = ["ffmpeg", "-i", input_path, "-acodec", "libmp3lame", "-ab", "128k", output_path] + result = await asyncio.to_thread(lambda: subprocess.run(cmd, capture_output=True, text=True, timeout=30)) + if result.returncode != 0: + raise RuntimeError(f"MP3 转换失败: {result.stderr}") + with open(output_path, "rb") as f: + audio_data = f.read() + finally: + for path in [input_path, output_path]: + if os.path.exists(path): + os.unlink(path) + logger.info("[TTS][%s] success duration_ms=%d", request_id, duration_ms) + return TTSResponse(audio_base64=base64.b64encode(audio_data).decode(), format=req.format, duration_ms=duration_ms) + except Exception as e: + logger.exception("[TTS] failed: %s", e) + raise HTTPException(status_code=500, detail=str(e)) + + +@router.post("/asr", response_model=ASRResponse) +async def speech_to_text(req: ASRRequest, api_key: str = Security(get_api_key)): + request_id = str(hash(req.audio_base64))[:8] + try: + logger.info("[ASR][%s] audio_base64_chars=%d language=%s", request_id, len(req.audio_base64), req.language) + audio_data = base64.b64decode(req.audio_base64) + text = await _speech_to_text(audio_data, req.language[:2]) + logger.info("[ASR][%s] success text_chars=%d", request_id, len(text)) + return ASRResponse(text=text, language=req.language) + except Exception as e: + logger.exception("[ASR] failed: %s", e) + raise HTTPException(status_code=500, detail=str(e)) + + +def register_tts_asr_routes(app): + app.include_router(router, prefix="/v1/tts-asr") diff --git a/backend/tts_stt.py b/backend/tts_stt.py deleted file mode 100644 index 33b0426..0000000 --- a/backend/tts_stt.py +++ /dev/null @@ -1,141 +0,0 @@ -# TTS and Speech Recognition API for macOS Silicon -import os -import asyncio -import logging -import base64 -from typing import Optional -from fastapi import APIRouter, UploadFile, File, HTTPException, Security -from pydantic import BaseModel -from fastapi.security import APIKeyHeader - -router = APIRouter() -api_key_header = APIKeyHeader(name="X-API-Key") -logger = logging.getLogger("tts_stt") - - -def _speak_text_macos(text: str, voice: str = "meijia", rate: float = 0.5) -> bytes: - import subprocess - import tempfile - with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tmp: - output_path = tmp.name - try: - cmd = ["say", "-v", voice, "-r", str(rate * 10), "--output-format", "WAVE", "-o", output_path, text] - result = subprocess.run(cmd, capture_output=True, text=True, timeout=30) - if result.returncode != 0: - raise Exception(f"TTS failed: {result.stderr}") - with open(output_path, "rb") as f: - audio_data = f.read() - return audio_data - finally: - if os.path.exists(output_path): - os.unlink(output_path) - - -async def _speak_text_macos_async(text: str, voice: str = "meijia", rate: float = 0.5) -> bytes: - loop = asyncio.get_event_loop() - return await loop.run_in_executor(None, _speak_text_macos, text, voice, rate) - - -def _recognize_speech_macos(audio_data: bytes, language: str = "zh-CN") -> str: - import tempfile - try: - import whisper - model = whisper.load_model("tiny") - with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tmp2: - tmp2.write(audio_data) - audio_for_whisper = tmp2.name - try: - result = model.transcribe(audio_for_whisper, language=language[:2]) - return result["text"] - finally: - if os.path.exists(audio_for_whisper): - os.unlink(audio_for_whisper) - except ImportError: - raise Exception("Whisper is required for speech recognition on macOS") - - -async def _recognize_speech_macos_async(audio_data: bytes, language: str = "zh-CN") -> str: - loop = asyncio.get_event_loop() - return await loop.run_in_executor(None, _recognize_speech_macos, audio_data, language) - - -class TTSRequest(BaseModel): - text: str - voice: str = "meijia" - rate: float = 0.5 - format: str = "wav" - - -class TTSResponse(BaseModel): - audio_base64: str - format: str - duration_ms: int - - -class STTRequest(BaseModel): - audio_base64: str - language: str = "zh-CN" - - -class STTResponse(BaseModel): - text: str - language: str - - -@router.post("/tts", response_model=TTSResponse) -async def text_to_speech(req: TTSRequest, api_key: str = Security(get_api_key)): - request_id = str(hash(req.text))[:8] - try: - logger.info("[TTS][%s] text_chars=%d voice=%s", request_id, len(req.text), req.voice) - audio_data = await _speak_text_macos_async(req.text, req.voice, req.rate) - if req.format.lower() == "mp3": - import tempfile - import subprocess - with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tmp_in: - tmp_in.write(audio_data) - input_path = tmp_in.name - with tempfile.NamedTemporaryFile(suffix=".mp3", delete=False) as tmp_out: - output_path = tmp_out.name - try: - cmd = ["ffmpeg", "-i", input_path, "-acodec", "libmp3lame", output_path] - result = subprocess.run(cmd, capture_output=True, text=True, timeout=30) - if result.returncode != 0: - raise Exception(f"MP3 conversion failed: {result.stderr}") - with open(output_path, "rb") as f: - audio_data = f.read() - finally: - for p in [input_path, output_path]: - if os.path.exists(p): - os.unlink(p) - duration_ms = len(audio_data) * 1000 // 16000 - logger.info("[TTS][%s] success duration_ms=%d", request_id, duration_ms) - return TTSResponse(audio_base64=base64.b64encode(audio_data).decode(), format=req.format, duration_ms=duration_ms) - except Exception as e: - logger.exception("[TTS] failed: %s", e) - raise HTTPException(status_code=500, detail=str(e)) - - -@router.post("/stt", response_model=STTResponse) -async def speech_to_text(req: STTRequest, api_key: str = Security(get_api_key)): - request_id = str(hash(req.audio_base64))[:8] - try: - logger.info("[STT][%s] audio_base64_chars=%d language=%s", request_id, len(req.audio_base64), req.language) - audio_data = base64.b64decode(req.audio_base64) - text = await _recognize_speech_macos_async(audio_data, req.language) - logger.info("[STT][%s] success text_chars=%d", request_id, len(text)) - return STTResponse(text=text, language=req.language) - except Exception as e: - logger.exception("[STT] failed: %s", e) - raise HTTPException(status_code=500, detail=str(e)) - - -def get_api_key(api_key: str): - from backend.main import API_KEY - if api_key != API_KEY: - from fastapi import HTTPException - raise HTTPException(status_code=403, detail="Could not validate credentials") - return api_key - - -def register_tts_stt_routes(app): - app.include_router(router, prefix="/v1/tts-stt") \ No newline at end of file diff --git a/original_readme.md b/original_readme.md deleted file mode 100644 index 20c37416155c5710ee41a9bc24ec7561e88cdd45..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10274 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.9.0" } }, + "node_modules/@blocknote/core": { + "version": "0.47.3", + "resolved": "https://registry.npmjs.org/@blocknote/core/-/core-0.47.3.tgz", + "integrity": "sha512-+YIOEXmmRXzDULYlQaycaFDofwQ/W79CWsU3GwNuRzp62QC+WpRLFLkgkoU4Pb1Vo7RvmzU1+udESdNUGhO2KA==", + "license": "MPL-2.0", + "dependencies": { + "@emoji-mart/data": "^1.2.1", + "@handlewithcare/prosemirror-inputrules": "^0.1.4", + "@shikijs/types": "^3", + "@tanstack/store": "^0.7.7", + "@tiptap/core": "^3.13.0", + "@tiptap/extension-bold": "^3.13.0", + "@tiptap/extension-code": "^3.13.0", + "@tiptap/extension-horizontal-rule": "^3.13.0", + "@tiptap/extension-italic": "^3.13.0", + "@tiptap/extension-link": "^3.13.0", + "@tiptap/extension-paragraph": "^3.13.0", + "@tiptap/extension-strike": "^3.13.0", + "@tiptap/extension-text": "^3.13.0", + "@tiptap/extension-underline": "^3.13.0", + "@tiptap/extensions": "^3.13.0", + "@tiptap/pm": "^3.13.0", + "emoji-mart": "^5.6.0", + "fast-deep-equal": "^3.1.3", + "hast-util-from-dom": "^5.0.1", + "prosemirror-highlight": "^0.13.0", + "prosemirror-model": "^1.25.4", + "prosemirror-state": "^1.4.4", + "prosemirror-tables": "^1.8.3", + "prosemirror-transform": "^1.10.5", + "prosemirror-view": "^1.41.4", + "rehype-format": "^5.0.1", + "rehype-parse": "^9.0.1", + "rehype-remark": "^10.0.1", + "rehype-stringify": "^10.0.1", + "remark-gfm": "^4.0.1", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.1.2", + "remark-stringify": "^11.0.0", + "unified": "^11.0.5", + "unist-util-visit": "^5.0.0", + "uuid": "^8.3.2", + "y-prosemirror": "^1.3.7", + "y-protocols": "^1.0.6", + "yjs": "^13.6.27" + } + }, + "node_modules/@blocknote/core/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@blocknote/react": { + "version": "0.47.3", + "resolved": "https://registry.npmjs.org/@blocknote/react/-/react-0.47.3.tgz", + "integrity": "sha512-aWDgfnf/ufgqYfXtIy8QepfrgdVaC5DfU/BAWbazb/O5ucEFfCr6lK0SUVvswK1CkxL/KBpHpdAd+uT+rfgkmg==", + "license": "MPL-2.0", + "dependencies": { + "@blocknote/core": "0.47.3", + "@emoji-mart/data": "^1.2.1", + "@floating-ui/react": "^0.27.18", + "@floating-ui/utils": "^0.2.10", + "@tanstack/react-store": "0.7.7", + "@tiptap/core": "^3.13.0", + "@tiptap/pm": "^3.13.0", + "@tiptap/react": "^3.13.0", + "@types/use-sync-external-store": "1.5.0", + "emoji-mart": "^5.6.0", + "fast-deep-equal": "^3.1.3", + "lodash.merge": "^4.6.2", + "react-icons": "^5.5.0", + "use-sync-external-store": "1.6.0" + }, + "peerDependencies": { + "react": "^18.0 || ^19.0 || >= 19.0.0-rc", + "react-dom": "^18.0 || ^19.0 || >= 19.0.0-rc" + } + }, + "node_modules/@blocknote/xl-docx-exporter": { + "version": "0.47.3", + "resolved": "https://registry.npmjs.org/@blocknote/xl-docx-exporter/-/xl-docx-exporter-0.47.3.tgz", + "integrity": "sha512-HE8JkQQ+G7wBiejFmyNlk04uUfSg6WMsKXPIHtrXK38kwXzlRTq/hu0lziZ990xc/G02/yETZNm/K1qpV9xmzA==", + "license": "GPL-3.0 OR PROPRIETARY", + "dependencies": { + "@blocknote/core": "0.47.3", + "@blocknote/xl-multi-column": "0.47.3", + "buffer": "^6.0.3", + "docx": "^9.5.1", + "image-meta": "^0.2.2" + }, + "peerDependencies": { + "react": "^18.0 || ^19.0 || >= 19.0.0-rc", + "react-dom": "^18.0 || ^19.0 || >= 19.0.0-rc" + } + }, + "node_modules/@blocknote/xl-multi-column": { + "version": "0.47.3", + "resolved": "https://registry.npmjs.org/@blocknote/xl-multi-column/-/xl-multi-column-0.47.3.tgz", + "integrity": "sha512-gK0dOXJFvwVQi3Esee0vNbk+DsSxDub0BKLDwdUBDyfk+OkS6I82MUSAmEyvCiJG0G9Vo+hSaCMduX0YdgzYAQ==", + "license": "GPL-3.0 OR PROPRIETARY", + "dependencies": { + "@blocknote/core": "0.47.3", + "@blocknote/react": "0.47.3", + "@tiptap/core": "^3.13.0", + "prosemirror-model": "^1.25.4", + "prosemirror-state": "^1.4.4", + "prosemirror-tables": "^1.8.3", + "prosemirror-transform": "^1.10.5", + "prosemirror-view": "^1.41.4", + "react-icons": "^5.5.0" + }, + "peerDependencies": { + "react": "^18.0 || ^19.0 || >= 19.0.0-rc", + "react-dom": "^18.0 || ^19.0 || >= 19.0.0-rc" + } + }, "node_modules/@braintree/sanitize-url": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-7.1.2.tgz", @@ -567,6 +691,12 @@ "w3c-keyname": "^2.2.4" } }, + "node_modules/@emoji-mart/data": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emoji-mart/data/-/data-1.2.1.tgz", + "integrity": "sha512-no2pQMWiBy6gpBEiqGeU77/bFejDqUTRY7KX+0+iur13op3bqUsXdnwoZs6Xb1zbv0gAj5VvS1PWoUUckSr5Dw==", + "license": "MIT" + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.27.5", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.5.tgz", @@ -1023,17 +1153,61 @@ "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", "license": "MIT", + "peer": true, "dependencies": { "@floating-ui/core": "^1.7.5", "@floating-ui/utils": "^0.2.11" } }, + "node_modules/@floating-ui/react": { + "version": "0.27.19", + "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.27.19.tgz", + "integrity": "sha512-31B8h5mm8YxotlE7/AU/PhNAl8eWxAmjL/v2QOxroDNkTFLk3Uu82u63N3b6TXa4EGJeeZLVcd/9AlNlVqzeog==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.1.8", + "@floating-ui/utils": "^0.2.11", + "tabbable": "^6.0.0" + }, + "peerDependencies": { + "react": ">=17.0.0", + "react-dom": ">=17.0.0" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.8.tgz", + "integrity": "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.6" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, "node_modules/@floating-ui/utils": { "version": "0.2.11", "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", "license": "MIT" }, + "node_modules/@handlewithcare/prosemirror-inputrules": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/@handlewithcare/prosemirror-inputrules/-/prosemirror-inputrules-0.1.4.tgz", + "integrity": "sha512-GMqlBeG2MKM+tXEFd2N+wIv5z4VvJTg8JtfJUrdjvFq2W6v+AW8oTgiWyFw8L3iEQwvtQcVJxU873iB0LXUNNw==", + "license": "MIT", + "dependencies": { + "prosemirror-history": "^1.4.1", + "prosemirror-transform": "^1.0.0" + }, + "peerDependencies": { + "prosemirror-model": "^1.0.0", + "prosemirror-state": "^1.0.0", + "prosemirror-view": "^1.0.0" + } + }, "node_modules/@iconify/types": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", @@ -1602,6 +1776,12 @@ "url": "https://github.com/sponsors/ocavue" } }, + "node_modules/@remirror/core-constants": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@remirror/core-constants/-/core-constants-3.0.0.tgz", + "integrity": "sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==", + "license": "MIT" + }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-rc.2", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.2.tgz", @@ -1959,6 +2139,299 @@ "win32" ] }, + "node_modules/@shikijs/types": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.23.0.tgz", + "integrity": "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + } + }, + "node_modules/@shikijs/vscode-textmate": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", + "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", + "license": "MIT" + }, + "node_modules/@tanstack/react-store": { + "version": "0.7.7", + "resolved": "https://registry.npmjs.org/@tanstack/react-store/-/react-store-0.7.7.tgz", + "integrity": "sha512-qqT0ufegFRDGSof9D/VqaZgjNgp4tRPHZIJq2+QIHkMUtHjaJ0lYrrXjeIUJvjnTbgPfSD1XgOMEt0lmANn6Zg==", + "license": "MIT", + "dependencies": { + "@tanstack/store": "0.7.7", + "use-sync-external-store": "^1.5.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@tanstack/store": { + "version": "0.7.7", + "resolved": "https://registry.npmjs.org/@tanstack/store/-/store-0.7.7.tgz", + "integrity": "sha512-xa6pTan1bcaqYDS9BDpSiS63qa6EoDkPN9RsRaxHuDdVDNntzq3xNwR5YKTU/V3SkSyC9T4YVOPh2zRQN0nhIQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tiptap/core": { + "version": "3.22.2", + "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.22.2.tgz", + "integrity": "sha512-atq35NkpeEphH6vNYJ0pTLLBA73FAbvTV9Ovd3AaTC5s99/KF5Q86zVJXvml8xPRcMGM6dLp+eSSd06oTscMSA==", + "license": "MIT", + "peer": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/pm": "^3.22.2" + } + }, + "node_modules/@tiptap/extension-bold": { + "version": "3.22.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-3.22.2.tgz", + "integrity": "sha512-bqsPJyKcT/RWse4e16U2EKhraR8a2+98TUuk1amG3yCyFJZStoO/j+pN0IqZdZZjr3WtxFyvwWp7Kc59UN+jUA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.22.2" + } + }, + "node_modules/@tiptap/extension-bubble-menu": { + "version": "3.22.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-3.22.2.tgz", + "integrity": "sha512-5hbyDOSkJwA2uh0v9Mm0Dd9bb9inx6tHBEDSH2tCB9Rm23poz3yOreB7SNX8xDMe5L0/PQesfWC14RitcmhKPg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@floating-ui/dom": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.22.2", + "@tiptap/pm": "^3.22.2" + } + }, + "node_modules/@tiptap/extension-code": { + "version": "3.22.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-3.22.2.tgz", + "integrity": "sha512-iYFY+yzfYA9MKt7nupyW/PzqL9XC2D0mC8l1z2Y10i0/fGL8NbqIYjhNUAyXGqH3QWcI+DirI66842y2OadPOg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.22.2" + } + }, + "node_modules/@tiptap/extension-floating-menu": { + "version": "3.22.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-3.22.2.tgz", + "integrity": "sha512-r0ZTeh9rNtj9Api+G0YyaB+tAKPDn7aYWg+qSrmAC5EyUPee6Zjn3zlw0q4renCeQflvNRK20xHM8zokC41jOA==", + "license": "MIT", + "optional": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@floating-ui/dom": "^1.0.0", + "@tiptap/core": "^3.22.2", + "@tiptap/pm": "^3.22.2" + } + }, + "node_modules/@tiptap/extension-horizontal-rule": { + "version": "3.22.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-3.22.2.tgz", + "integrity": "sha512-Oz8KN5KJAWV1mFNE9UIWXdMD6xa5zPf/0yLsT8V4sgaRm+VsdFKllN58BY9qCZf/kIZbaOez5KkaoeAcm0MAZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.22.2", + "@tiptap/pm": "^3.22.2" + } + }, + "node_modules/@tiptap/extension-italic": { + "version": "3.22.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-3.22.2.tgz", + "integrity": "sha512-fmtQu2HDnV3sOZPdz0+1lOLI7UtrIhusohJj2UwOLQxG8qqhLwbvWx2OQTlfblgY0z+CjLRr6ANbNDxOTIblfg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.22.2" + } + }, + "node_modules/@tiptap/extension-link": { + "version": "3.22.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-link/-/extension-link-3.22.2.tgz", + "integrity": "sha512-TXfSoKmng5pecvQUZqdsx6ICeob5V5hhYOj2vCEtjfcjWsyCndqFIl1w+Nt/yI5ehrFNOVPyj3ZvcELuuAW6pw==", + "license": "MIT", + "dependencies": { + "linkifyjs": "^4.3.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.22.2", + "@tiptap/pm": "^3.22.2" + } + }, + "node_modules/@tiptap/extension-paragraph": { + "version": "3.22.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-3.22.2.tgz", + "integrity": "sha512-EHZZzxVhvzEPDPWtRBF1YKhB+WCUjd1C2NhjHfL3Dl71PBqM3ZWA6qN7NDGPyNyGGWauui/NR/4X+5AfPqlHyA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.22.2" + } + }, + "node_modules/@tiptap/extension-strike": { + "version": "3.22.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-3.22.2.tgz", + "integrity": "sha512-YFC3elKU1L8PiGbcB6tqd/7vWPF5IbydJz0POJpHzSjstX+VfT8VsvS7ubxVuSIWQ11kGkH3mzX6LX8JHsHZxg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.22.2" + } + }, + "node_modules/@tiptap/extension-text": { + "version": "3.22.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-3.22.2.tgz", + "integrity": "sha512-J1w7JwijfSD7ah0WfiwZ/DVWCIGT9x369RM4RJc57i44mIBElj7tl1dh+N5KPGOXKUup4gr7sSJAE38lgeaDMg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.22.2" + } + }, + "node_modules/@tiptap/extension-underline": { + "version": "3.22.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-underline/-/extension-underline-3.22.2.tgz", + "integrity": "sha512-BaV6WOowxdkGTLWiU7DdZ3Twh633O4RGqwUM5dDas5LvaqL8AMWGTO8Wg9yAaaKXzd9MtKI1ZCqS/+MtzusgkQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.22.2" + } + }, + "node_modules/@tiptap/extensions": { + "version": "3.22.2", + "resolved": "https://registry.npmjs.org/@tiptap/extensions/-/extensions-3.22.2.tgz", + "integrity": "sha512-s7MZmm2Xdq+8feIXgY3v7gVpQ5ClqBZi20KheouS7KSbBlrY4fu2irYR1EGc6r1UUVaHMxEa+cx5knhx+mIPUw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.22.2", + "@tiptap/pm": "^3.22.2" + } + }, + "node_modules/@tiptap/pm": { + "version": "3.22.2", + "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.22.2.tgz", + "integrity": "sha512-G2ENwIazoSKkAnN5MN5yN91TIZNFm6TxB74kPf3Empr2k9W51Hkcier70jHGpArhgcEaL4BVreuU1PRDRwCeGw==", + "license": "MIT", + "peer": true, + "dependencies": { + "prosemirror-changeset": "^2.3.0", + "prosemirror-collab": "^1.3.1", + "prosemirror-commands": "^1.6.2", + "prosemirror-dropcursor": "^1.8.1", + "prosemirror-gapcursor": "^1.3.2", + "prosemirror-history": "^1.4.1", + "prosemirror-inputrules": "^1.4.0", + "prosemirror-keymap": "^1.2.2", + "prosemirror-markdown": "^1.13.1", + "prosemirror-menu": "^1.2.4", + "prosemirror-model": "^1.24.1", + "prosemirror-schema-basic": "^1.2.3", + "prosemirror-schema-list": "^1.5.0", + "prosemirror-state": "^1.4.3", + "prosemirror-tables": "^1.6.4", + "prosemirror-trailing-node": "^3.0.0", + "prosemirror-transform": "^1.10.2", + "prosemirror-view": "^1.38.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + } + }, + "node_modules/@tiptap/react": { + "version": "3.22.2", + "resolved": "https://registry.npmjs.org/@tiptap/react/-/react-3.22.2.tgz", + "integrity": "sha512-tyGKG69e/MkpoD/JTpVPz0XydEHxh1MSAYnLb3gRvyvBDv2r/veLea+cApkmjQaCfkKC/CWwTFXBYlOB0caSBA==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "fast-equals": "^5.3.3", + "use-sync-external-store": "^1.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "optionalDependencies": { + "@tiptap/extension-bubble-menu": "^3.22.2", + "@tiptap/extension-floating-menu": "^3.22.2" + }, + "peerDependencies": { + "@tiptap/core": "^3.22.2", + "@tiptap/pm": "^3.22.2", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "@types/react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@tiptap/react/node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, "node_modules/@types/d3": { "version": "7.4.3", "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", @@ -2249,6 +2722,12 @@ "integrity": "sha512-trgaNyfU+Xh2Tc+ABIb44a5AYUpicB3uwirOioeOkNPPbmgRNtcWyDeeFRzjPZENO9Vq8gvVqfhaaXWLlevVwg==", "license": "MIT" }, + "node_modules/@types/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", + "license": "MIT" + }, "node_modules/@types/lodash": { "version": "4.17.24", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.24.tgz", @@ -2264,6 +2743,16 @@ "@types/lodash": "*" } }, + "node_modules/@types/markdown-it": { + "version": "14.1.2", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz", + "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", + "license": "MIT", + "dependencies": { + "@types/linkify-it": "^5", + "@types/mdurl": "^2" + } + }, "node_modules/@types/mdast": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", @@ -2273,6 +2762,12 @@ "@types/unist": "*" } }, + "node_modules/@types/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", + "license": "MIT" + }, "node_modules/@types/ms": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", @@ -2301,6 +2796,26 @@ "license": "MIT", "optional": true }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "license": "MIT", + "peer": true, + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "license": "MIT", + "peer": true, + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, "node_modules/@types/trusted-types": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", @@ -2314,6 +2829,18 @@ "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", "license": "MIT" }, + "node_modules/@types/use-sync-external-store": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", + "integrity": "sha512-5dyB8nLC/qogMrlCizZnYWQTA4lnb/v+It+sqNl5YnSRAPMlIqY/X0Xn+gZw8vOL+TgTTr28VEbn3uf8fUtAkw==", + "license": "MIT" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "license": "ISC" + }, "node_modules/@upsetjs/venn.js": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@upsetjs/venn.js/-/venn.js-2.0.0.tgz", @@ -2471,6 +2998,15 @@ "node": ">=0.4.0" } }, + "node_modules/adm-zip": { + "version": "0.5.17", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.17.tgz", + "integrity": "sha512-+Ut8d9LLqwEvHHJl1+PIHqoyDxFgVN847JTVM3Izi3xHDWPE4UtzzXysMZQs64DMcrJfBeS/uoEP4AD3HQHnQQ==", + "license": "MIT", + "engines": { + "node": ">=12.0" + } + }, "node_modules/align-text": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", @@ -2950,6 +3486,26 @@ "node": ">= 0.6.0" } }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/binary-extensions": { "version": "1.13.1", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", @@ -3010,6 +3566,30 @@ "license": "MIT", "optional": true }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, "node_modules/cache-base": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", @@ -3122,6 +3702,26 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/chevrotain": { "version": "11.1.2", "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.1.2.tgz", @@ -3277,6 +3877,16 @@ "node": ">=0.10.0" } }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/commander": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", @@ -4163,6 +4773,24 @@ "node": ">=10" } }, + "node_modules/docx-preview": { + "version": "0.3.7", + "resolved": "https://registry.npmjs.org/docx-preview/-/docx-preview-0.3.7.tgz", + "integrity": "sha512-Lav69CTA/IYZPJTsKH7oYeoZjyg96N0wEJMNslGJnZJ+dMUZK85Lt5ASC79yUlD48ecWjuv+rkcmFt6EVPV0Xg==", + "license": "Apache-2.0", + "dependencies": { + "jszip": ">=3.0.0" + } + }, + "node_modules/docx2pdf-converter": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/docx2pdf-converter/-/docx2pdf-converter-2.1.1.tgz", + "integrity": "sha512-XUxY4D6HGuWVdOXuEygko1TyA1y2vejqY6iv7KrKT2St99UxreokeP/ic70XcGQuZRXTwWo5GY0haiq9HCHU4Q==", + "license": "ISC", + "dependencies": { + "adm-zip": "^0.5.16" + } + }, "node_modules/dompurify": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.3.tgz", @@ -4172,6 +4800,12 @@ "@types/trusted-types": "^2.0.7" } }, + "node_modules/emoji-mart": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/emoji-mart/-/emoji-mart-5.6.0.tgz", + "integrity": "sha512-eJp3QRe79pjwa+duv+n7+5YsNhRcMl812EcFVwrnRvYKoNPoQb5qxU8DG6Bgwji0akHdp6D4Ln6tYLG58MFSow==", + "license": "MIT" + }, "node_modules/entities": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/entities/-/entities-3.0.1.tgz", @@ -4351,6 +4985,21 @@ "node": ">=0.10.0" } }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-equals": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.4.0.tgz", + "integrity": "sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/fast-png": { "version": "6.4.0", "resolved": "https://registry.npmjs.org/fast-png/-/fast-png-6.4.0.tgz", @@ -4691,6 +5340,273 @@ "node": ">= 0.4" } }, + "node_modules/hast-util-embedded": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-embedded/-/hast-util-embedded-3.0.0.tgz", + "integrity": "sha512-naH8sld4Pe2ep03qqULEtvYr7EjrLK2QHY8KJR6RJkTUjPGObe1vnx585uzem2hGra+s1q08DZZpfgDVYRbaXA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-is-element": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-format": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/hast-util-format/-/hast-util-format-1.1.0.tgz", + "integrity": "sha512-yY1UDz6bC9rDvCWHpx12aIBGRG7krurX0p0Fm6pT547LwDIZZiNr8a+IHDogorAdreULSEzP82Nlv5SZkHZcjA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-embedded": "^3.0.0", + "hast-util-minify-whitespace": "^1.0.0", + "hast-util-phrasing": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "html-whitespace-sensitive-tag-names": "^3.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-dom": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/hast-util-from-dom/-/hast-util-from-dom-5.0.1.tgz", + "integrity": "sha512-N+LqofjR2zuzTjCPzyDUdSshy4Ma6li7p/c3pA78uTwzFgENbgbUrm2ugwsOdcjI1muO+o6Dgzp9p8WHtn/39Q==", + "license": "ISC", + "dependencies": { + "@types/hast": "^3.0.0", + "hastscript": "^9.0.0", + "web-namespaces": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-html": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hast-util-from-html/-/hast-util-from-html-2.0.3.tgz", + "integrity": "sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "devlop": "^1.1.0", + "hast-util-from-parse5": "^8.0.0", + "parse5": "^7.0.0", + "vfile": "^6.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-parse5": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz", + "integrity": "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "devlop": "^1.0.0", + "hastscript": "^9.0.0", + "property-information": "^7.0.0", + "vfile": "^6.0.0", + "vfile-location": "^5.0.0", + "web-namespaces": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-has-property": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-has-property/-/hast-util-has-property-3.0.0.tgz", + "integrity": "sha512-MNilsvEKLFpV604hwfhVStK0usFY/QmM5zX16bo7EjnAEGofr5YyI37kzopBlZJkHD4t887i+q/C8/tr5Q94cA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-is-body-ok-link": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/hast-util-is-body-ok-link/-/hast-util-is-body-ok-link-3.0.1.tgz", + "integrity": "sha512-0qpnzOBLztXHbHQenVB8uNuxTnm/QBFUOmdOSsEn7GnBtyY07+ENTWVFBAnXd/zEgd9/SUG3lRY7hSIBWRgGpQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-is-element": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz", + "integrity": "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-minify-whitespace": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hast-util-minify-whitespace/-/hast-util-minify-whitespace-1.0.1.tgz", + "integrity": "sha512-L96fPOVpnclQE0xzdWb/D12VT5FabA7SnZOUMtL1DbXmYiHJMXZvFkIZfiMmTCNJHUeO2K9UYNXoVyfz+QHuOw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-embedded": "^3.0.0", + "hast-util-is-element": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-parse-selector": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", + "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-phrasing": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/hast-util-phrasing/-/hast-util-phrasing-3.0.1.tgz", + "integrity": "sha512-6h60VfI3uBQUxHqTyMymMZnEbNl1XmEGtOxxKYL7stY2o601COo62AWAYBQR9lZbYXYSBoxag8UpPRXK+9fqSQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-embedded": "^3.0.0", + "hast-util-has-property": "^3.0.0", + "hast-util-is-body-ok-link": "^3.0.0", + "hast-util-is-element": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-html": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", + "integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-whitespace": "^3.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "stringify-entities": "^4.0.0", + "zwitch": "^2.0.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-mdast": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/hast-util-to-mdast/-/hast-util-to-mdast-10.1.2.tgz", + "integrity": "sha512-FiCRI7NmOvM4y+f5w32jPRzcxDIz+PUqDwEqn1A+1q2cdp3B8Gx7aVrXORdOKjMNDQsD1ogOr896+0jJHW1EFQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "hast-util-phrasing": "^3.0.0", + "hast-util-to-html": "^9.0.0", + "hast-util-to-text": "^4.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "mdast-util-to-string": "^4.0.0", + "rehype-minify-whitespace": "^6.0.0", + "trim-trailing-lines": "^2.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-text": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/hast-util-to-text/-/hast-util-to-text-4.0.2.tgz", + "integrity": "sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "hast-util-is-element": "^3.0.0", + "unist-util-find-after": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hastscript": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz", + "integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-parse-selector": "^4.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/home-or-tmp": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/home-or-tmp/-/home-or-tmp-1.0.0.tgz", @@ -4705,6 +5621,26 @@ "node": ">=0.10.0" } }, + "node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/html-whitespace-sensitive-tag-names": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-whitespace-sensitive-tag-names/-/html-whitespace-sensitive-tag-names-3.0.1.tgz", + "integrity": "sha512-q+310vW8zmymYHALr1da4HyXUQ0zgiIwIicEfotYPWGN0OJVEN/58IJ3A4GBYcEq3LGAZqKb+ugvP0GNB9CEAA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/html2canvas": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", @@ -4741,6 +5677,32 @@ "node": ">=0.10.0" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/image-meta": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/image-meta/-/image-meta-0.2.2.tgz", + "integrity": "sha512-3MOLanc3sb3LNGWQl1RlQlNWURE5g32aUphrDyFeCsxBTk08iE3VNe4CwsUZ0Qs1X+EfX0+r29Sxdpza4B+yRA==", + "license": "MIT" + }, "node_modules/immediate": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", @@ -5042,6 +6004,16 @@ "node": ">=0.10.0" } }, + "node_modules/isomorphic.js": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.2.5.tgz", + "integrity": "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==", + "license": "MIT", + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + } + }, "node_modules/js-tokens": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-1.0.1.tgz", @@ -5196,6 +6168,27 @@ "node": ">=0.10.0" } }, + "node_modules/lib0": { + "version": "0.2.117", + "resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.117.tgz", + "integrity": "sha512-DeXj9X5xDCjgKLU/7RR+/HQEVzuuEUiwldwOGsHK/sfAfELGWEyTcf0x+uOvCvK3O2zPmZePXWL85vtia6GyZw==", + "license": "MIT", + "dependencies": { + "isomorphic.js": "^0.2.4" + }, + "bin": { + "0ecdsa-generate-keypair": "bin/0ecdsa-generate-keypair.js", + "0gentesthtml": "bin/gentesthtml.js", + "0serve": "bin/0serve.js" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + } + }, "node_modules/lie": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", @@ -5214,6 +6207,12 @@ "uc.micro": "^1.0.1" } }, + "node_modules/linkifyjs": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.3.2.tgz", + "integrity": "sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA==", + "license": "MIT" + }, "node_modules/lodash": { "version": "3.10.1", "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz", @@ -5227,6 +6226,12 @@ "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", "license": "MIT" }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "license": "MIT" + }, "node_modules/longest": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", @@ -5534,6 +6539,27 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/mdast-util-to-markdown": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", @@ -6604,6 +7630,30 @@ "node": ">=0.10.0" } }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/pascalcase": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", @@ -6820,6 +7870,16 @@ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "license": "MIT" }, + "node_modules/property-information": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/prosemirror-changeset": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/prosemirror-changeset/-/prosemirror-changeset-2.4.0.tgz", @@ -6829,6 +7889,15 @@ "prosemirror-transform": "^1.0.0" } }, + "node_modules/prosemirror-collab": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/prosemirror-collab/-/prosemirror-collab-1.3.1.tgz", + "integrity": "sha512-4SnynYR9TTYaQVXd/ieUvsVV4PDMBzrq2xPUWutHivDuOshZXqQ5rGbZM84HEaXKbLdItse7weMGOUdDVcLKEQ==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.0.0" + } + }, "node_modules/prosemirror-commands": { "version": "1.7.1", "resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.7.1.tgz", @@ -6878,6 +7947,59 @@ "prosemirror-view": "^1.0.0" } }, + "node_modules/prosemirror-highlight": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/prosemirror-highlight/-/prosemirror-highlight-0.13.1.tgz", + "integrity": "sha512-41EwMJDUeFBxizPP1/msQBjDke1YyaTy40w3CGoc7fjXboDBgyhz2LWThwaygL9LkDvBqn4pwBJg1PNtrNilGg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ocavue" + }, + "peerDependencies": { + "@shikijs/types": "^1.29.2 || ^2.0.0 || ^3.0.0", + "@types/hast": "^3.0.0", + "highlight.js": "^11.9.0", + "lowlight": "^3.1.0", + "prosemirror-model": "^1.19.3", + "prosemirror-state": "^1.4.3", + "prosemirror-transform": "^1.8.0", + "prosemirror-view": "^1.32.4", + "refractor": "^5.0.0", + "sugar-high": "^0.6.1 || ^0.7.0 || ^0.8.0 || ^0.9.0" + }, + "peerDependenciesMeta": { + "@shikijs/types": { + "optional": true + }, + "@types/hast": { + "optional": true + }, + "highlight.js": { + "optional": true + }, + "lowlight": { + "optional": true + }, + "prosemirror-model": { + "optional": true + }, + "prosemirror-state": { + "optional": true + }, + "prosemirror-transform": { + "optional": true + }, + "prosemirror-view": { + "optional": true + }, + "refractor": { + "optional": true + }, + "sugar-high": { + "optional": true + } + } + }, "node_modules/prosemirror-history": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/prosemirror-history/-/prosemirror-history-1.5.0.tgz", @@ -6910,6 +8032,79 @@ "w3c-keyname": "^2.2.0" } }, + "node_modules/prosemirror-markdown": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/prosemirror-markdown/-/prosemirror-markdown-1.13.4.tgz", + "integrity": "sha512-D98dm4cQ3Hs6EmjK500TdAOew4Z03EV71ajEFiWra3Upr7diytJsjF4mPV2dW+eK5uNectiRj0xFxYI9NLXDbw==", + "license": "MIT", + "dependencies": { + "@types/markdown-it": "^14.0.0", + "markdown-it": "^14.0.0", + "prosemirror-model": "^1.25.0" + } + }, + "node_modules/prosemirror-markdown/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/prosemirror-markdown/node_modules/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "license": "MIT", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, + "node_modules/prosemirror-markdown/node_modules/markdown-it": { + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.1.tgz", + "integrity": "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.4.0", + "linkify-it": "^5.0.0", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/prosemirror-markdown/node_modules/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "license": "MIT" + }, + "node_modules/prosemirror-markdown/node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "license": "MIT" + }, + "node_modules/prosemirror-menu": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/prosemirror-menu/-/prosemirror-menu-1.3.0.tgz", + "integrity": "sha512-TImyPXCHPcDsSka2/lwJ6WjTASr4re/qWq1yoTTuLOqfXucwF6VcRa2LWCkM/EyTD1UO3CUwiH8qURJoWJRxwg==", + "license": "MIT", + "dependencies": { + "crelt": "^1.0.0", + "prosemirror-commands": "^1.0.0", + "prosemirror-history": "^1.0.0", + "prosemirror-state": "^1.0.0" + } + }, "node_modules/prosemirror-model": { "version": "1.25.4", "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.4.tgz", @@ -6933,6 +8128,15 @@ "url": "https://github.com/sponsors/ocavue" } }, + "node_modules/prosemirror-schema-basic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/prosemirror-schema-basic/-/prosemirror-schema-basic-1.2.4.tgz", + "integrity": "sha512-ELxP4TlX3yr2v5rM7Sb70SqStq5NvI15c0j9j/gjsrO5vaw+fnnpovCLEGIcpeGfifkuqJwl4fon6b+KdrODYQ==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.25.0" + } + }, "node_modules/prosemirror-schema-list": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/prosemirror-schema-list/-/prosemirror-schema-list-1.5.1.tgz", @@ -6969,6 +8173,33 @@ "prosemirror-view": "^1.41.4" } }, + "node_modules/prosemirror-trailing-node": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/prosemirror-trailing-node/-/prosemirror-trailing-node-3.0.0.tgz", + "integrity": "sha512-xiun5/3q0w5eRnGYfNlW1uU9W6x5MoFKWwq/0TIRgt09lv7Hcser2QYV8t4muXbEr+Fwo0geYn79Xs4GKywrRQ==", + "license": "MIT", + "dependencies": { + "@remirror/core-constants": "3.0.0", + "escape-string-regexp": "^4.0.0" + }, + "peerDependencies": { + "prosemirror-model": "^1.22.1", + "prosemirror-state": "^1.4.2", + "prosemirror-view": "^1.33.8" + } + }, + "node_modules/prosemirror-trailing-node/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/prosemirror-transform": { "version": "1.12.0", "resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.12.0.tgz", @@ -7015,6 +8246,15 @@ } } }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/q": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", @@ -7072,6 +8312,38 @@ "node": ">=0.10.0" } }, + "node_modules/react": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.4" + } + }, + "node_modules/react-icons": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.6.0.tgz", + "integrity": "sha512-RH93p5ki6LfOiIt0UtDyNg/cee+HLVR6cHHtW3wALfo+eOHTp8RnU2kRkI6E+H19zMIs03DyxUG/GfZMOGvmiA==", + "license": "MIT", + "peerDependencies": { + "react": "*" + } + }, "node_modules/readable-stream": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", @@ -7477,6 +8749,81 @@ "regjsparser": "bin/parser" } }, + "node_modules/rehype-format": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/rehype-format/-/rehype-format-5.0.1.tgz", + "integrity": "sha512-zvmVru9uB0josBVpr946OR8ui7nJEdzZobwLOOqHb/OOD88W0Vk2SqLwoVOj0fM6IPCCO6TaV9CvQvJMWwukFQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-format": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-minify-whitespace": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/rehype-minify-whitespace/-/rehype-minify-whitespace-6.0.2.tgz", + "integrity": "sha512-Zk0pyQ06A3Lyxhe9vGtOtzz3Z0+qZ5+7icZ/PL/2x1SHPbKao5oB/g/rlc6BCTajqBb33JcOe71Ye1oFsuYbnw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-minify-whitespace": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-parse": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/rehype-parse/-/rehype-parse-9.0.1.tgz", + "integrity": "sha512-ksCzCD0Fgfh7trPDxr2rSylbwq9iYDkSn8TCDmEJ49ljEUBxDVCzCHv7QNzZOfODanX4+bWQ4WZqLCRWYLfhag==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-from-html": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-remark": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/rehype-remark/-/rehype-remark-10.0.1.tgz", + "integrity": "sha512-EmDndlb5NVwXGfUa4c9GPK+lXeItTilLhE6ADSaQuHr4JUlKw9MidzGzx4HpqZrNCt6vnHmEifXQiiA+CEnjYQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "hast-util-to-mdast": "^10.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-stringify": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/rehype-stringify/-/rehype-stringify-10.0.1.tgz", + "integrity": "sha512-k9ecfXHmIPuFVI61B9DeLPN0qFHfawM6RsuX48hoqlaKSF61RskNjSm1lI8PhBEM0MRdLxVVm4WmTqJQccH9mA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-to-html": "^9.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/remark": { "version": "15.0.1", "resolved": "https://registry.npmjs.org/remark/-/remark-15.0.1.tgz", @@ -7558,6 +8905,23 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/remark-rehype": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", + "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/remark-stringify": { "version": "11.0.0", "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", @@ -7799,6 +9163,12 @@ "node": ">=11.0.0" } }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, "node_modules/set-value": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", @@ -8042,6 +9412,16 @@ "license": "MIT", "optional": true }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/split-string": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", @@ -8123,6 +9503,20 @@ "safe-buffer": "~5.1.0" } }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/stringmap": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/stringmap/-/stringmap-0.2.2.tgz", @@ -8195,6 +9589,12 @@ "node": ">=12.0.0" } }, + "node_modules/tabbable": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.4.0.tgz", + "integrity": "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==", + "license": "MIT" + }, "node_modules/text-segmentation": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", @@ -8303,6 +9703,16 @@ "node": ">=0.10.0" } }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/trim-right": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", @@ -8313,6 +9723,16 @@ "node": ">=0.10.0" } }, + "node_modules/trim-trailing-lines": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/trim-trailing-lines/-/trim-trailing-lines-2.1.0.tgz", + "integrity": "sha512-5UR5Biq4VlVOtzqkm2AZlgvSlDJtME46uV0br0gENbwN4l5+mMKT4b9gJKqWtuL2zAIqajGJGuvbCbcAJUZqBg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/trough": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", @@ -8400,6 +9820,20 @@ "node": ">=0.10.0" } }, + "node_modules/unist-util-find-after": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-find-after/-/unist-util-find-after-5.0.0.tgz", + "integrity": "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/unist-util-is": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", @@ -8413,6 +9847,19 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/unist-util-remove-position": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-5.0.0.tgz", @@ -8549,6 +9996,15 @@ "node": ">=0.10.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/user-home": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/user-home/-/user-home-1.1.1.tgz", @@ -8604,6 +10060,20 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/vfile-location": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.3.tgz", + "integrity": "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/vfile-message": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", @@ -8827,6 +10297,16 @@ "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", "license": "MIT" }, + "node_modules/web-namespaces": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", + "integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/window-size": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.4.tgz", @@ -8875,6 +10355,51 @@ "xml-js": "bin/cli.js" } }, + "node_modules/y-prosemirror": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/y-prosemirror/-/y-prosemirror-1.3.7.tgz", + "integrity": "sha512-NpM99WSdD4Fx4if5xOMDpPtU3oAmTSjlzh5U4353ABbRHl1HtAFUx6HlebLZfyFxXN9jzKMDkVbcRjqOZVkYQg==", + "license": "MIT", + "dependencies": { + "lib0": "^0.2.109" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=8.0.0" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + }, + "peerDependencies": { + "prosemirror-model": "^1.7.1", + "prosemirror-state": "^1.2.3", + "prosemirror-view": "^1.9.10", + "y-protocols": "^1.0.1", + "yjs": "^13.5.38" + } + }, + "node_modules/y-protocols": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/y-protocols/-/y-protocols-1.0.7.tgz", + "integrity": "sha512-YSVsLoXxO67J6eE/nV4AtFtT3QEotZf5sK5BHxFBXso7VDUT3Tx07IfA6hsu5Q5OmBdMkQVmFZ9QOA7fikWvnw==", + "license": "MIT", + "peer": true, + "dependencies": { + "lib0": "^0.2.85" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=8.0.0" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + }, + "peerDependencies": { + "yjs": "^13.0.0" + } + }, "node_modules/y18n": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.2.tgz", @@ -8897,6 +10422,24 @@ "y18n": "^3.2.0" } }, + "node_modules/yjs": { + "version": "13.6.30", + "resolved": "https://registry.npmjs.org/yjs/-/yjs-13.6.30.tgz", + "integrity": "sha512-vv/9h42eCMC81ZHDFswuu/MKzkl/vyq1BhaNGfHyOonwlG4CJbQF4oiBBJPvfdeCt/PlVDWh7Nov9D34YY09uQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "lib0": "^0.2.99" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=8.0.0" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + } + }, "node_modules/zwitch": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", diff --git a/package.json b/package.json index e8f7817..a7dd887 100644 --- a/package.json +++ b/package.json @@ -11,13 +11,17 @@ "check": "npm run build" }, "dependencies": { + "@blocknote/xl-docx-exporter": "^0.47.3", "@milkdown/core": "^7.18.0", "@milkdown/crepe": "^7.18.0", "@milkdown/kit": "^7.18.0", "@milkdown/theme-nord": "^7.18.0", "@milkdown/vue": "^7.18.0", "docx": "^9.6.0", + "docx-preview": "^0.3.7", + "docx2pdf-converter": "^2.1.1", "html2pdf.js": "^0.14.0", + "jspdf": "^4.2.1", "katex": "^0.16.9", "markdown-it": "^13.0.0", "markdown-it-math": "^3.0.2", diff --git a/src/components/DocBlockCrepe.vue b/src/components/DocBlockCrepe.vue index 88e9583..e1e7622 100644 --- a/src/components/DocBlockCrepe.vue +++ b/src/components/DocBlockCrepe.vue @@ -1,250 +1,329 @@ diff --git a/src/components/MilkdownEditor.vue b/src/components/MilkdownEditor.vue index 794f912..91e1124 100644 --- a/src/components/MilkdownEditor.vue +++ b/src/components/MilkdownEditor.vue @@ -35,8 +35,9 @@ - + - +
+ +
+ + + +
+