From 896f429fec1c779e9c55bfa3be182cc9ed4f3d60 Mon Sep 17 00:00:00 2001 From: web4zn Date: Tue, 5 May 2026 20:24:20 +0800 Subject: [PATCH] feat: add Markdown rich content support for mindmap nodes - Extend MindMapNode with optional content/contentType fields - Render Markdown (bold, italic, inline code, strikethrough, code blocks, tables) via react-markdown - Add edit/preview toggle for Markdown content in node edit modal - Update mindmap generator prompts and parsing for content/contentType - Defensive JSON parsing with multi-stage fallback for LLM output - Isolate node scroll wheel from canvas zoom - Auto-activate conversation on unarchive - Archive rich-node-content change --- .../.openspec.yaml | 0 .../2026-05-05-rich-node-content}/design.md | 0 .../2026-05-05-rich-node-content}/proposal.md | 0 .../specs/mindmap-data/spec.md | 2 +- .../specs/mindmap-generation/spec.md | 0 .../specs/mindmap-node-editing/spec.md | 0 .../specs/rich-node-content/spec.md | 0 .../2026-05-05-rich-node-content}/tasks.md | 0 openspec/specs/mindmap-data/spec.md | 18 ++- openspec/specs/mindmap-generation/spec.md | 13 ++- openspec/specs/mindmap-node-editing/spec.md | 8 +- src/features/mindmap/MindMapEditModal.tsx | 105 +++++++++++++++++- src/features/mindmap/MindMapNodeComponent.tsx | 39 ++++++- src/features/mindmap/MindMapTree.tsx | 10 +- src/features/mindmap/types.ts | 2 + src/lib/mindmap-generator.ts | 88 ++++++++++++--- src/lib/mindmap-layout.ts | 11 +- src/stores/conversationStore.ts | 1 + src/stores/mindmapStore.ts | 4 +- src/types/mindmap.ts | 20 +++- 20 files changed, 283 insertions(+), 38 deletions(-) rename openspec/changes/{rich-node-content => archive/2026-05-05-rich-node-content}/.openspec.yaml (100%) rename openspec/changes/{rich-node-content => archive/2026-05-05-rich-node-content}/design.md (100%) rename openspec/changes/{rich-node-content => archive/2026-05-05-rich-node-content}/proposal.md (100%) rename openspec/changes/{rich-node-content => archive/2026-05-05-rich-node-content}/specs/mindmap-data/spec.md (96%) rename openspec/changes/{rich-node-content => archive/2026-05-05-rich-node-content}/specs/mindmap-generation/spec.md (100%) rename openspec/changes/{rich-node-content => archive/2026-05-05-rich-node-content}/specs/mindmap-node-editing/spec.md (100%) rename openspec/changes/{rich-node-content => archive/2026-05-05-rich-node-content}/specs/rich-node-content/spec.md (100%) rename openspec/changes/{rich-node-content => archive/2026-05-05-rich-node-content}/tasks.md (100%) diff --git a/openspec/changes/rich-node-content/.openspec.yaml b/openspec/changes/archive/2026-05-05-rich-node-content/.openspec.yaml similarity index 100% rename from openspec/changes/rich-node-content/.openspec.yaml rename to openspec/changes/archive/2026-05-05-rich-node-content/.openspec.yaml diff --git a/openspec/changes/rich-node-content/design.md b/openspec/changes/archive/2026-05-05-rich-node-content/design.md similarity index 100% rename from openspec/changes/rich-node-content/design.md rename to openspec/changes/archive/2026-05-05-rich-node-content/design.md diff --git a/openspec/changes/rich-node-content/proposal.md b/openspec/changes/archive/2026-05-05-rich-node-content/proposal.md similarity index 100% rename from openspec/changes/rich-node-content/proposal.md rename to openspec/changes/archive/2026-05-05-rich-node-content/proposal.md diff --git a/openspec/changes/rich-node-content/specs/mindmap-data/spec.md b/openspec/changes/archive/2026-05-05-rich-node-content/specs/mindmap-data/spec.md similarity index 96% rename from openspec/changes/rich-node-content/specs/mindmap-data/spec.md rename to openspec/changes/archive/2026-05-05-rich-node-content/specs/mindmap-data/spec.md index 8846c19..f226adc 100644 --- a/openspec/changes/rich-node-content/specs/mindmap-data/spec.md +++ b/openspec/changes/archive/2026-05-05-rich-node-content/specs/mindmap-data/spec.md @@ -1,6 +1,6 @@ ## MODIFIED Requirements -### Requirement: MindMapNode data model +### Requirement: MindMap data model MindMapNode SHALL 包含以下字段:`id: string`(唯一标识)、`label: string`(节点标题)、`summary: string`(纯文本摘要)、`content?: string`(可选 Markdown 内容)、`contentType?: 'text' | 'markdown'`(可选内容类型,默认 `'text'`)、`children: MindMapNode[]`(子节点)、`sourceConversationIds: string[]`(来源对话 ID)、`sourceExcerpts: Record`(来源摘录)、`editedByUser: boolean`(是否被用户编辑)。 #### Scenario: New node with markdown content diff --git a/openspec/changes/rich-node-content/specs/mindmap-generation/spec.md b/openspec/changes/archive/2026-05-05-rich-node-content/specs/mindmap-generation/spec.md similarity index 100% rename from openspec/changes/rich-node-content/specs/mindmap-generation/spec.md rename to openspec/changes/archive/2026-05-05-rich-node-content/specs/mindmap-generation/spec.md diff --git a/openspec/changes/rich-node-content/specs/mindmap-node-editing/spec.md b/openspec/changes/archive/2026-05-05-rich-node-content/specs/mindmap-node-editing/spec.md similarity index 100% rename from openspec/changes/rich-node-content/specs/mindmap-node-editing/spec.md rename to openspec/changes/archive/2026-05-05-rich-node-content/specs/mindmap-node-editing/spec.md diff --git a/openspec/changes/rich-node-content/specs/rich-node-content/spec.md b/openspec/changes/archive/2026-05-05-rich-node-content/specs/rich-node-content/spec.md similarity index 100% rename from openspec/changes/rich-node-content/specs/rich-node-content/spec.md rename to openspec/changes/archive/2026-05-05-rich-node-content/specs/rich-node-content/spec.md diff --git a/openspec/changes/rich-node-content/tasks.md b/openspec/changes/archive/2026-05-05-rich-node-content/tasks.md similarity index 100% rename from openspec/changes/rich-node-content/tasks.md rename to openspec/changes/archive/2026-05-05-rich-node-content/tasks.md diff --git a/openspec/specs/mindmap-data/spec.md b/openspec/specs/mindmap-data/spec.md index d16ab88..11902f9 100644 --- a/openspec/specs/mindmap-data/spec.md +++ b/openspec/specs/mindmap-data/spec.md @@ -1,4 +1,8 @@ -## ADDED Requirements +## Purpose + +Define the core data model for mindmap nodes and mindmap collections, supporting tree structures, source provenance tracking, and IndexedDB persistence. + +## Requirements ### Requirement: MindMap data model 系统 SHALL 使用以下数据模型表示思维导图: @@ -11,6 +15,7 @@ MindMap { maxDepth?: number // 最大生成深度:3-5 或 0(自动),默认 3 corpus: CorpusEntry[] // 语料库条目列表 monitoredConversationIds: string[] // 监听的对话 ID 列表 + collapsedNodeIds?: string[] // 折叠节点 ID 列表 forceFullRebuild?: boolean // 强制全量重建模式 lastGeneratedAt?: number // 上次生成时间戳 createdAt: number // 创建时间戳 @@ -23,12 +28,13 @@ MindMapNode { id: string // UUID label: string // 节点显示文本 summary: string // 节点描述 + content?: string // 可选 Markdown 内容 + contentType?: 'text' | 'markdown' // 内容类型,默认 'text' children: MindMapNode[] // 子节点列表 sourceConversationIds: string[] // 贡献该节点的 Conversation ID 列表 sourceExcerpts: Record // Conversation ID → 消息文本摘录 editedByUser: boolean // 是否被用户手动编辑过(默认 false) } -``` #### Scenario: MindMap data structure - **WHEN** 系统创建新的思维导图 @@ -42,6 +48,14 @@ MindMapNode { - **WHEN** 用户手动编辑节点 - **THEN** 节点 `editedByUser` 设为 true,`sourceConversationIds` 清空 +#### Scenario: New node with markdown content +- **WHEN** 创建 MindMapNode 且指定 `contentType: 'markdown'` 和 `content: '## Title\n\nContent'` +- **THEN** 节点存储完整 Markdown 内容且类型标记为 markdown + +#### Scenario: Existing node remains compatible +- **WHEN** 现有节点(无 `contentType` 和 `content` 字段)被反序列化 +- **THEN** 节点正常加载,`contentType` 默认为 `'text'`,行为与旧版本一致 + ### Requirement: Create mindmap 系统 SHALL 允许用户创建新的思维导图。用户 MUST 提供图谱标题。创建后 SHALL 自动在侧边栏显示新图谱条目。 diff --git a/openspec/specs/mindmap-generation/spec.md b/openspec/specs/mindmap-generation/spec.md index 38d6424..f76e47f 100644 --- a/openspec/specs/mindmap-generation/spec.md +++ b/openspec/specs/mindmap-generation/spec.md @@ -1,7 +1,11 @@ -## ADDED Requirements +## Purpose + +Enable AI-powered mindmap generation from conversation history using LLM prompts, supporting both full rebuild and incremental update modes with structured JSON output. + +## Requirements ### Requirement: Generate mindmap from conversation history -系统 SHALL 支持通过 LLM 从对话内容生成思维导图树结构。输入内容 SHALL 优先使用图谱语料库内容。生成模式 SHALL 根据图谱状态自动选择:首次生成使用全量模式(输出完整 Markdown/JSON 树),后续生成使用增量模式(输出操作指令)。增量模式 SHALL 提供旧树摘要而非完整树,减少 prompt token 消耗。 +系统 SHALL 支持通过 LLM 从对话内容生成思维导图树结构。输入内容 SHALL 优先使用图谱语料库内容。生成模式 SHALL 根据图谱状态自动选择:首次生成使用全量模式(输出完整 Markdown/JSON 树),后续生成使用增量模式(输出操作指令)。增量模式 SHALL 提供旧树摘要而非完整树,减少 prompt token 消耗。生成 prompt SHALL 指示 LLM 在节点中使用 `content` 字段承载 Markdown 格式内容(加粗、斜体、行内代码、删除线、代码块、表格),并在输出 JSON 中标注 `contentType: 'markdown'`。系统 SHALL 在解析 JSON 响应时识别 `contentType` 和 `content` 字段并存储到 MindMapNode。 #### Scenario: First-time generation - **WHEN** 用户对未包含树的图谱触发生成 @@ -11,6 +15,11 @@ - **WHEN** 图谱已有树结构,用户传入新内容触发生成 - **THEN** 系统使用增量模式,提供旧树摘要,LLM 输出操作指令 +#### Scenario: AI generates node with markdown content +- **WHEN** 语料包含代码示例,且生成模式为全量或增量 +- **THEN** LLM 输出的节点可能包含带 `contentType: 'markdown'` 的 `content` 字段(含代码块或表格) +- **AND** 系统正确解析并存储 `contentType` 和 `content` 字段 + ### Requirement: Markdown to tree parsing 系统 SHALL 将 LLM 返回的 Markdown 文本解析为 MindMapNode 树结构。解析规则: - `# 标题` → 根节点(Tree 数组中添加一项) diff --git a/openspec/specs/mindmap-node-editing/spec.md b/openspec/specs/mindmap-node-editing/spec.md index 268bf1f..192f56f 100644 --- a/openspec/specs/mindmap-node-editing/spec.md +++ b/openspec/specs/mindmap-node-editing/spec.md @@ -1,7 +1,11 @@ -## MODIFIED Requirements +## Purpose + +Allow users to edit mindmap node labels, summaries, and Markdown content through a modal dialog, with validation and user-edit tracking. + +## Requirements ### Requirement: Node edit mode -系统 SHALL 允许用户双击节点进入编辑模式。编辑模式下 SHALL 弹出居中 Modal 弹窗(`MindMapEditModal`),包含 label 输入框和 summary 文本域。按 Enter 确认编辑并调用 `mindmapStore.updateNode`,按 Escape 或点击 Modal 外区域取消编辑。确认后节点 `editedByUser` 标记为 true。 +系统 SHALL 允许用户双击节点进入编辑模式。编辑模式下 SHALL 弹出居中 Modal 弹窗(`MindMapEditModal`),包含 label 输入框、summary 文本域,以及当 `contentType` 为 `'markdown'` 时的 content Markdown 编辑器与预览切换按钮。按 Enter 确认编辑并调用 `mindmapStore.updateNode`,按 Escape 或点击 Modal 外区域取消编辑。确认后节点 `editedByUser` 标记为 true。 #### Scenario: Double-click to edit node - **WHEN** 用户双击画布中的某个节点 diff --git a/src/features/mindmap/MindMapEditModal.tsx b/src/features/mindmap/MindMapEditModal.tsx index 8ac0494..fa59e52 100644 --- a/src/features/mindmap/MindMapEditModal.tsx +++ b/src/features/mindmap/MindMapEditModal.tsx @@ -1,17 +1,30 @@ import { useState, useEffect, useRef } from 'react' import { createPortal } from 'react-dom' +import Markdown from 'react-markdown' +import remarkGfm from 'remark-gfm' import { Button } from '@/components/ui/button' import type { MindMapNode } from '@/types/mindmap' interface MindMapEditModalProps { node: MindMapNode - onConfirm: (nodeId: string, label: string, summary: string) => void + onConfirm: ( + nodeId: string, + label: string, + summary: string, + content?: string, + contentType?: 'text' | 'markdown', + ) => void onCancel: () => void } export default function MindMapEditModal({ node, onConfirm, onCancel }: MindMapEditModalProps) { const [label, setLabel] = useState(node.label) const [summary, setSummary] = useState(node.summary) + const [content, setContent] = useState(node.content ?? '') + const [contentType, setContentType] = useState<'text' | 'markdown'>( + node.contentType === 'markdown' ? 'markdown' : 'text', + ) + const [previewing, setPreviewing] = useState(false) const inputRef = useRef(null) useEffect(() => { @@ -21,14 +34,22 @@ export default function MindMapEditModal({ node, onConfirm, onCancel }: MindMapE const handleConfirm = () => { if (label.trim()) { - onConfirm(node.id, label.trim(), summary.trim()) + onConfirm( + node.id, + label.trim(), + summary.trim(), + contentType === 'markdown' ? content.trim() : undefined, + contentType, + ) } } + const isMarkdown = contentType === 'markdown' + return createPortal(
-
+

编辑节点

@@ -56,12 +77,86 @@ export default function MindMapEditModal({ node, onConfirm, onCancel }: MindMapE value={summary} onChange={(e) => setSummary(e.target.value)} onKeyDown={(e) => { - if (e.key === 'Escape') onCancel() + if (e.key === 'Escape' && !e.shiftKey) onCancel() }} placeholder="摘要内容" - rows={3} + rows={2} />
+
+
+ +
+ + +
+
+ {isMarkdown && ( +
+
+ + Markdown 内容(可选) + + +
+ {previewing ? ( +
+ + {content || '_无内容_'} + +
+ ) : ( +