diff --git a/frontend-web/docs/ai-writing-右侧编辑器-滑词与段落菜单.md b/frontend-web/docs/ai-writing-右侧编辑器-滑词与段落菜单.md new file mode 100644 index 00000000..ea9c4d50 --- /dev/null +++ b/frontend-web/docs/ai-writing-右侧编辑器-滑词与段落菜单.md @@ -0,0 +1,228 @@ +# AI 写作 · 右侧文本编辑器(滑词 / 段落菜单) + +> 说明 AI 写作页、开放画布中文稿右侧编辑器的实现方式:**编辑器本体基于 Tiptap v3**;选中文本浮层(「滑词」)与段落悬停按钮为**自研 React 组件**,未使用 `@tiptap/extension-bubble-menu` 官方 BubbleMenu。 + +--- + +## 一、功能概览 + +| 交互 | 用户感知 | 组件 | 触发方式 | +|------|----------|------|----------| +| **滑词** | 拖选一段文字后,选区上方出现工具条(AI 改写、加粗、斜体、下划线、H1、复制) | `WritingBubbleMenu` | Tiptap `selectionUpdate` + 浏览器 `window.getSelection()` | +| **段落菜单** | 鼠标移入某段/标题/引用块时,段落左侧出现 ✨ 按钮,点击可 AI 改写整段 | `WritingParagraphMenu` | 编辑器 DOM `mousemove` + ProseMirror `posAtCoords` | +| **AI 改写面板** | 点击「AI 改写」后弹出润色/扩写等操作与预览 | `WritingAiMenu` / `WritingRewritePreview` | 由 `TextArtifactRenderer` + `useWritingRewrite` 串联 | + +画布线程页(`/page/canvas/:thread_id`)与 AI 写作页(`/page/workspace/ai-writing`)右侧草稿**共用同一套** `TextArtifactRenderer` 与菜单组件。 + +--- + +## 二、在 AI 写作中的挂载路径 + +```mermaid +flowchart TB + AIWritingPage --> CanvasResizableLayout + CanvasResizableLayout --> AIWritingDraftPanel + AIWritingDraftPanel --> DraftStreamingPreview + AIWritingDraftPanel --> TextArtifactRenderer + DraftStreamingPreview["DraftStreamingPreview
(流式只读 Markdown)"] + TextArtifactRenderer --> WritingBubbleMenu + TextArtifactRenderer --> WritingParagraphMenu + TextArtifactRenderer --> TiptapEditor + TextArtifactRenderer --> WritingAiMenu +``` + +| 阶段 | 右侧展示 | 滑词 / 段落菜单 | +|------|----------|-----------------| +| 草稿流式生成中(`writing_draft` / `revising` 且有 section 流) | `DraftStreamingPreview`(ReactMarkdown 只读) | **不可用** | +| 成稿可编辑(`isDraftEditable === true`) | `TextArtifactRenderer` + Tiptap | **可用** | +| 流式结束但尚未 editable | `TextArtifactRenderer` 且 `isStreaming={true}` | **不可用**(`editable: false`) | + +相关状态由 [CanvasUIContext.tsx](../src/open-canvas/contexts/CanvasUIContext.tsx) 的 `isDraftStreaming`、`isDraftEditable` 控制;注释中「禁用编辑/润色/滑词」即指生成完成前不进入可编辑渲染器。 + +入口文件: + +- [AIWritingDraftPanel.tsx](../src/open-canvas/components/ai-writing/AIWritingDraftPanel.tsx) — 决定流式预览 vs `TextArtifactRenderer` +- [TextArtifactRenderer.tsx](../src/open-canvas/components/artifact/TextArtifactRenderer.tsx) — 挂载编辑器与两套浮层菜单 + +--- + +## 三、技术栈:编辑器 vs 浮层 + +### 3.1 Tiptap 编辑器(有使用) + +| 包 | 用途 | +|----|------| +| `@tiptap/react` | `useEditor`、`EditorContent` | +| `@tiptap/starter-kit` | 段落、标题、加粗、斜体、下划线等 | +| `@tiptap/extension-placeholder` | 空文档占位符 | +| `tiptap-markdown` | Markdown ↔ 文档互转 | +| 自研 `AiHighlightExtension` | AI 改写前高亮选区(ProseMirror Decoration) | + +扩展注册见 [TiptapExtensions.ts](../src/open-canvas/editor/TiptapExtensions.ts);实例创建见 [useWritingEditor.ts](../src/open-canvas/hooks/useWritingEditor.ts)。 + +### 3.2 浮层菜单(自研,非 Tiptap 官方插件) + +| 包 | 是否用于滑词/段落菜单 | +|----|----------------------| +| `@tiptap/extension-bubble-menu` | **未使用**(`package.json` 中有依赖,代码未 import) | +| `@tiptap/react` 的 `` | **未使用** | + +两套菜单均采用: + +1. 监听 Tiptap / DOM 事件更新位置与选区; +2. `createPortal(..., document.body)` 挂到 `document.body`,避免被右侧滚动容器 `overflow` 裁切; +3. `position: fixed` + `getBoundingClientRect()` 定位。 + +命名上的 `WritingBubbleMenu` 仅表示「气泡式选区工具条」,实现上与 Tiptap 官方 Bubble Menu 扩展无关。 + +--- + +## 四、核心文件索引 + +| 文件 | 职责 | +|------|------| +| [TiptapEditor.tsx](../src/open-canvas/editor/TiptapEditor.tsx) | `EditorContent` 渲染容器 | +| [TiptapExtensions.ts](../src/open-canvas/editor/TiptapExtensions.ts) | StarterKit、Placeholder、Markdown、AiHighlight | +| [AiHighlightExtension.ts](../src/open-canvas/editor/AiHighlightExtension.ts) | `setAiHighlight` / `clearAiHighlight` 命令 | +| [useWritingEditor.ts](../src/open-canvas/hooks/useWritingEditor.ts) | 创建 editor、同步外部 markdown、editable | +| [WritingBubbleMenu.tsx](../src/open-canvas/components/artifact/WritingBubbleMenu.tsx) | **滑词** 选区浮层 | +| [WritingParagraphMenu.tsx](../src/open-canvas/components/artifact/WritingParagraphMenu.tsx) | **段落** 左侧 ✨ 按钮 | +| [WritingAiMenu.tsx](../src/open-canvas/components/artifact/WritingAiMenu.tsx) | AI 改写操作菜单 | +| [WritingRewritePreview.tsx](../src/open-canvas/components/artifact/WritingRewritePreview.tsx) | 改写结果预览 | +| [useWritingRewrite.ts](../src/open-canvas/hooks/useWritingRewrite.ts) | 调用润色 API、写回编辑器 | +| [apply-rewrite.ts](../src/open-canvas/utils/apply-rewrite.ts) | 将改写结果应用到 ProseMirror 选区 | +| [CanvasUIContext.tsx](../src/open-canvas/contexts/CanvasUIContext.tsx) | `writingSelection` 全局状态 | +| [selection.ts](../src/open-canvas/types/selection.ts) | `WritingSelection` 类型定义 | + +--- + +## 五、滑词:`WritingBubbleMenu` + +### 5.1 行为 + +- 用户在编辑器内拖选非空文本时显示浮层;选区为空或位于 `codeBlock` 内时隐藏。 +- 浮层按钮:`AI 改写`、`B` / `I` / `U`、`H1`、复制。 +- 按钮 `onMouseDown` 使用 `e.preventDefault()`,避免点击时编辑器失焦导致选区丢失。 + +### 5.2 实现要点 + +```text +editor.on("selectionUpdate") + → 读取 editor.state.selection (from, to, empty) + → window.getSelection() + Range.getBoundingClientRect() 算屏幕坐标 + → setWritingSelection({ from, to, text }) 写入 CanvasUIContext + → createPortal 渲染工具条,useLayoutEffect 做贴边 / 上方空间不足时翻到选区下方 +``` + +关键代码位置:[WritingBubbleMenu.tsx](../src/open-canvas/components/artifact/WritingBubbleMenu.tsx) 中 `useEffect` 监听 `selectionUpdate` 与 `blur`。 + +点击「AI 改写」时: + +1. `editor.commands.setAiHighlight(from, to)` 高亮选区; +2. 调用父组件传入的 `onAiRewrite()`,由 `TextArtifactRenderer` 打开 `WritingAiMenu`。 + +--- + +## 六、段落菜单:`WritingParagraphMenu` + +### 6.1 行为 + +- 鼠标在编辑器内移动时,检测光标下的**块级节点**; +- 仅对 `paragraph`、`heading`、`blockquote` 且文本非空时,在块左侧显示 ✨ 按钮; +- 鼠标离开编辑器或移出可改写块后,延迟约 **380ms** 隐藏(`HIDE_DELAY`),并在按钮与正文之间留有 hover 桥接区,避免移向按钮时闪烁消失。 + +### 6.2 实现要点 + +```text +editor.view.dom mousemove + → editor.view.posAtCoords({ left, top }) + → doc.resolve(pos).before(depth) 定位块起点 + → editor.view.nodeDOM(nodeStart) 取 DOM 矩形 + → createPortal 固定定位按钮在块左侧 (BTN_SIZE=24, BTN_GAP=6) +``` + +点击 ✨ 时: + +1. `setTextSelection` 选中整段 ProseMirror 范围(`nodeFrom` ~ `nodeTo`); +2. `setAiHighlight` + `setWritingSelection`; +3. 触发与滑词相同的 `onAiRewrite()` 流程。 + +关键代码位置:[WritingParagraphMenu.tsx](../src/open-canvas/components/artifact/WritingParagraphMenu.tsx)。 + +--- + +## 七、AI 改写数据流 + +```mermaid +sequenceDiagram + participant User + participant Menu as BubbleMenu / ParagraphMenu + participant Ctx as CanvasUIContext + participant Renderer as TextArtifactRenderer + participant API as rewrite API + + User->>Menu: 选中文本或点段落 ✨ + Menu->>Ctx: setWritingSelection + Menu->>Renderer: onAiRewrite() + Renderer->>Renderer: phase = menu, WritingAiMenu + User->>Renderer: 选择润色/扩写等 + Renderer->>API: useWritingRewrite.rewrite + API-->>Renderer: 流式结果 + Renderer->>Renderer: WritingRewritePreview + User->>Renderer: 接受改写 + Renderer->>Renderer: applyReplace / applyInsert +``` + +- 选区信息类型:`WritingSelection`(`from`、`to`、`text`),见 [selection.ts](../src/open-canvas/types/selection.ts)。 +- 与画布聊天侧选区 `CanvasTextSelection` 分离;后者用于快捷操作栏等,不用于 Tiptap 改写。 + +--- + +## 八、与顶部工具栏、目录栏的关系 + +同一 `TextArtifactRenderer` 编辑模式下还包含: + +| 组件 | 作用 | +|------|------| +| [WritingToolbar.tsx](../src/open-canvas/components/artifact/WritingToolbar.tsx) | 顶部固定格式栏、导出等 | +| [DraftOutlineSidebar.tsx](../src/open-canvas/components/artifact/DraftOutlineSidebar.tsx) | 左侧标题目录,点击滚动到对应 heading | + +滑词 / 段落菜单与工具栏互不替代:工具栏面向全局格式;浮层面向**当前选区或当前段落**的快捷操作与 AI 改写入口。 + +--- + +## 九、开发与改造建议 + +### 9.1 若要改用官方 Bubble Menu + +1. 在 [TiptapExtensions.ts](../src/open-canvas/editor/TiptapExtensions.ts) 或组件层引入 `@tiptap/react` 的 `BubbleMenu`(或注册 `@tiptap/extension-bubble-menu`); +2. 将 [WritingBubbleMenu.tsx](../src/open-canvas/components/artifact/WritingBubbleMenu.tsx) 内手写 `selectionUpdate` + Portal 逻辑替换为官方组件的 `shouldShow` / `tippyOptions`(v3 API 以官方文档为准); +3. **段落左侧按钮**仍建议保留自研 `WritingParagraphMenu`(官方 Bubble Menu 只覆盖选区,不覆盖 hover 段落场景)。 + +### 9.2 新增浮层按钮 + +- **滑词**:在 `WritingBubbleMenu` 的 `BubbleButton` 列表中增加,使用 `editor.chain().focus()....run()`。 +- **段落**:在 `WritingParagraphMenu` 的 `handleClick` 或拆出多按钮布局;注意 `REWRITABLE_TYPES` 与 hover 延迟逻辑。 + +### 9.3 流式阶段禁止交互 + +不要在 `DraftStreamingPreview` 中挂载 Tiptap 菜单:流式阶段 token 高频更新,挂载 `useWritingEditor` 易引发更新风暴(见 [DraftStreamingPreview.tsx](../src/open-canvas/components/ai-writing/DraftStreamingPreview.tsx) 文件头注释)。应等 `isDraftEditable` 后再走 `TextArtifactRenderer`。 + +--- + +## 十、相关文档 + +- [前端开发.md](./前端开发.md) §25.4 — Canvas / AI 写作路由与组件总表 +- [ai-writing-用户自带大纲-测试用例.md](./ai-writing-用户自带大纲-测试用例.md) — AI 写作业务流程测试(含右侧草稿层级校验) + +--- + +## 十一、依赖版本(摘录) + +以 [package.json](../package.json) 为准,编辑器相关主要为: + +- `@tiptap/core`、`@tiptap/react`、`@tiptap/starter-kit`:`^3.23.4` +- `@tiptap/extension-bubble-menu`:已安装但未接入滑词实现 +- `tiptap-markdown`:Markdown 扩展 + +升级 Tiptap 大版本前,请对照官方迁移指南,并重点回归:`WritingBubbleMenu` 选区坐标、`WritingParagraphMenu` 的 `posAtCoords`、以及 `AiHighlightExtension` 与 StarterKit 扩展名冲突(勿重复注册 `underline`)。 diff --git a/frontend-web/docs/ai-writing-用户自带大纲-测试用例.md b/frontend-web/docs/ai-writing-用户自带大纲-测试用例.md new file mode 100644 index 00000000..da73fbeb --- /dev/null +++ b/frontend-web/docs/ai-writing-用户自带大纲-测试用例.md @@ -0,0 +1,274 @@ +# AI 写作 ·「用户自带大纲」手动测试用例 + +> 功能:用户在 AI 写作表单粘贴已有大纲后,系统**完全按用户大纲生成正文**,兼容一/二/三级目录。 +> 入口页面:`http://localhost:5173/#/page/workspace/ai-writing`(或 `2026` 端口经 nginx) + +--- + +## 一、测试目标 + +验证三件事: +1. **识别**——用户粘贴的各种格式大纲,能被正确解析成 章节(##) → 小节(###) → 子要点(####) 三级结构。 +2. **保真**——生成的大纲标题、层级**与用户粘贴的完全一致**,不增、不删、不改写。 +3. **兼容**——只有一级 / 一+二级 / 一+二+三级 三种深度都能正确生成对应层级的正文。 + +--- + +## 二、前置条件 + +| 项 | 要求 | +|----|------| +| 服务 | 后端(Gateway 8001)+ 前端(Vite 5174/5173)均已启动,可正常发起一次普通 AI 写作 | +| 模型 | 写作模型已在表单可选、能正常输出(先用不带大纲的普通流程跑通一次作为基线) | +| 账号 | 已登录(或无鉴权模式) | +| 入口 | 进入 `/page/workspace/ai-writing`,能看到「写作配置选项」表单 | + +--- + +## 三、通用操作步骤 + +除非用例另有说明,每条用例都按以下步骤执行: + +1. 进入 AI 写作页面,点开**「已有大纲(可选)」** 旁的「我已有大纲,直接粘贴 ▾」。 +2. 在「写作意图」填一句话(如:`写一篇关于新能源汽车出口的分析文章`)。 +3. 把用例里的**「输入大纲」**整段粘贴进「已有大纲」输入框。 +4. 文章类型 / 目标字数 / 素材来源保持默认即可。 +5. 点击「开始 AI 写作」。 +6. 等待流程走到 **PAUSE-2「大纲确认」**,对照**「预期大纲结构」**核对左侧/卡片里的大纲。 +7. 点「确认」继续,等正文生成完,对照**「预期正文层级」**核对右侧草稿的标题层级。 + +> 关键校验点都在第 6 步(大纲确认卡)。正文层级(第 7 步)作为二次确认。 + +--- + +## 四、测试用例 + +### TC-01 Markdown 三级 + 文章标题 + +**输入大纲:** +``` +# 新能源汽车出口为何高速增长 +## 引言 +### 出口数据创新高 +#### 同比增幅 +#### 全球排名变化 +### 市场背景 +## 增长动因 +### 成本优势 +``` + +**预期大纲结构:** +- 文章标题 = `新能源汽车出口为何高速增长`(首个孤立 `#` 被识别为标题,不作为章节) +- 章节1 `引言` + - 小节 `出口数据创新高` → 子要点:`同比增幅`、`全球排名变化` + - 小节 `市场背景`(无子要点) +- 章节2 `增长动因` + - 小节 `成本优势` + +**预期正文层级:** `#` 文章标题 → `##` 引言/增长动因 → `###` 小节 → `####` 同比增幅/全球排名变化,各级都有正文。 + +--- + +### TC-02 中文编号三级(一、/(一)/ 1.) + +**输入大纲:** +``` +一、引言 +(一)背景 +(二)意义 +二、现状分析 +(一)市场数据 +1. 国内市场 +2. 海外市场 +``` + +**预期大纲结构:** +- 章节1 `引言` → 小节 `背景`、`意义` +- 章节2 `现状分析` → 小节 `市场数据` → 子要点 `国内市场`、`海外市场` +- 标题留空 → 用「写作意图」兜底(卡片标题约等于意图前 30 字) + +**校验重点:** `一、` > `(一)` > `1.` 的层级顺序正确(符合中文公文惯例)。 + +--- + +### TC-03 阿拉伯小数点编号(1. / 1.1 / 1.1.1) + +**输入大纲:** +``` +1. 引言 +1.1 研究背景 +1.2 研究意义 +2. 现状 +2.1 行业概况 +``` + +**预期大纲结构:** +- 章节1 `引言` → 小节 `研究背景`、`研究意义` +- 章节2 `现状` → 小节 `行业概况` + +--- + +### TC-04 缩进 bullet 表示层级 + +**输入大纲:** +``` +- 引言 + - 背景 + - 意义 +- 现状 + - 数据 + - 细节展开 +``` + +**预期大纲结构:** +- 章节1 `引言` → 小节 `背景`、`意义` +- 章节2 `现状` → 小节 `数据` → 子要点 `细节展开` + +**校验重点:** 缩进深度正确映射成层级。 + +--- + +### TC-05 只有一级目录(平铺 Markdown) + +**输入大纲:** +``` +## 引言 +## 现状 +## 对策 +## 结论 +``` + +**预期大纲结构:** 4 个章节 `引言/现状/对策/结论`,**均无小节**。 +**预期正文层级:** 每个章节 `##` 标题下是一整段连贯正文(不出现 `###`)。 + +**校验重点:** 一级目录场景不报错、不被强行拆出多余小节。 + +--- + +### TC-06 只有一级目录(中文编号) + +**输入大纲:** +``` +一、引言 +二、现状 +三、结论 +``` + +**预期大纲结构:** 3 个章节 `引言/现状/结论`,无小节。与 TC-05 等价,验证另一种一级写法。 + +--- + +### TC-07 纯文本平铺(无任何标记) + +**输入大纲:** +``` +引言 +现状 +对策 +总结 +``` + +**预期大纲结构:** 4 个章节 `引言/现状/对策/总结`(每行一个短标题 → 一级章节)。 + +**校验重点:** 无编号无缩进的短标题列表也能识别为一级大纲。 + +--- + +### TC-08 格式混乱 / 散文化(走 LLM 兜底) + +**输入大纲:** +``` +我想写一篇分析今年AI大模型发展趋势的文章,重点关注国内企业的布局。 +希望先讲背景,再讲现状,最后给出展望。 +``` + +**预期行为:** +- 确定性解析判定为「散文」→ 自动走 **LLM 归一兜底**,整理成一份结构化大纲。 +- 大纲确认卡能正常显示一份大纲(不报错、不卡死)。 +- 大纲内容应贴近原意(背景 / 现状 / 展望之类),**但不要求逐字一致**(这一条是兜底鲁棒性,不是保真用例)。 + +**校验重点:** 流程不崩、能产出可用大纲。 + +--- + +### TC-09 保真校验(标题原样保留) + +**输入大纲:** +``` +一、A股市场2024年回顾与展望 +(一)宏观经济(GDP/CPI)影响 +(二)流动性与利率 +二、行业轮动主线 +``` + +**预期大纲结构:** 标题文本**逐字保留**,仅编号被剥离: +- 章节1 `A股市场2024年回顾与展望` + - 小节 `宏观经济(GDP/CPI)影响`、`流动性与利率` +- 章节2 `行业轮动主线` + +**校验重点:** 标题里的中文括号、英文缩写、年份等**不被改写**。 + +--- + +### TC-10 大纲确认卡里再编辑 / 重新规划 + +**前置:** 先用 TC-02 的大纲跑到「大纲确认」卡。 + +**操作:** +1. 在确认卡里**手动改一个小节标题**(如把 `背景` 改成 `行业背景`)→ 点「确认」。 +2. 或点「重新规划」并填修改要求(如 `给"现状分析"再加一个小节`)。 + +**预期行为:** +- 手动改后确认:正文按改后的大纲生成。 +- 重新规划:基于**用户这份大纲**做编辑式调整(在原结构上加/改),而不是从零另起一份无关大纲。 + +**校验重点:** 自带大纲 → 确认 → 编辑 这条回路正常,不会丢掉用户原始结构。 + +--- + +### TC-11 回归:不填大纲(默认流程不受影响) + +**操作:** 「已有大纲」**留空**,只填写作意图,正常发起。 + +**预期行为:** 与功能上线前完全一致——系统自动检索素材、**自动规划大纲**(大纲由 AI 生成,非用户提供)。 + +**校验重点:** 增量功能不污染老流程。 + +--- + +### TC-12 素材关联(仍走检索) + +**前置:** 素材来源选「知识库」,用 TC-02 的大纲发起,等检索出素材并确认。 + +**预期行为:** +- 走到大纲时,用户的章节会被**关联上检索到的素材**(正文里能体现素材中的事实/数据)。 +- 关联**只填素材、不改大纲标题与结构**。 + +**校验重点:** 「自带大纲」与「检索素材」能共存——大纲是用户的,正文有素材支撑。 + +--- + +## 五、缺陷记录模板 + +| 用例 | 输入大纲(摘要) | 预期 | 实际 | 是否通过 | 备注 | +|------|------------------|------|------|----------|------| +| TC-01 | Markdown 三级 | 标题+2章节+子要点 | | ☐ | | +| TC-02 | 一、/(一)/1. | 中文三级 | | ☐ | | +| TC-03 | 1./1.1/1.1.1 | 阿拉伯三级 | | ☐ | | +| TC-04 | 缩进 bullet | 缩进三级 | | ☐ | | +| TC-05 | 平铺 ## | 4 章节无小节 | | ☐ | | +| TC-06 | 平铺 一、 | 3 章节无小节 | | ☐ | | +| TC-07 | 纯文本平铺 | 4 章节 | | ☐ | | +| TC-08 | 散文 | LLM 兜底产出大纲 | | ☐ | | +| TC-09 | 带括号/缩写标题 | 标题逐字保留 | | ☐ | | +| TC-10 | 确认卡编辑/重规划 | 在原大纲上调整 | | ☐ | | +| TC-11 | 留空 | 走自动规划 | | ☐ | | +| TC-12 | 检索+自带大纲 | 素材关联不改结构 | | ☐ | | + +--- + +## 六、备注 + +- TC-01~07、09 的解析逻辑已有**自动化单测**覆盖(`backend/tests/test_ai_writing_outline_parser.py`,19 个用例),本文档侧重**端到端真机表现**(流式展示、确认卡渲染、正文层级、素材关联)。 +- 若某条解析结果与预期不符,先看大纲确认卡里的**结构**是否正确(定位是解析问题还是正文生成问题)。 +- 相关后端实现:`packages/harness/deerflow/agents/ai_writing/outline_parser.py`(解析)、`nodes/writer_outline.py`(自带大纲分支 + 素材关联)。 diff --git a/frontend-web/src/open-canvas/components/ai-writing/AIWritingForm.tsx b/frontend-web/src/open-canvas/components/ai-writing/AIWritingForm.tsx index 54c20f4a..246a4b09 100644 --- a/frontend-web/src/open-canvas/components/ai-writing/AIWritingForm.tsx +++ b/frontend-web/src/open-canvas/components/ai-writing/AIWritingForm.tsx @@ -50,6 +50,8 @@ export function AIWritingForm({ hideAdvancedFields = false, }: AIWritingFormProps) { const [intent, setIntent] = useState(''); + const [showOutline, setShowOutline] = useState(false); + const [outlineText, setOutlineText] = useState(''); const [articleType, setArticleType] = useState('analysis'); const [audience, setAudience] = useState('普通读者'); const [wordCountInput, setWordCountInput] = useState('800'); @@ -115,6 +117,7 @@ export function AIWritingForm({ const wordCount = Math.max(100, parseInt(wordCountInput, 10) || 800); onSubmit({ userIntent: intent.trim(), + userOutline: outlineText.trim() || undefined, articleType, targetAudience: hideAdvancedFields ? DEFAULT_AUDIENCE @@ -155,6 +158,43 @@ export function AIWritingForm({ /> +
+ + {showOutline && ( + <> +