Skip to content

Commit 929c8d2

Browse files
nieaoclaude
andcommitted
feat(meta): ConceptNode 加元认知 + 圈选组合分析 + 批量推进
三件改动一起做: 1. 抽 MetaAnalysisInline 共享组件 (5 维度结构化展示) · OntologyNode + ConceptNode + 组合分析节点 都复用 · 重用 onReanalyze + isAnalyzing 接口 2. ConceptNode 加 ⚡ 元认知按钮 + inline 折叠区 · 复用现成的 analyzeNodeMetaCognitive store action (节点无关) · 单按钮一行, 比 OntologyNode 简洁 (不需要派 Hermes / 反驳) · 已分析后变 ▾/▴ 切换 3. 圈选组合分析 + 批量推进 (SelectionToolbar 加 2 按钮) · 🧠 组合分析: 把选中节点当一个系统看, 生成新组合分析节点 (variant=goal 深底凸显), 自动连虚线到所有源节点, inline 展开 5 维度组合洞察 · 🚀 批量推进 (下拉): 批量元认知 / 批量拆解 / 批量派 Hermes - 批量元认知对所有节点 (含 ConceptNode) 跑 - 批量拆解 / 批量派 Hermes 仅对 OntologyNode 生效, 自动跳过普通概念 底层支持: - aiService 新加 analyzeGroupMeta + GROUP_META_ANALYSIS_SYSTEM_PROMPT (跟单节点 prompt 不同, 强调"组合涌现的洞察"和"跨节点依赖断裂") - store 新加 analyzeGroupMetaCognitive (建占位节点 + LLM + inline 展开) - store 新加 batchAdvance (mode: analyze / decompose / promote, Promise.allSettled 并发) - KnowledgeGraph onSelectionAction 路由 groupAnalyzeMeta + batchAdvance 自测: .test-group-meta.mjs (mock LLM) - ✓ ConceptNode 元认知按钮存在 - ✓ 组合分析: 新建 1 节点 + 3 连边 + 5 维度落地 - ✓ 批量元认知: 3/3 节点全部成功 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent fb7aa33 commit 929c8d2

7 files changed

Lines changed: 439 additions & 62 deletions

File tree

src/components/canvas/ConceptNode.jsx

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
import { memo, useState, useRef, useEffect } from 'react'
88
import { Handle, Position } from 'reactflow'
99
import ColorAccentBar from './ColorAccentBar'
10+
import MetaAnalysisInline from './MetaAnalysisInline'
11+
import useCanvasStore from '../../stores/useCanvasStore'
1012

