|
| 1 | +/** |
| 2 | + * OntologyNode — Aletheia 本体节点 |
| 3 | + * |
| 4 | + * 4 种 variant (从飞书 wiki Aletheia 设计): |
| 5 | + * - goal: 顶层目标 (黑底 + 暖色边) |
| 6 | + * - entity: 核心实体 (白底 + 暖色细线) |
| 7 | + * - constraint: 硬约束 (暖色背景 + 重边) |
| 8 | + * - assumption: 隐含假设 (灰背景 + 虚线, 等待验证) |
| 9 | + * |
| 10 | + * 每个节点底部 2 个动作按钮: |
| 11 | + * - "派 Hermes →" → 把节点转为 TaskNode 派单 (调研/执行) |
| 12 | + * - "反驳 ⚔" → 调反驳引擎生成 ChallengeNode (Devil's Advocate) |
| 13 | + */ |
| 14 | + |
| 15 | +import { memo } from 'react' |
| 16 | +import { Handle, Position } from 'reactflow' |
| 17 | +import useCanvasStore from '../../stores/useCanvasStore' |
| 18 | + |
| 19 | +const VARIANT_META = { |
| 20 | + goal: { |
| 21 | + label: 'GOAL', |
| 22 | + bg: '#1a1a1a', |
| 23 | + color: '#fafafa', |
| 24 | + border: '#c8a882', |
| 25 | + accent: '#c8a882', |
| 26 | + width: 280, |
| 27 | + }, |
| 28 | + entity: { |
| 29 | + label: 'ENTITY', |
| 30 | + bg: '#fafafa', |
| 31 | + color: '#1a1a1a', |
| 32 | + border: '#e8e8e8', |
| 33 | + accent: '#c8a882', |
| 34 | + width: 220, |
| 35 | + }, |
| 36 | + constraint: { |
| 37 | + label: 'CONSTRAINT', |
| 38 | + bg: '#f5f0eb', |
| 39 | + color: '#1a1a1a', |
| 40 | + border: '#c8a882', |
| 41 | + accent: '#c8a882', |
| 42 | + width: 220, |
| 43 | + }, |
| 44 | + assumption: { |
| 45 | + label: 'ASSUMPTION', |
| 46 | + bg: '#fafafa', |
| 47 | + color: '#555', |
| 48 | + border: '#bbb', |
| 49 | + borderStyle: 'dashed', |
| 50 | + accent: '#888', |
| 51 | + width: 220, |
| 52 | + }, |
| 53 | +} |
| 54 | + |
| 55 | +function OntologyNodeImpl({ id, data, selected }) { |
| 56 | + const promoteToTask = useCanvasStore((s) => s.promoteOntologyToTask) |
| 57 | + const challenge = useCanvasStore((s) => s.dispatchChallenge) |
| 58 | + const updateNode = useCanvasStore((s) => s.updateNode) |
| 59 | + |
| 60 | + const variant = data.variant || 'entity' |
| 61 | + const meta = VARIANT_META[variant] || VARIANT_META.entity |
| 62 | + const title = data.title || '' |
| 63 | + const description = data.description || '' |
| 64 | + const isChallenging = data.challenging === true |
| 65 | + |
| 66 | + const onPromoteToTask = (e) => { |
| 67 | + e.stopPropagation() |
| 68 | + promoteToTask(id).catch((err) => { |
| 69 | + console.error('[OntologyNode] promote failed:', err) |
| 70 | + }) |
| 71 | + } |
| 72 | + |
| 73 | + const onChallenge = (e) => { |
| 74 | + e.stopPropagation() |
| 75 | + if (isChallenging) return |
| 76 | + updateNode(id, { challenging: true }) |
| 77 | + challenge(id) |
| 78 | + .catch((err) => console.error('[OntologyNode] challenge failed:', err)) |
| 79 | + .finally(() => updateNode(id, { challenging: false })) |
| 80 | + } |
| 81 | + |
| 82 | + return ( |
| 83 | + <div |
| 84 | + className="relative shadow-sm transition-all duration-300" |
| 85 | + style={{ |
| 86 | + width: meta.width, |
| 87 | + background: meta.bg, |
| 88 | + color: meta.color, |
| 89 | + border: `${selected ? '2px' : '1px'} ${meta.borderStyle || 'solid'} ${selected ? '#c8a882' : meta.border}`, |
| 90 | + borderRadius: 4, |
| 91 | + }} |
| 92 | + > |
| 93 | + <Handle type="target" position={Position.Top} style={{ background: meta.accent }} /> |
| 94 | + <Handle type="source" position={Position.Bottom} style={{ background: meta.accent }} /> |
| 95 | + |
| 96 | + <div className="px-4 py-3"> |
| 97 | + {/* 顶部标签 */} |
| 98 | + <div className="flex items-center justify-between mb-2"> |
| 99 | + <span |
| 100 | + className="text-[9px] font-semibold" |
| 101 | + style={{ color: meta.accent, letterSpacing: '0.25em' }} |
| 102 | + > |
| 103 | + {meta.label} |
| 104 | + </span> |
| 105 | + {variant === 'assumption' && ( |
| 106 | + <span className="text-[9px]" style={{ color: meta.accent }}>未验证</span> |
| 107 | + )} |
| 108 | + </div> |
| 109 | + |
| 110 | + {/* 标题 */} |
| 111 | + <input |
| 112 | + type="text" |
| 113 | + className="w-full text-sm font-medium bg-transparent border-none outline-none" |
| 114 | + style={{ color: meta.color, fontFamily: variant === 'goal' ? 'var(--font-serif), Georgia, serif' : 'inherit' }} |
| 115 | + placeholder="节点标题…" |
| 116 | + value={title} |
| 117 | + onChange={(e) => updateNode(id, { title: e.target.value })} |
| 118 | + /> |
| 119 | + |
| 120 | + {/* 描述 */} |
| 121 | + {description && ( |
| 122 | + <div className="text-[11px] mt-1.5 leading-relaxed" style={{ color: meta.color, opacity: 0.7 }}> |
| 123 | + {description} |
| 124 | + </div> |
| 125 | + )} |
| 126 | + |
| 127 | + {/* 动作按钮 — goal 不可派 (太抽象), 其他 3 类都可 */} |
| 128 | + {variant !== 'goal' && ( |
| 129 | + <div className="flex gap-1.5 mt-3"> |
| 130 | + <button |
| 131 | + onClick={onPromoteToTask} |
| 132 | + disabled={!title.trim()} |
| 133 | + className="flex-1 text-[10px] py-1 px-2 rounded-sm border transition-all" |
| 134 | + style={{ |
| 135 | + borderColor: title.trim() ? '#c8a882' : '#e5e5e5', |
| 136 | + color: title.trim() ? '#1a1a1a' : '#bbb', |
| 137 | + background: title.trim() ? 'rgba(245,240,235,0.6)' : 'transparent', |
| 138 | + cursor: title.trim() ? 'pointer' : 'not-allowed', |
| 139 | + }} |
| 140 | + title="转为 Hermes 任务节点 (执行/调研)" |
| 141 | + > |
| 142 | + 派 Hermes → |
| 143 | + </button> |
| 144 | + <button |
| 145 | + onClick={onChallenge} |
| 146 | + disabled={!title.trim() || isChallenging} |
| 147 | + className="flex-1 text-[10px] py-1 px-2 rounded-sm border transition-all" |
| 148 | + style={{ |
| 149 | + borderColor: title.trim() ? '#b27c8b' : '#e5e5e5', |
| 150 | + color: title.trim() ? '#7a3a4a' : '#bbb', |
| 151 | + background: title.trim() ? 'rgba(245,235,237,0.6)' : 'transparent', |
| 152 | + cursor: title.trim() && !isChallenging ? 'pointer' : 'not-allowed', |
| 153 | + }} |
| 154 | + title="生成 Devil's Advocate 反驳论点" |
| 155 | + > |
| 156 | + {isChallenging ? '反驳中…' : '反驳 ⚔'} |
| 157 | + </button> |
| 158 | + </div> |
| 159 | + )} |
| 160 | + |
| 161 | + {/* goal 节点显示提示 */} |
| 162 | + {variant === 'goal' && ( |
| 163 | + <div className="text-[10px] mt-2 opacity-50" style={{ color: meta.color }}> |
| 164 | + ↓ 已自动拆解为下方实体 / 约束 / 假设 |
| 165 | + </div> |
| 166 | + )} |
| 167 | + </div> |
| 168 | + </div> |
| 169 | + ) |
| 170 | +} |
| 171 | + |
| 172 | +export default memo(OntologyNodeImpl) |
0 commit comments