1113
// 安全获取字符串内容
1214
const safeString = (val) => {
@@ -98,6 +100,14 @@ function ConceptNode({ id, data, selected }) {
98100
const hoverTimeoutRef = useRef(null)
99101
const collapseTimeoutRef = useRef(null)
100102

103+
// 元认知按钮状态来自 store data — 跟 yjs 同步
104+
const analyzeMeta = useCanvasStore((s) => s.analyzeNodeMetaCognitive)
105+
const updateNode = useCanvasStore((s) => s.updateNode)
106+
const isAnalyzing = data.metaAnalyzing === true
107+
const metaAnalysis = data.metaAnalysis
108+
const metaError = data.metaAnalysisError
109+
const metaExpanded = data.metaExpanded === true
110+
101111
const sizeStyle = SIZE_SCALES[data.size] || SIZE_SCALES.medium
102112
const isMarked = data.marked
103113
const markColor = data.markColor || 'var(--accent)'
@@ -108,6 +118,22 @@ function ConceptNode({ id, data, selected }) {
108118
const hasRichContent = data.description && data.description.length > 60
109119
const isExpanded = (isHovered || isPinned) && hasRichContent
110120

121+
// 元认知按钮处理
122+
const onAnalyzeMeta = (e) => {
123+
e.stopPropagation()
124+
if (isAnalyzing || !data.title?.trim()) return
125+
if (metaAnalysis) {
126+
updateNode(id, { metaExpanded: !metaExpanded })
127+
return
128+
}
129+
analyzeMeta(id).catch((err) => console.error('[ConceptNode] analyze failed:', err))
130+
}
131+
const onReanalyze = (e) => {
132+
e.stopPropagation()
133+
if (isAnalyzing) return
134+
analyzeMeta(id).catch((err) => console.error('[ConceptNode] reanalyze failed:', err))
135+
}
136+
111137
// 清理定时器
112138
useEffect(() => {
113139
return () => {
@@ -324,6 +350,54 @@ function ConceptNode({ id, data, selected }) {
324350
))}
325351
</div>
326352
)}
353+
354+
{/* 元认知按钮 (一直可见) */}
355+
{data.title && (
356+
<div className="mt-2">
357+
<button
358+
onClick={onAnalyzeMeta}
359+
disabled={isAnalyzing}
360+
className="w-full text-[10px] py-1 px-2 rounded-sm border transition-all"
361+
style={{
362+
borderColor: metaAnalysis ? 'var(--accent)' : 'var(--accent-soft, var(--accent))',
363+
color: 'var(--accent)',
364+
background: metaAnalysis ? 'var(--accent-bg, rgba(245,240,235,0.7))' : 'var(--accent-bg, rgba(245,240,235,0.4))',
365+
cursor: isAnalyzing ? 'wait' : 'pointer',
366+
opacity: isAnalyzing ? 0.6 : 1,
367+
fontWeight: metaAnalysis ? 500 : 400,
368+
}}
369+
title={
370+
isAnalyzing ? '正在分析中…' :
371+
metaAnalysis ? `点击${metaExpanded ? '收起' : '展开'}元认知分析` :
372+
'一次 LLM 调用 → 5 维度分析'
373+
}
374+
>
375+
{isAnalyzing ? '分析中…' : metaAnalysis ? `⚡ 元认知 ${metaExpanded ? '▴' : '▾'}` : '⚡ 元认知分析'}
376+
</button>
377+
</div>
378+
)}
379+
380+
{/* 元认知错误 */}
381+
{metaError && !metaAnalysis && (
382+
<div className="text-[10px] mt-2 px-2 py-1 rounded-sm" style={{
383+
color: '#7a3a4a',
384+
background: 'rgba(245,235,237,0.6)',
385+
border: '1px solid #b27c8b',
386+
}}>
387+
分析失败: {metaError}
388+
<button onClick={onReanalyze} className="ml-2 underline" style={{ color: '#7a3a4a' }}>重试</button>
389+
</div>
390+
)}
391+
392+
{/* 元认知 inline 折叠区 */}
393+
{metaAnalysis && metaExpanded && (
394+
<MetaAnalysisInline
395+
analysis={metaAnalysis}
396+
textColor="var(--text-secondary)"
397+
onReanalyze={onReanalyze}
398+
isAnalyzing={isAnalyzing}
399+
/>
400+
)}
327401
</div>
328402

329403
{/* 底部信息栏 */}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/**
2+
* MetaAnalysisInline — 节点内 inline 元认知分析折叠区
3+
*
4+
* 5 维度结构化展示:
5+
* - 核心意图 (1 句)
6+
* - 隐含目标 (2-3 条)
7+
* - 关键风险 (2-3 条, 粉灰警示色)
8+
* - 前置依赖 (2-3 条)
9+
* - 下一步行动 (1-3 条, 有序列表)
10+
*
11+
* 由 OntologyNode + ConceptNode + 组合分析节点共用.
12+
*
13+
* Props:
14+
* - analysis: { core_intent, implicit_goals, key_risks, dependencies, next_actions }
15+
* - textColor: 文字主色 (跟随节点 variant 配色)
16+
* - onReanalyze: 重跑回调
17+
* - isAnalyzing: 是否正在重跑 (loading)
18+
*/
19+
20+
import { memo } from 'react'
21+
22+
function MetaAnalysisInlineImpl({ analysis, textColor = 'var(--text-primary)', onReanalyze, isAnalyzing = false }) {
23+
if (!analysis) return null
24+
25+
return (
26+
<div className="mt-3 pt-2" style={{ borderTop: '1px dashed var(--border-subtle)' }}>
27+
<div className="flex items-center justify-between mb-1.5">
28+
<span className="text-[9px] font-semibold" style={{ color: 'var(--accent)', letterSpacing: '0.2em' }}>
29+
META-COGNITIVE
30+
</span>
31+
{onReanalyze && (
32+
<button
33+
onClick={onReanalyze}
34+
disabled={isAnalyzing}
35+
className="text-[9px] underline"
36+
style={{ color: 'var(--text-muted)', cursor: isAnalyzing ? 'wait' : 'pointer' }}
37+
title="重新分析"
38+
>
39+
{isAnalyzing ? '...' : '↻ 重跑'}
40+
</button>
41+
)}
42+
</div>
43+
44+
{analysis.core_intent && (
45+
<div className="mb-2">
46+
<div className="text-[9px] font-medium mb-0.5" style={{ color: 'var(--accent)' }}>核心意图</div>
47+
<div className="text-[11px] leading-relaxed" style={{ color: textColor }}>{analysis.core_intent}</div>
48+
</div>
49+
)}
50+
51+
{analysis.implicit_goals?.length > 0 && (
52+
<div className="mb-2">
53+
<div className="text-[9px] font-medium mb-0.5" style={{ color: 'var(--accent)' }}>隐含目标</div>
54+
<ul className="text-[10.5px] leading-snug pl-3" style={{ color: textColor, opacity: 0.85, listStyleType: 'disc' }}>
55+
{analysis.implicit_goals.map((g, i) => <li key={i}>{g}</li>)}
56+
</ul>
57+
</div>
58+
)}
59+
60+
{analysis.key_risks?.length > 0 && (
61+
<div className="mb-2">
62+
<div className="text-[9px] font-medium mb-0.5" style={{ color: '#7a3a4a' }}>关键风险</div>
63+
<ul className="text-[10.5px] leading-snug pl-3" style={{ color: textColor, opacity: 0.85, listStyleType: 'disc' }}>
64+
{analysis.key_risks.map((r, i) => <li key={i}>{r}</li>)}
65+
</ul>
66+
</div>
67+
)}
68+
69+
{analysis.dependencies?.length > 0 && (
70+
<div className="mb-2">
71+
<div className="text-[9px] font-medium mb-0.5" style={{ color: 'var(--accent)' }}>前置依赖</div>
72+
<ul className="text-[10.5px] leading-snug pl-3" style={{ color: textColor, opacity: 0.85, listStyleType: 'disc' }}>
73+
{analysis.dependencies.map((d, i) => <li key={i}>{d}</li>)}
74+
</ul>
75+
</div>
76+
)}
77+
78+
{analysis.next_actions?.length > 0 && (
79+
<div>
80+
<div className="text-[9px] font-medium mb-0.5" style={{ color: 'var(--accent)' }}>下一步行动</div>
81+
<ol className="text-[10.5px] leading-snug pl-3" style={{ color: textColor, listStyleType: 'decimal' }}>
82+
{analysis.next_actions.map((a, i) => <li key={i}>{a}</li>)}
83+
</ol>
84+
</div>
85+
)}
86+
</div>
87+
)
88+
}
89+
90+
export default memo(MetaAnalysisInlineImpl)

src/components/canvas/OntologyNode.jsx

Lines changed: 8 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import { memo, useState } from 'react'
1818
import { Handle, Position } from 'reactflow'
1919
import useCanvasStore from '../../stores/useCanvasStore'
20+
import MetaAnalysisInline from './MetaAnalysisInline'
2021

2122
const VARIANT_META = {
2223
goal: {
@@ -260,67 +261,14 @@ function OntologyNodeImpl({ id, data, selected }) {
260261
</div>
261262
)}
262263

263-
{/* 元认知分析结果 inline 折叠区 */}
264+
{/* 元认知分析结果 inline 折叠区 — 共享组件 */}
264265
{metaAnalysis && metaExpanded && (
265-
<div className="mt-3 pt-2" style={{ borderTop: '1px dashed var(--border-subtle)' }}>
266-
<div className="flex items-center justify-between mb-1.5">
267-
<span className="text-[9px] font-semibold" style={{ color: 'var(--accent)', letterSpacing: '0.2em' }}>
268-
META-COGNITIVE
269-
</span>
270-
<button
271-
onClick={onReanalyze}
272-
disabled={isAnalyzing}
273-
className="text-[9px] underline"
274-
style={{ color: 'var(--text-muted)', cursor: isAnalyzing ? 'wait' : 'pointer' }}
275-
title="重新分析"
276-
>
277-
{isAnalyzing ? '...' : '↻ 重跑'}
278-
</button>
279-
</div>
280-
281-
{metaAnalysis.core_intent && (
282-
<div className="mb-2">
283-
<div className="text-[9px] font-medium mb-0.5" style={{ color: 'var(--accent)' }}>核心意图</div>
284-
<div className="text-[11px] leading-relaxed" style={{ color: meta.color }}>{metaAnalysis.core_intent}</div>
285-
</div>
286-
)}
287-
288-
{metaAnalysis.implicit_goals?.length > 0 && (
289-
<div className="mb-2">
290-
<div className="text-[9px] font-medium mb-0.5" style={{ color: 'var(--accent)' }}>隐含目标</div>
291-
<ul className="text-[10.5px] leading-snug pl-3" style={{ color: meta.color, opacity: 0.85, listStyleType: 'disc' }}>
292-
{metaAnalysis.implicit_goals.map((g, i) => <li key={i}>{g}</li>)}
293-
</ul>
294-
</div>
295-
)}
296-
297-
{metaAnalysis.key_risks?.length > 0 && (
298-
<div className="mb-2">
299-
<div className="text-[9px] font-medium mb-0.5" style={{ color: '#7a3a4a' }}>关键风险</div>
300-
<ul className="text-[10.5px] leading-snug pl-3" style={{ color: meta.color, opacity: 0.85, listStyleType: 'disc' }}>
301-
{metaAnalysis.key_risks.map((r, i) => <li key={i}>{r}</li>)}
302-
</ul>
303-
</div>
304-
)}
305-
306-
{metaAnalysis.dependencies?.length > 0 && (
307-
<div className="mb-2">
308-
<div className="text-[9px] font-medium mb-0.5" style={{ color: 'var(--accent)' }}>前置依赖</div>
309-
<ul className="text-[10.5px] leading-snug pl-3" style={{ color: meta.color, opacity: 0.85, listStyleType: 'disc' }}>
310-
{metaAnalysis.dependencies.map((d, i) => <li key={i}>{d}</li>)}
311-
</ul>
312-
</div>
313-
)}
314-
315-
{metaAnalysis.next_actions?.length > 0 && (
316-
<div>
317-
<div className="text-[9px] font-medium mb-0.5" style={{ color: 'var(--accent)' }}>下一步行动</div>
318-
<ol className="text-[10.5px] leading-snug pl-3" style={{ color: meta.color, listStyleType: 'decimal' }}>
319-
{metaAnalysis.next_actions.map((a, i) => <li key={i}>{a}</li>)}
320-
</ol>
321-
</div>
322-
)}
323-
</div>
266+
<MetaAnalysisInline
267+
analysis={metaAnalysis}
268+
textColor={meta.color}
269+
onReanalyze={onReanalyze}
270+
isAnalyzing={isAnalyzing}
271+
/>
324272
)}
325273
</div>
326274
</div>

src/components/canvas/SelectionToolbar.jsx

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ function SelectionToolbar({ selectedCount, position, onAction }) {
3636
const [showMarkMenu, setShowMarkMenu] = useState(false)
3737
const [showCategoryMenu, setShowCategoryMenu] = useState(false)
3838
const [showTagInput, setShowTagInput] = useState(false)
39+
const [showAdvanceMenu, setShowAdvanceMenu] = useState(false)
3940
const [tagInput, setTagInput] = useState('')
4041

4142
if (selectedCount < 2) return null
@@ -84,6 +85,17 @@ function SelectionToolbar({ selectedCount, position, onAction }) {
8485
setShowMarkMenu(false)
8586
setShowCategoryMenu(false)
8687
setShowTagInput(false)
88+
setShowAdvanceMenu(false)
89+
}
90+
91+
const handleGroupMeta = () => {
92+
closeAllMenus()
93+
dispatch('groupAnalyzeMeta', {})
94+
}
95+
96+
const handleBatchAdvance = (mode) => {
97+
closeAllMenus()
98+
dispatch('batchAdvance', { mode })
8799
}
88100

89101
// 按钮通用样式
@@ -110,6 +122,74 @@ function SelectionToolbar({ selectedCount, position, onAction }) {
110122
{selectedCount} 已选
111123
</div>
112124

125+
{/* 组合元认知分析 — 把选中节点当一个系统看, 生成新组合分析节点 */}
126+
<button
127+
onClick={handleGroupMeta}
128+
className={btnClass}
129+
style={{ backgroundColor: 'var(--accent-bg)', color: 'var(--accent)', fontWeight: 500 }}
130+
title="组合元认知分析: 把选中的节点当成一个系统, LLM 生成 5 维度组合分析新节点"
131+
>
132+
<span>🧠</span>
133+
<span>组合分析</span>
134+
</button>
135+
136+
{/* 批量推进 — 对每个选中节点并发跑同一种推进动作 */}
137+
<div className="relative">
138+
<button
139+
onClick={() => { closeAllMenus(); setShowAdvanceMenu(!showAdvanceMenu) }}
140+
className={btnClass}
141+
style={{ backgroundColor: 'var(--accent-bg)', color: 'var(--accent)', fontWeight: 500 }}
142+
title="批量推进: 对每个选中节点跑同一动作"
143+
>
144+
<span>🚀</span>
145+
<span>批量推进</span>
146+
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
147+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
148+
</svg>
149+
</button>
150+
151+
{showAdvanceMenu && (
152+
<div
153+
className="absolute top-full left-0 mt-1 rounded-lg shadow-xl py-2 min-w-[180px] z-60"
154+
style={{ backgroundColor: 'var(--surface)', border: '1px solid var(--border-subtle)' }}
155+
>
156+
<div className="px-3 py-1 text-xs font-medium" style={{ color: 'var(--text-faint)', letterSpacing: '0.1em' }}>
157+
选择批量动作 ({selectedCount} 个节点)
158+
</div>
159+
<button
160+
onClick={() => handleBatchAdvance('analyze')}
161+
className="w-full px-3 py-2 text-left text-sm hover:bg-gray-50 flex items-center gap-2"
162+
style={{ color: 'var(--accent)' }}
163+
title="并发对每个节点单独做元认知分析"
164+
>
165+
<span></span>
166+
<span>批量元认知</span>
167+
</button>
168+
<button
169+
onClick={() => handleBatchAdvance('decompose')}
170+
className="w-full px-3 py-2 text-left text-sm hover:bg-gray-50 flex items-center gap-2"
171+
style={{ color: 'var(--accent)' }}
172+
title="把每个 OntologyNode 拆成子节点 (跳过普通概念节点)"
173+
>
174+
<span>🔧</span>
175+
<span>批量拆解 (仅本体节点)</span>
176+
</button>
177+
<button
178+
onClick={() => handleBatchAdvance('promote')}
179+
className="w-full px-3 py-2 text-left text-sm hover:bg-gray-50 flex items-center gap-2"
180+
style={{ color: 'var(--accent)' }}
181+
title="把每个 OntologyNode 派给 Hermes (跳过普通概念节点)"
182+
>
183+
<span>🚀</span>
184+
<span>批量派 Hermes (仅本体节点)</span>
185+
</button>
186+
</div>
187+
)}
188+
</div>
189+
190+
{/* 分隔 */}
191+
<div className="w-px h-6 mx-1" style={{ backgroundColor: 'var(--border-subtle)' }} />
192+
113193
{/* 创建分组 */}
114194
<button
115195
onClick={handleCreateGroup}
@@ -317,7 +397,7 @@ function SelectionToolbar({ selectedCount, position, onAction }) {
317397
</div>
318398

319399
{/* 点击遮罩关闭下拉菜单 */}
320-
{(showLinkMenu || showMarkMenu || showCategoryMenu || showTagInput) && (
400+
{(showLinkMenu || showMarkMenu || showCategoryMenu || showTagInput || showAdvanceMenu) && (
321401
<div className="fixed inset-0 z-50" onClick={closeAllMenus} />
322402
)}
323403
</div>

0 commit comments

Comments
 (0)