helixevo 0.2.0 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dashboard/app/api/skills/route.ts +298 -0
- package/dashboard/app/evolution/page.tsx +128 -0
- package/dashboard/app/frontier/page.tsx +128 -0
- package/dashboard/app/globals.css +1211 -0
- package/dashboard/app/guide/page.tsx +984 -0
- package/dashboard/app/layout.tsx +79 -0
- package/dashboard/app/network/client.tsx +1090 -0
- package/dashboard/app/network/page.tsx +68 -0
- package/dashboard/app/page.tsx +147 -0
- package/dashboard/app/research/page.tsx +124 -0
- package/dashboard/components/SkillFlowNode.tsx +192 -0
- package/dashboard/lib/data.ts +146 -0
- package/dashboard/next-env.d.ts +6 -0
- package/dashboard/package-lock.json +1182 -0
- package/dashboard/package.json +21 -0
- package/dashboard/tsconfig.json +40 -0
- package/dist/cli.js +81 -10
- package/package.json +8 -1
|
@@ -0,0 +1,1090 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useCallback, useMemo, useState } from 'react'
|
|
4
|
+
import {
|
|
5
|
+
ReactFlow,
|
|
6
|
+
Background,
|
|
7
|
+
Controls,
|
|
8
|
+
type Node,
|
|
9
|
+
type Edge,
|
|
10
|
+
MarkerType,
|
|
11
|
+
ConnectionLineType,
|
|
12
|
+
} from '@xyflow/react'
|
|
13
|
+
import '@xyflow/react/dist/style.css'
|
|
14
|
+
import SkillFlowNode from '@/components/SkillFlowNode'
|
|
15
|
+
|
|
16
|
+
// ─── Types ──────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
interface SkillNode {
|
|
19
|
+
id: string; name: string; score: number; generation: number
|
|
20
|
+
layer: string; status: string; tags: string[]; failureCount: number; lastEvolved: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface GraphEdge {
|
|
24
|
+
from: string; to: string; type: string; strength: number; detectedBy: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface EvolutionEntry {
|
|
28
|
+
id: string; timestamp: string; action: string; description: string
|
|
29
|
+
outcome: string; outcomeReason: string; task: number; align: number; sideEffect: number
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface ProjectData {
|
|
33
|
+
name: string; failures: any[]; skills: string[]
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface Props {
|
|
37
|
+
allNodes: SkillNode[]
|
|
38
|
+
generalized: SkillNode[]
|
|
39
|
+
evolved: SkillNode[]
|
|
40
|
+
original: SkillNode[]
|
|
41
|
+
edges: GraphEdge[]
|
|
42
|
+
evolutionBySkill: Record<string, EvolutionEntry[]>
|
|
43
|
+
skillContents: Record<string, string>
|
|
44
|
+
projects: ProjectData[]
|
|
45
|
+
stats: { nodes: number; edges: number; clusters: number; generalized: number; evolved: number; original: number }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
type SubView = 'graph' | 'general' | 'projects' | 'coevolution'
|
|
49
|
+
|
|
50
|
+
const SUB_VIEWS: { key: SubView; label: string; desc: string }[] = [
|
|
51
|
+
{ key: 'graph', label: 'Skill Graph', desc: 'Interactive network visualization' },
|
|
52
|
+
{ key: 'general', label: 'General Skills', desc: 'Abstract and domain-level skills' },
|
|
53
|
+
{ key: 'projects', label: 'Project Skills', desc: 'Per-project specialized skills' },
|
|
54
|
+
{ key: 'coevolution', label: 'Co-Evolution', desc: 'Cross-layer knowledge flow' },
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
const nodeTypes = { skillNode: SkillFlowNode }
|
|
58
|
+
|
|
59
|
+
// ─── Helpers ────────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
function scoreColor(s: number) {
|
|
62
|
+
return s >= 0.8 ? 'var(--green)' : s >= 0.6 ? 'var(--yellow)' : 'var(--red)'
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const COLUMN_X = { generalized: 50, evolved: 400, original: 750, project: 1100 }
|
|
66
|
+
const EDGE_COLORS: Record<string, string> = {
|
|
67
|
+
inherits: '#818cf8', enhances: '#22c55e', conflicts: '#ef4444', 'co-evolves': '#f59e0b', depends: '#6366f1',
|
|
68
|
+
}
|
|
69
|
+
const EDGE_LABELS: Record<string, string> = {
|
|
70
|
+
inherits: 'inherits', enhances: 'enhances', conflicts: 'conflicts', 'co-evolves': 'co-evolves',
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function actionBtnStyle(color?: string): React.CSSProperties {
|
|
74
|
+
return {
|
|
75
|
+
padding: '6px 12px', fontSize: 11, fontWeight: 600, borderRadius: 6, cursor: 'pointer',
|
|
76
|
+
border: `1px solid ${color ?? 'var(--border)'}`,
|
|
77
|
+
background: color ? `${color}10` : 'var(--bg-section)',
|
|
78
|
+
color: color ?? 'var(--text-dim)',
|
|
79
|
+
fontFamily: 'var(--font)',
|
|
80
|
+
transition: 'all 0.15s ease',
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ─── Main Component ─────────────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
export default function NetworkClient({
|
|
87
|
+
allNodes, generalized, evolved, original, edges: graphEdges, evolutionBySkill, skillContents, projects, stats
|
|
88
|
+
}: Props) {
|
|
89
|
+
const [view, setView] = useState<SubView>('graph')
|
|
90
|
+
const [selectedSkill, setSelectedSkill] = useState<string | null>(null)
|
|
91
|
+
const [editing, setEditing] = useState(false)
|
|
92
|
+
const [editContent, setEditContent] = useState('')
|
|
93
|
+
const [creating, setCreating] = useState(false)
|
|
94
|
+
const [newSkillSlug, setNewSkillSlug] = useState('')
|
|
95
|
+
const [newSkillContent, setNewSkillContent] = useState('')
|
|
96
|
+
const [adaptation, setAdaptation] = useState<{ status: string; messages: { type: string; text: string }[]; suggestions: { type: string; description: string }[] } | null>(null)
|
|
97
|
+
const [actionLoading, setActionLoading] = useState(false)
|
|
98
|
+
|
|
99
|
+
// ─── Graph Sub-View Data ──────────────────────────────────────
|
|
100
|
+
|
|
101
|
+
const { flowNodes, flowEdges } = useMemo(() => {
|
|
102
|
+
const flowNodes: Node[] = []
|
|
103
|
+
const ROW_HEIGHT = 240
|
|
104
|
+
|
|
105
|
+
function getEdgesFor(id: string) {
|
|
106
|
+
return {
|
|
107
|
+
inheritsFrom: graphEdges.filter(e => e.type === 'inherits' && e.to === id).map(e => e.from),
|
|
108
|
+
children: graphEdges.filter(e => e.type === 'inherits' && e.from === id).map(e => e.to),
|
|
109
|
+
enhances: [...new Set(graphEdges.filter(e => e.type === 'enhances' && (e.from === id || e.to === id)).map(e => e.from === id ? e.to : e.from))],
|
|
110
|
+
conflicts: [...new Set(graphEdges.filter(e => e.type === 'conflicts' && (e.from === id || e.to === id)).map(e => e.from === id ? e.to : e.from))],
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Columns: Generalized → Evolved → Original → Projects
|
|
115
|
+
const addNodes = (nodes: SkillNode[], x: number, category: string) => {
|
|
116
|
+
nodes.forEach((n, i) => {
|
|
117
|
+
const ne = getEdgesFor(n.id)
|
|
118
|
+
const evo = evolutionBySkill[n.id] ?? []
|
|
119
|
+
flowNodes.push({
|
|
120
|
+
id: n.id, type: 'skillNode',
|
|
121
|
+
position: { x, y: 80 + i * ROW_HEIGHT },
|
|
122
|
+
data: {
|
|
123
|
+
name: n.name, score: n.score, generation: n.generation,
|
|
124
|
+
category, layer: n.layer,
|
|
125
|
+
description: (skillContents[n.id] ?? '').slice(0, 100),
|
|
126
|
+
inheritsFrom: ne.inheritsFrom, children: ne.children,
|
|
127
|
+
enhances: ne.enhances, conflicts: ne.conflicts,
|
|
128
|
+
evolutionSummary: evo.filter(e => e.outcome === 'accepted').map(e => e.description),
|
|
129
|
+
tags: n.tags.length > 0 ? n.tags : [category],
|
|
130
|
+
},
|
|
131
|
+
})
|
|
132
|
+
})
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
addNodes(generalized, COLUMN_X.generalized, 'generalized')
|
|
136
|
+
addNodes(evolved, COLUMN_X.evolved, 'evolved')
|
|
137
|
+
addNodes(original, COLUMN_X.original, 'original')
|
|
138
|
+
|
|
139
|
+
// Project nodes
|
|
140
|
+
let projY = 80
|
|
141
|
+
projects.forEach(proj => {
|
|
142
|
+
flowNodes.push({
|
|
143
|
+
id: `project-${proj.name}`, type: 'skillNode',
|
|
144
|
+
position: { x: COLUMN_X.project, y: projY },
|
|
145
|
+
data: {
|
|
146
|
+
name: proj.name,
|
|
147
|
+
score: 1 - (proj.failures.filter((f: any) => !f.resolved).length / Math.max(1, proj.failures.length)),
|
|
148
|
+
generation: 0, category: 'project', layer: 'project',
|
|
149
|
+
description: `${proj.failures.length} failures, ${proj.skills.length} skills`,
|
|
150
|
+
inheritsFrom: [], children: [],
|
|
151
|
+
enhances: proj.skills, conflicts: [],
|
|
152
|
+
evolutionSummary: proj.failures.slice(0, 2).map((f: any) => `${f.userRequest.slice(0, 50)} → ${f.correction.slice(0, 30)}`),
|
|
153
|
+
tags: proj.skills.slice(0, 3),
|
|
154
|
+
},
|
|
155
|
+
})
|
|
156
|
+
projY += ROW_HEIGHT
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
// Edges
|
|
160
|
+
const flowEdges: Edge[] = []
|
|
161
|
+
const seenEdges = new Set<string>()
|
|
162
|
+
|
|
163
|
+
for (const e of graphEdges) {
|
|
164
|
+
const key = `${e.from}-${e.to}-${e.type}`
|
|
165
|
+
if (seenEdges.has(key)) continue
|
|
166
|
+
seenEdges.add(key)
|
|
167
|
+
if (!flowNodes.some(n => n.id === e.from) || !flowNodes.some(n => n.id === e.to)) continue
|
|
168
|
+
|
|
169
|
+
const color = EDGE_COLORS[e.type] ?? '#9ca3af'
|
|
170
|
+
flowEdges.push({
|
|
171
|
+
id: key, source: e.from, target: e.to,
|
|
172
|
+
sourceHandle: 'right', targetHandle: 'left',
|
|
173
|
+
type: 'smoothstep',
|
|
174
|
+
animated: e.type === 'co-evolves',
|
|
175
|
+
label: EDGE_LABELS[e.type],
|
|
176
|
+
labelStyle: { fontSize: 10, fontWeight: 500, fill: color },
|
|
177
|
+
labelBgStyle: { fill: '#fff', fillOpacity: 0.9 },
|
|
178
|
+
labelBgPadding: [4, 6] as [number, number],
|
|
179
|
+
labelBgBorderRadius: 4,
|
|
180
|
+
style: { stroke: color, strokeWidth: e.type === 'inherits' ? 2 : 1.5, strokeDasharray: e.type === 'conflicts' ? '6 4' : undefined },
|
|
181
|
+
markerEnd: { type: MarkerType.ArrowClosed, color, width: 12, height: 12 },
|
|
182
|
+
})
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Project ↔ skill edges
|
|
186
|
+
for (const proj of projects) {
|
|
187
|
+
for (const skillSlug of proj.skills) {
|
|
188
|
+
if (flowNodes.some(n => n.id === skillSlug)) {
|
|
189
|
+
flowEdges.push({
|
|
190
|
+
id: `project-${proj.name}-${skillSlug}`,
|
|
191
|
+
source: skillSlug, target: `project-${proj.name}`,
|
|
192
|
+
sourceHandle: 'right', targetHandle: 'left',
|
|
193
|
+
type: 'smoothstep',
|
|
194
|
+
label: 'used in',
|
|
195
|
+
labelStyle: { fontSize: 9, fill: '#d97706' },
|
|
196
|
+
labelBgStyle: { fill: '#fff', fillOpacity: 0.9 },
|
|
197
|
+
labelBgPadding: [3, 5] as [number, number],
|
|
198
|
+
labelBgBorderRadius: 3,
|
|
199
|
+
style: { stroke: '#fbbf24', strokeWidth: 1, strokeDasharray: '4 3' },
|
|
200
|
+
markerEnd: { type: MarkerType.ArrowClosed, color: '#fbbf24', width: 10, height: 10 },
|
|
201
|
+
})
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return { flowNodes, flowEdges }
|
|
207
|
+
}, [generalized, evolved, original, graphEdges, evolutionBySkill, skillContents, projects])
|
|
208
|
+
|
|
209
|
+
// ─── Detail Panel ─────────────────────────────────────────────
|
|
210
|
+
|
|
211
|
+
const selectedNode = allNodes.find(n => n.id === selectedSkill)
|
|
212
|
+
const selectedEvo = selectedSkill ? (evolutionBySkill[selectedSkill] ?? []) : []
|
|
213
|
+
const selectedContent = selectedSkill ? (skillContents[selectedSkill] ?? '') : ''
|
|
214
|
+
const selectedEdges = selectedSkill ? {
|
|
215
|
+
inheritsFrom: graphEdges.filter(e => e.type === 'inherits' && e.to === selectedSkill),
|
|
216
|
+
children: graphEdges.filter(e => e.type === 'inherits' && e.from === selectedSkill),
|
|
217
|
+
enhances: graphEdges.filter(e => e.type === 'enhances' && (e.from === selectedSkill || e.to === selectedSkill)),
|
|
218
|
+
conflicts: graphEdges.filter(e => e.type === 'conflicts' && (e.from === selectedSkill || e.to === selectedSkill)),
|
|
219
|
+
} : null
|
|
220
|
+
|
|
221
|
+
// ─── Helpers ──────────────────────────────────────────────────
|
|
222
|
+
|
|
223
|
+
const handleCreateSkill = async () => {
|
|
224
|
+
if (!newSkillSlug || !newSkillContent) return
|
|
225
|
+
setActionLoading(true)
|
|
226
|
+
const res = await fetch(`/api/skills`, { method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
227
|
+
body: JSON.stringify({ slug: newSkillSlug, content: newSkillContent }) })
|
|
228
|
+
const data = await res.json()
|
|
229
|
+
setAdaptation(data.adaptation)
|
|
230
|
+
setActionLoading(false)
|
|
231
|
+
if (data.success) {
|
|
232
|
+
setCreating(false)
|
|
233
|
+
setNewSkillSlug('')
|
|
234
|
+
setNewSkillContent('')
|
|
235
|
+
setTimeout(() => window.location.reload(), 1000)
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ─── Render ───────────────────────────────────────────────────
|
|
240
|
+
|
|
241
|
+
return (
|
|
242
|
+
<div>
|
|
243
|
+
{/* Page Header */}
|
|
244
|
+
<div className="page-header">
|
|
245
|
+
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
|
|
246
|
+
<div>
|
|
247
|
+
<h1 className="page-title">Skill Network</h1>
|
|
248
|
+
<p className="page-desc">
|
|
249
|
+
{stats.nodes} skills · {stats.edges} relationships · {stats.clusters} clusters
|
|
250
|
+
{stats.generalized > 0 && ` · ${stats.generalized} generalized`}
|
|
251
|
+
</p>
|
|
252
|
+
</div>
|
|
253
|
+
<button onClick={() => setCreating(!creating)} style={{
|
|
254
|
+
background: creating ? 'var(--bg-section)' : 'linear-gradient(135deg, var(--purple), #4f46e5)',
|
|
255
|
+
color: creating ? 'var(--text-dim)' : '#fff',
|
|
256
|
+
border: 'none', borderRadius: 8, padding: '8px 16px', fontSize: 13, fontWeight: 600,
|
|
257
|
+
cursor: 'pointer', display: 'flex', alignItems: 'center', gap: 6, fontFamily: 'var(--font)',
|
|
258
|
+
}}>{creating ? '✕ Cancel' : '+ New Skill'}</button>
|
|
259
|
+
</div>
|
|
260
|
+
</div>
|
|
261
|
+
|
|
262
|
+
{/* Create Skill Panel */}
|
|
263
|
+
{creating && (
|
|
264
|
+
<div className="card" style={{ marginBottom: 20 }}>
|
|
265
|
+
<div className="card-body">
|
|
266
|
+
<div className="card-header-label">Create New Skill</div>
|
|
267
|
+
<div style={{ display: 'flex', gap: 12, marginBottom: 12 }}>
|
|
268
|
+
<div style={{ flex: 1 }}>
|
|
269
|
+
<label style={{ fontSize: 11, fontWeight: 600, color: 'var(--text-dim)', display: 'block', marginBottom: 4 }}>Skill Slug</label>
|
|
270
|
+
<input value={newSkillSlug} onChange={e => setNewSkillSlug(e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, '-'))}
|
|
271
|
+
placeholder="my-new-skill" style={{
|
|
272
|
+
width: '100%', padding: '8px 12px', border: '1px solid var(--border)', borderRadius: 8,
|
|
273
|
+
fontSize: 13, fontFamily: 'var(--font-mono)', background: 'var(--bg-input)', color: 'var(--text)',
|
|
274
|
+
}} />
|
|
275
|
+
</div>
|
|
276
|
+
</div>
|
|
277
|
+
<label style={{ fontSize: 11, fontWeight: 600, color: 'var(--text-dim)', display: 'block', marginBottom: 4 }}>SKILL.md Content</label>
|
|
278
|
+
<textarea value={newSkillContent} onChange={e => setNewSkillContent(e.target.value)}
|
|
279
|
+
placeholder={`---\nname: My New Skill\ndescription: What this skill does\nlayer: domain\ntags: [tag1, tag2]\n---\n\n## Rules\n\n- Rule 1\n- Rule 2`}
|
|
280
|
+
style={{
|
|
281
|
+
width: '100%', height: 200, fontFamily: 'var(--font-mono)', fontSize: 11.5,
|
|
282
|
+
background: '#1e1e2e', color: '#cdd6f4', border: '1px solid var(--border)',
|
|
283
|
+
borderRadius: 8, padding: 12, resize: 'vertical', lineHeight: 1.6,
|
|
284
|
+
}} />
|
|
285
|
+
<div style={{ display: 'flex', gap: 8, marginTop: 12, justifyContent: 'flex-end' }}>
|
|
286
|
+
<button onClick={handleCreateSkill} disabled={!newSkillSlug || !newSkillContent || actionLoading}
|
|
287
|
+
style={{
|
|
288
|
+
background: newSkillSlug && newSkillContent ? 'linear-gradient(135deg, var(--green), #059669)' : 'var(--bg-section)',
|
|
289
|
+
color: newSkillSlug && newSkillContent ? '#fff' : 'var(--text-muted)',
|
|
290
|
+
border: 'none', borderRadius: 8, padding: '8px 20px', fontSize: 13, fontWeight: 600,
|
|
291
|
+
cursor: newSkillSlug && newSkillContent ? 'pointer' : 'default', fontFamily: 'var(--font)',
|
|
292
|
+
}}>{actionLoading ? 'Creating...' : '✓ Create Skill'}</button>
|
|
293
|
+
</div>
|
|
294
|
+
|
|
295
|
+
{/* Show adaptation feedback for create */}
|
|
296
|
+
{adaptation && creating && (
|
|
297
|
+
<div style={{ marginTop: 12 }}>
|
|
298
|
+
{adaptation.messages.map((m, i) => (
|
|
299
|
+
<div key={i} style={{ fontSize: 12, padding: '6px 10px', marginBottom: 3, borderRadius: 6,
|
|
300
|
+
background: m.type === 'warning' ? 'var(--yellow-light)' : 'var(--bg-section)',
|
|
301
|
+
color: m.type === 'warning' ? 'var(--yellow)' : 'var(--text-dim)',
|
|
302
|
+
}}>{m.type === 'warning' ? '⚠ ' : 'ℹ '}{m.text}</div>
|
|
303
|
+
))}
|
|
304
|
+
</div>
|
|
305
|
+
)}
|
|
306
|
+
</div>
|
|
307
|
+
</div>
|
|
308
|
+
)}
|
|
309
|
+
|
|
310
|
+
{/* Sub-View Tabs */}
|
|
311
|
+
<div className="tab-bar">
|
|
312
|
+
{SUB_VIEWS.map(sv => (
|
|
313
|
+
<button
|
|
314
|
+
key={sv.key}
|
|
315
|
+
className={`tab-item ${view === sv.key ? 'active' : ''}`}
|
|
316
|
+
onClick={() => { setView(sv.key); setSelectedSkill(null) }}
|
|
317
|
+
>
|
|
318
|
+
{sv.label}
|
|
319
|
+
</button>
|
|
320
|
+
))}
|
|
321
|
+
</div>
|
|
322
|
+
|
|
323
|
+
{/* Content area with optional detail panel */}
|
|
324
|
+
<div style={{ display: 'flex', gap: 0 }}>
|
|
325
|
+
{/* Main content */}
|
|
326
|
+
<div style={{ flex: 1, minWidth: 0 }}>
|
|
327
|
+
{view === 'graph' && <GraphView flowNodes={flowNodes} flowEdges={flowEdges} stats={stats} />}
|
|
328
|
+
{view === 'general' && (
|
|
329
|
+
<GeneralView
|
|
330
|
+
generalized={generalized} evolved={evolved} original={original}
|
|
331
|
+
edges={graphEdges} evolutionBySkill={evolutionBySkill} skillContents={skillContents}
|
|
332
|
+
onSelect={setSelectedSkill} selected={selectedSkill}
|
|
333
|
+
/>
|
|
334
|
+
)}
|
|
335
|
+
{view === 'projects' && (
|
|
336
|
+
<ProjectsView projects={projects} allNodes={allNodes} onSelect={setSelectedSkill} selected={selectedSkill} />
|
|
337
|
+
)}
|
|
338
|
+
{view === 'coevolution' && (
|
|
339
|
+
<CoEvolutionView
|
|
340
|
+
generalized={generalized} evolved={evolved} original={original}
|
|
341
|
+
edges={graphEdges} evolutionBySkill={evolutionBySkill} projects={projects}
|
|
342
|
+
/>
|
|
343
|
+
)}
|
|
344
|
+
</div>
|
|
345
|
+
|
|
346
|
+
{/* Detail Panel */}
|
|
347
|
+
{selectedSkill && selectedNode && (
|
|
348
|
+
<div className="detail-panel">
|
|
349
|
+
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 16 }}>
|
|
350
|
+
<div>
|
|
351
|
+
<div style={{ fontSize: 17, fontWeight: 700, marginBottom: 4 }}>{selectedNode.name}</div>
|
|
352
|
+
<div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
|
|
353
|
+
<span className="badge badge-gray">{selectedNode.layer}</span>
|
|
354
|
+
{selectedNode.generation > 0 && <span className="badge badge-green">gen {selectedNode.generation}</span>}
|
|
355
|
+
{selectedNode.tags.map(t => <span key={t} className="badge badge-gray">{t}</span>)}
|
|
356
|
+
</div>
|
|
357
|
+
</div>
|
|
358
|
+
<button onClick={() => setSelectedSkill(null)} style={{
|
|
359
|
+
background: 'var(--bg-section)', border: 'none', borderRadius: 6, width: 28, height: 28,
|
|
360
|
+
cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
361
|
+
color: 'var(--text-dim)', fontSize: 14,
|
|
362
|
+
}}>✕</button>
|
|
363
|
+
</div>
|
|
364
|
+
|
|
365
|
+
{/* Score */}
|
|
366
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 20, padding: '12px 14px', background: 'var(--bg-section)', borderRadius: 10 }}>
|
|
367
|
+
<div style={{ fontSize: 32, fontWeight: 800, color: scoreColor(selectedNode.score), lineHeight: 1 }}>
|
|
368
|
+
{(selectedNode.score * 100).toFixed(0)}
|
|
369
|
+
</div>
|
|
370
|
+
<div style={{ flex: 1 }}>
|
|
371
|
+
<div style={{ fontSize: 11, color: 'var(--text-dim)', marginBottom: 4 }}>Quality Score</div>
|
|
372
|
+
<div className="score-track" style={{ height: 6 }}>
|
|
373
|
+
<div className="score-fill" style={{
|
|
374
|
+
width: `${selectedNode.score * 100}%`,
|
|
375
|
+
background: `linear-gradient(90deg, var(--blue), ${scoreColor(selectedNode.score)})`,
|
|
376
|
+
}} />
|
|
377
|
+
</div>
|
|
378
|
+
</div>
|
|
379
|
+
</div>
|
|
380
|
+
|
|
381
|
+
{/* Connections */}
|
|
382
|
+
{selectedEdges && (selectedEdges.inheritsFrom.length > 0 || selectedEdges.children.length > 0 || selectedEdges.enhances.length > 0 || selectedEdges.conflicts.length > 0) && (
|
|
383
|
+
<div style={{ marginBottom: 20 }}>
|
|
384
|
+
<div className="section-label">Connections</div>
|
|
385
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
|
386
|
+
{selectedEdges.inheritsFrom.length > 0 && (
|
|
387
|
+
<div style={{ fontSize: 12 }}>
|
|
388
|
+
<span style={{ color: 'var(--text-dim)' }}>Inherits from: </span>
|
|
389
|
+
{selectedEdges.inheritsFrom.map(e => <span key={e.from} className="badge badge-purple" style={{ marginRight: 3 }}>{e.from}</span>)}
|
|
390
|
+
</div>
|
|
391
|
+
)}
|
|
392
|
+
{selectedEdges.children.length > 0 && (
|
|
393
|
+
<div style={{ fontSize: 12 }}>
|
|
394
|
+
<span style={{ color: 'var(--text-dim)' }}>Children: </span>
|
|
395
|
+
{selectedEdges.children.map(e => <span key={e.to} className="badge badge-blue" style={{ marginRight: 3 }}>{e.to}</span>)}
|
|
396
|
+
</div>
|
|
397
|
+
)}
|
|
398
|
+
{selectedEdges.enhances.length > 0 && (
|
|
399
|
+
<div style={{ fontSize: 12 }}>
|
|
400
|
+
<span style={{ color: 'var(--text-dim)' }}>Enhances: </span>
|
|
401
|
+
{[...new Set(selectedEdges.enhances.map(e => e.from === selectedSkill ? e.to : e.from))].map(id =>
|
|
402
|
+
<span key={id} className="badge badge-green" style={{ marginRight: 3 }}>{id}</span>
|
|
403
|
+
)}
|
|
404
|
+
</div>
|
|
405
|
+
)}
|
|
406
|
+
{selectedEdges.conflicts.length > 0 && (
|
|
407
|
+
<div style={{ fontSize: 12 }}>
|
|
408
|
+
<span style={{ color: 'var(--text-dim)' }}>Conflicts: </span>
|
|
409
|
+
{[...new Set(selectedEdges.conflicts.map(e => e.from === selectedSkill ? e.to : e.from))].map(id =>
|
|
410
|
+
<span key={id} className="badge badge-red" style={{ marginRight: 3 }}>{id}</span>
|
|
411
|
+
)}
|
|
412
|
+
</div>
|
|
413
|
+
)}
|
|
414
|
+
</div>
|
|
415
|
+
</div>
|
|
416
|
+
)}
|
|
417
|
+
|
|
418
|
+
{/* Evolution History */}
|
|
419
|
+
{selectedEvo.length > 0 && (
|
|
420
|
+
<div style={{ marginBottom: 20 }}>
|
|
421
|
+
<div className="section-label">Evolution History</div>
|
|
422
|
+
{selectedEvo.map((e, i) => (
|
|
423
|
+
<div key={i} style={{
|
|
424
|
+
padding: '8px 12px', marginBottom: 6, borderRadius: 8,
|
|
425
|
+
background: 'var(--bg-section)',
|
|
426
|
+
borderLeft: `3px solid ${e.outcome === 'accepted' ? 'var(--green)' : 'var(--red)'}`,
|
|
427
|
+
}}>
|
|
428
|
+
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 10, marginBottom: 3 }}>
|
|
429
|
+
<span className={`badge ${e.outcome === 'accepted' ? 'badge-green' : 'badge-red'}`}>{e.outcome}</span>
|
|
430
|
+
<span style={{ color: 'var(--text-muted)' }}>{new Date(e.timestamp).toLocaleDateString()}</span>
|
|
431
|
+
</div>
|
|
432
|
+
<div style={{ fontSize: 12, color: 'var(--text-secondary)', lineHeight: 1.5, marginBottom: 4 }}>{e.description.slice(0, 150)}</div>
|
|
433
|
+
<div style={{ display: 'flex', gap: 8, fontSize: 10 }}>
|
|
434
|
+
<span className="score-pill">T:{e.task}</span>
|
|
435
|
+
<span className="score-pill">A:{e.align}</span>
|
|
436
|
+
<span className="score-pill">S:{e.sideEffect}</span>
|
|
437
|
+
</div>
|
|
438
|
+
</div>
|
|
439
|
+
))}
|
|
440
|
+
</div>
|
|
441
|
+
)}
|
|
442
|
+
|
|
443
|
+
{/* ─── Management Actions ─── */}
|
|
444
|
+
<div style={{ marginBottom: 20 }}>
|
|
445
|
+
<div className="section-label">Actions</div>
|
|
446
|
+
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
|
|
447
|
+
<button onClick={() => { setEditing(!editing); setEditContent(skillContents[selectedSkill!] ?? ''); setAdaptation(null) }}
|
|
448
|
+
style={actionBtnStyle(editing ? 'var(--purple)' : undefined)}>
|
|
449
|
+
{editing ? '✕ Cancel' : '✎ Edit'}
|
|
450
|
+
</button>
|
|
451
|
+
<button onClick={async () => {
|
|
452
|
+
if (!selectedNode) return
|
|
453
|
+
const layers = ['project', 'domain', 'system'] as const
|
|
454
|
+
const currentIdx = layers.indexOf(selectedNode.layer as typeof layers[number])
|
|
455
|
+
const nextLayer = layers[Math.min(currentIdx + 1, 2)]
|
|
456
|
+
if (nextLayer === selectedNode.layer) return
|
|
457
|
+
setActionLoading(true)
|
|
458
|
+
const res = await fetch(`/api/skills`, { method: 'PUT', headers: { 'Content-Type': 'application/json' },
|
|
459
|
+
body: JSON.stringify({ slug: selectedSkill, layer: nextLayer }) })
|
|
460
|
+
const data = await res.json()
|
|
461
|
+
setAdaptation(data.adaptation)
|
|
462
|
+
setActionLoading(false)
|
|
463
|
+
}} style={actionBtnStyle('var(--blue)')}>
|
|
464
|
+
↑ Promote
|
|
465
|
+
</button>
|
|
466
|
+
<button onClick={async () => {
|
|
467
|
+
if (!selectedNode) return
|
|
468
|
+
const layers = ['project', 'domain', 'system'] as const
|
|
469
|
+
const currentIdx = layers.indexOf(selectedNode.layer as typeof layers[number])
|
|
470
|
+
const nextLayer = layers[Math.max(currentIdx - 1, 0)]
|
|
471
|
+
if (nextLayer === selectedNode.layer) return
|
|
472
|
+
setActionLoading(true)
|
|
473
|
+
const res = await fetch(`/api/skills`, { method: 'PUT', headers: { 'Content-Type': 'application/json' },
|
|
474
|
+
body: JSON.stringify({ slug: selectedSkill, layer: nextLayer }) })
|
|
475
|
+
const data = await res.json()
|
|
476
|
+
setAdaptation(data.adaptation)
|
|
477
|
+
setActionLoading(false)
|
|
478
|
+
}} style={actionBtnStyle('var(--yellow)')}>
|
|
479
|
+
↓ Demote
|
|
480
|
+
</button>
|
|
481
|
+
<button onClick={async () => {
|
|
482
|
+
if (!confirm(`Delete "${selectedNode?.name}"? This cannot be undone.`)) return
|
|
483
|
+
setActionLoading(true)
|
|
484
|
+
const res = await fetch(`/api/skills?slug=${selectedSkill}`, { method: 'DELETE' })
|
|
485
|
+
const data = await res.json()
|
|
486
|
+
setAdaptation(data.adaptation)
|
|
487
|
+
setActionLoading(false)
|
|
488
|
+
if (data.success) setTimeout(() => window.location.reload(), 1500)
|
|
489
|
+
}} style={actionBtnStyle('var(--red)')}>
|
|
490
|
+
✕ Delete
|
|
491
|
+
</button>
|
|
492
|
+
</div>
|
|
493
|
+
</div>
|
|
494
|
+
|
|
495
|
+
{/* ─── Edit Mode ─── */}
|
|
496
|
+
{editing && (
|
|
497
|
+
<div style={{ marginBottom: 20 }}>
|
|
498
|
+
<div className="section-label">Edit Skill Content</div>
|
|
499
|
+
<textarea
|
|
500
|
+
value={editContent}
|
|
501
|
+
onChange={e => setEditContent(e.target.value)}
|
|
502
|
+
style={{
|
|
503
|
+
width: '100%', height: 250, fontFamily: 'var(--font-mono)', fontSize: 11,
|
|
504
|
+
background: '#1e1e2e', color: '#cdd6f4', border: '1px solid var(--border)',
|
|
505
|
+
borderRadius: 8, padding: 12, resize: 'vertical', lineHeight: 1.6,
|
|
506
|
+
}}
|
|
507
|
+
/>
|
|
508
|
+
<button onClick={async () => {
|
|
509
|
+
setActionLoading(true)
|
|
510
|
+
const res = await fetch(`/api/skills`, { method: 'PUT', headers: { 'Content-Type': 'application/json' },
|
|
511
|
+
body: JSON.stringify({ slug: selectedSkill, content: `---\n${Object.entries(selectedNode ? { name: selectedNode.name, description: '', layer: selectedNode.layer } : {}).map(([k,v]) => `${k}: ${v}`).join('\n')}\n---\n\n${editContent}` }) })
|
|
512
|
+
const data = await res.json()
|
|
513
|
+
setAdaptation(data.adaptation)
|
|
514
|
+
setEditing(false)
|
|
515
|
+
setActionLoading(false)
|
|
516
|
+
setTimeout(() => window.location.reload(), 1000)
|
|
517
|
+
}} disabled={actionLoading}
|
|
518
|
+
style={{ ...actionBtnStyle('var(--green)'), width: '100%', marginTop: 8 }}>
|
|
519
|
+
{actionLoading ? 'Saving...' : '✓ Save Changes'}
|
|
520
|
+
</button>
|
|
521
|
+
</div>
|
|
522
|
+
)}
|
|
523
|
+
|
|
524
|
+
{/* ─── Network Adaptation Feedback ─── */}
|
|
525
|
+
{adaptation && (
|
|
526
|
+
<div style={{ marginBottom: 20 }}>
|
|
527
|
+
<div className="section-label">
|
|
528
|
+
Network Adaptation
|
|
529
|
+
<span style={{
|
|
530
|
+
marginLeft: 8, fontSize: 10, fontWeight: 600, padding: '2px 8px', borderRadius: 6,
|
|
531
|
+
background: adaptation.status === 'ok' ? 'var(--green-light)' : adaptation.status === 'warning' ? 'var(--yellow-light)' : 'var(--red-light)',
|
|
532
|
+
color: adaptation.status === 'ok' ? 'var(--green)' : adaptation.status === 'warning' ? 'var(--yellow)' : 'var(--red)',
|
|
533
|
+
textTransform: 'none', letterSpacing: 0,
|
|
534
|
+
}}>{adaptation.status}</span>
|
|
535
|
+
</div>
|
|
536
|
+
{adaptation.messages.map((m, i) => (
|
|
537
|
+
<div key={i} style={{
|
|
538
|
+
padding: '8px 12px', marginBottom: 4, borderRadius: 6, fontSize: 12, lineHeight: 1.5,
|
|
539
|
+
background: m.type === 'warning' ? 'var(--yellow-light)' : m.type === 'action' ? 'var(--red-light)' : 'var(--bg-section)',
|
|
540
|
+
color: m.type === 'warning' ? 'var(--yellow)' : m.type === 'action' ? 'var(--red)' : 'var(--text-secondary)',
|
|
541
|
+
border: `1px solid ${m.type === 'warning' ? 'var(--yellow-border)' : m.type === 'action' ? 'var(--red-border)' : 'var(--border)'}`,
|
|
542
|
+
}}>{m.type === 'warning' ? '⚠ ' : m.type === 'action' ? '⚡ ' : 'ℹ '}{m.text}</div>
|
|
543
|
+
))}
|
|
544
|
+
{adaptation.suggestions.length > 0 && (
|
|
545
|
+
<div style={{ marginTop: 8 }}>
|
|
546
|
+
<div style={{ fontSize: 10, fontWeight: 600, color: 'var(--text-muted)', marginBottom: 4 }}>SUGGESTIONS</div>
|
|
547
|
+
{adaptation.suggestions.map((s, i) => (
|
|
548
|
+
<div key={i} style={{
|
|
549
|
+
padding: '6px 10px', fontSize: 11, color: 'var(--text-dim)', lineHeight: 1.5,
|
|
550
|
+
borderLeft: '2px solid var(--purple)', marginBottom: 4, paddingLeft: 10,
|
|
551
|
+
}}>
|
|
552
|
+
<span className="badge badge-purple" style={{ marginRight: 6 }}>{s.type}</span>
|
|
553
|
+
{s.description}
|
|
554
|
+
</div>
|
|
555
|
+
))}
|
|
556
|
+
</div>
|
|
557
|
+
)}
|
|
558
|
+
</div>
|
|
559
|
+
)}
|
|
560
|
+
|
|
561
|
+
{/* Skill Content (read-only when not editing) */}
|
|
562
|
+
{selectedContent && !editing && (
|
|
563
|
+
<div>
|
|
564
|
+
<div className="section-label">Skill Content</div>
|
|
565
|
+
<pre style={{
|
|
566
|
+
background: 'var(--bg-section)', borderRadius: 8, padding: 12,
|
|
567
|
+
whiteSpace: 'pre-wrap', wordBreak: 'break-word',
|
|
568
|
+
color: 'var(--text-secondary)', lineHeight: 1.6, maxHeight: 300, overflow: 'auto',
|
|
569
|
+
fontSize: 11, border: '1px solid var(--border)',
|
|
570
|
+
}}>
|
|
571
|
+
{selectedContent.slice(0, 1500)}
|
|
572
|
+
</pre>
|
|
573
|
+
</div>
|
|
574
|
+
)}
|
|
575
|
+
</div>
|
|
576
|
+
)}
|
|
577
|
+
</div>
|
|
578
|
+
</div>
|
|
579
|
+
)
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
583
|
+
// SUB-VIEW: Graph
|
|
584
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
585
|
+
|
|
586
|
+
function GraphView({ flowNodes, flowEdges, stats }: { flowNodes: Node[]; flowEdges: Edge[]; stats: Props['stats'] }) {
|
|
587
|
+
return (
|
|
588
|
+
<div style={{ height: 'calc(100vh - 200px)', width: '100%', borderRadius: 12, overflow: 'hidden', border: '1px solid var(--border)', background: '#fafafa' }}>
|
|
589
|
+
{/* Column Headers */}
|
|
590
|
+
<div style={{
|
|
591
|
+
position: 'absolute', top: 8, left: 0, right: 0, zIndex: 10,
|
|
592
|
+
display: 'flex', justifyContent: 'space-around', padding: '0 60px',
|
|
593
|
+
pointerEvents: 'none',
|
|
594
|
+
}}>
|
|
595
|
+
{[
|
|
596
|
+
{ label: 'GENERALIZED', color: '#7c3aed', count: stats.generalized },
|
|
597
|
+
{ label: 'EVOLVED', color: '#16a34a', count: stats.evolved },
|
|
598
|
+
{ label: 'ORIGINAL', color: '#6b7280', count: stats.original },
|
|
599
|
+
{ label: 'PROJECTS', color: '#b45309', count: 0 },
|
|
600
|
+
].map(col => (
|
|
601
|
+
<div key={col.label} style={{
|
|
602
|
+
background: '#fff', border: '1px solid #e5e7eb', borderRadius: 8,
|
|
603
|
+
padding: '5px 14px', textAlign: 'center',
|
|
604
|
+
boxShadow: '0 1px 4px rgba(0,0,0,0.05)',
|
|
605
|
+
}}>
|
|
606
|
+
<div style={{ fontSize: 9, fontWeight: 700, letterSpacing: 1.5, color: col.color }}>{col.label}</div>
|
|
607
|
+
{col.count > 0 && <div style={{ fontSize: 9, color: '#9ca3af', marginTop: 1 }}>{col.count} skills</div>}
|
|
608
|
+
</div>
|
|
609
|
+
))}
|
|
610
|
+
</div>
|
|
611
|
+
|
|
612
|
+
<ReactFlow
|
|
613
|
+
nodes={flowNodes}
|
|
614
|
+
edges={flowEdges}
|
|
615
|
+
nodeTypes={nodeTypes}
|
|
616
|
+
connectionLineType={ConnectionLineType.SmoothStep}
|
|
617
|
+
fitView
|
|
618
|
+
fitViewOptions={{ padding: 0.2 }}
|
|
619
|
+
proOptions={{ hideAttribution: true }}
|
|
620
|
+
style={{ background: '#fafafa' }}
|
|
621
|
+
minZoom={0.3}
|
|
622
|
+
maxZoom={1.5}
|
|
623
|
+
>
|
|
624
|
+
<Background color="#e5e7eb" gap={20} size={1} />
|
|
625
|
+
<Controls position="bottom-left" />
|
|
626
|
+
</ReactFlow>
|
|
627
|
+
|
|
628
|
+
{/* Legend */}
|
|
629
|
+
<div style={{
|
|
630
|
+
position: 'absolute', bottom: 12, right: 12, zIndex: 10,
|
|
631
|
+
background: '#fff', border: '1px solid #e5e7eb', borderRadius: 8,
|
|
632
|
+
padding: '8px 14px', display: 'flex', gap: 14, fontSize: 10, color: '#6b7280',
|
|
633
|
+
boxShadow: '0 1px 3px rgba(0,0,0,0.06)',
|
|
634
|
+
}}>
|
|
635
|
+
<span><span style={{ display: 'inline-block', width: 12, height: 2, background: '#818cf8', marginRight: 4, verticalAlign: 'middle' }} /> inherits</span>
|
|
636
|
+
<span><span style={{ display: 'inline-block', width: 12, height: 2, background: '#22c55e', marginRight: 4, verticalAlign: 'middle' }} /> enhances</span>
|
|
637
|
+
<span><span style={{ display: 'inline-block', width: 12, height: 2, background: '#ef4444', marginRight: 4, verticalAlign: 'middle', borderTop: '1px dashed #ef4444' }} /> conflicts</span>
|
|
638
|
+
<span><span style={{ display: 'inline-block', width: 12, height: 2, background: '#fbbf24', marginRight: 4, verticalAlign: 'middle' }} /> project</span>
|
|
639
|
+
<span style={{ color: '#b0b5c8' }}>Scroll to zoom · Drag to pan</span>
|
|
640
|
+
</div>
|
|
641
|
+
</div>
|
|
642
|
+
)
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
646
|
+
// SUB-VIEW: General Skills
|
|
647
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
648
|
+
|
|
649
|
+
function GeneralView({
|
|
650
|
+
generalized, evolved, original, edges, evolutionBySkill, skillContents, onSelect, selected,
|
|
651
|
+
}: {
|
|
652
|
+
generalized: SkillNode[]; evolved: SkillNode[]; original: SkillNode[]
|
|
653
|
+
edges: GraphEdge[]; evolutionBySkill: Record<string, EvolutionEntry[]>
|
|
654
|
+
skillContents: Record<string, string>; onSelect: (id: string) => void; selected: string | null
|
|
655
|
+
}) {
|
|
656
|
+
function SkillCard({ node, category }: { node: SkillNode; category: string }) {
|
|
657
|
+
const nodeEdges = {
|
|
658
|
+
inheritsFrom: edges.filter(e => e.type === 'inherits' && e.to === node.id),
|
|
659
|
+
children: edges.filter(e => e.type === 'inherits' && e.from === node.id),
|
|
660
|
+
enhances: edges.filter(e => e.type === 'enhances' && (e.from === node.id || e.to === node.id)),
|
|
661
|
+
}
|
|
662
|
+
const evo = evolutionBySkill[node.id] ?? []
|
|
663
|
+
const borderColor = category === 'generalized' ? 'var(--purple)' : category === 'evolved' ? 'var(--green)' : 'var(--border)'
|
|
664
|
+
const isSelected = selected === node.id
|
|
665
|
+
|
|
666
|
+
return (
|
|
667
|
+
<div
|
|
668
|
+
className={`card ${isSelected ? 'selected' : ''}`}
|
|
669
|
+
onClick={() => onSelect(node.id)}
|
|
670
|
+
style={{ borderLeft: `3px solid ${borderColor}`, cursor: 'pointer', overflow: 'hidden' }}
|
|
671
|
+
>
|
|
672
|
+
<div className="card-body" style={{ padding: 16 }}>
|
|
673
|
+
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 10 }}>
|
|
674
|
+
<div>
|
|
675
|
+
<div style={{ fontSize: 14, fontWeight: 700, marginBottom: 4 }}>{node.name}</div>
|
|
676
|
+
<div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
|
|
677
|
+
<span className={`badge ${category === 'generalized' ? 'badge-purple' : category === 'evolved' ? 'badge-green' : 'badge-gray'}`}>
|
|
678
|
+
{category}
|
|
679
|
+
</span>
|
|
680
|
+
{node.generation > 0 && <span className="badge badge-green">gen {node.generation}</span>}
|
|
681
|
+
<span className="badge badge-gray">{node.layer}</span>
|
|
682
|
+
</div>
|
|
683
|
+
</div>
|
|
684
|
+
<div style={{ textAlign: 'right' }}>
|
|
685
|
+
<div style={{ fontSize: 24, fontWeight: 800, color: scoreColor(node.score), lineHeight: 1 }}>
|
|
686
|
+
{(node.score * 100).toFixed(0)}
|
|
687
|
+
</div>
|
|
688
|
+
<div style={{ fontSize: 10, color: 'var(--text-muted)' }}>score</div>
|
|
689
|
+
</div>
|
|
690
|
+
</div>
|
|
691
|
+
|
|
692
|
+
<div className="score-track" style={{ height: 4, marginBottom: 12 }}>
|
|
693
|
+
<div className="score-fill" style={{
|
|
694
|
+
width: `${node.score * 100}%`,
|
|
695
|
+
background: `linear-gradient(90deg, var(--blue), ${scoreColor(node.score)})`,
|
|
696
|
+
}} />
|
|
697
|
+
</div>
|
|
698
|
+
|
|
699
|
+
{/* Connections summary */}
|
|
700
|
+
{(nodeEdges.inheritsFrom.length > 0 || nodeEdges.children.length > 0 || nodeEdges.enhances.length > 0) && (
|
|
701
|
+
<div style={{ display: 'flex', gap: 8, fontSize: 11, marginBottom: 10, flexWrap: 'wrap' }}>
|
|
702
|
+
{nodeEdges.inheritsFrom.length > 0 && (
|
|
703
|
+
<span style={{ color: 'var(--text-dim)' }}>↑ {nodeEdges.inheritsFrom.length} parent{nodeEdges.inheritsFrom.length > 1 ? 's' : ''}</span>
|
|
704
|
+
)}
|
|
705
|
+
{nodeEdges.children.length > 0 && (
|
|
706
|
+
<span style={{ color: 'var(--text-dim)' }}>↓ {nodeEdges.children.length} child{nodeEdges.children.length > 1 ? 'ren' : ''}</span>
|
|
707
|
+
)}
|
|
708
|
+
{nodeEdges.enhances.length > 0 && (
|
|
709
|
+
<span style={{ color: 'var(--text-dim)' }}>↔ {nodeEdges.enhances.length} enhance</span>
|
|
710
|
+
)}
|
|
711
|
+
</div>
|
|
712
|
+
)}
|
|
713
|
+
|
|
714
|
+
{/* Evolution badges */}
|
|
715
|
+
{evo.length > 0 && (
|
|
716
|
+
<div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
|
|
717
|
+
{evo.slice(0, 3).map((e, i) => (
|
|
718
|
+
<span key={i} className={`badge ${e.outcome === 'accepted' ? 'badge-green' : 'badge-red'}`} style={{ fontSize: 9 }}>
|
|
719
|
+
{e.action} {e.outcome === 'accepted' ? '✓' : '✕'}
|
|
720
|
+
</span>
|
|
721
|
+
))}
|
|
722
|
+
{evo.length > 3 && <span className="badge badge-gray" style={{ fontSize: 9 }}>+{evo.length - 3}</span>}
|
|
723
|
+
</div>
|
|
724
|
+
)}
|
|
725
|
+
</div>
|
|
726
|
+
</div>
|
|
727
|
+
)
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
return (
|
|
731
|
+
<div>
|
|
732
|
+
{/* Generalized Layer */}
|
|
733
|
+
{generalized.length > 0 && (
|
|
734
|
+
<div className="layer-section layer-generalized">
|
|
735
|
+
<div className="section-label" style={{ color: 'var(--purple)' }}>
|
|
736
|
+
Generalized Skills
|
|
737
|
+
<span style={{ fontWeight: 400, color: 'var(--text-dim)', fontSize: 11, textTransform: 'none', letterSpacing: 0 }}>
|
|
738
|
+
— abstracted from cross-project patterns
|
|
739
|
+
</span>
|
|
740
|
+
</div>
|
|
741
|
+
<div className="grid-auto">
|
|
742
|
+
{generalized.map(n => <SkillCard key={n.id} node={n} category="generalized" />)}
|
|
743
|
+
</div>
|
|
744
|
+
<div className="flow-arrow">▼ specializes into domain skills ▼</div>
|
|
745
|
+
</div>
|
|
746
|
+
)}
|
|
747
|
+
|
|
748
|
+
{/* Evolved Layer */}
|
|
749
|
+
{evolved.length > 0 && (
|
|
750
|
+
<div className="layer-section layer-evolved">
|
|
751
|
+
<div className="section-label" style={{ color: 'var(--green)' }}>
|
|
752
|
+
Evolved Skills
|
|
753
|
+
<span style={{ fontWeight: 400, color: 'var(--text-dim)', fontSize: 11, textTransform: 'none', letterSpacing: 0 }}>
|
|
754
|
+
— improved through failure-driven evolution
|
|
755
|
+
</span>
|
|
756
|
+
</div>
|
|
757
|
+
<div className="grid-auto">
|
|
758
|
+
{evolved.map(n => <SkillCard key={n.id} node={n} category="evolved" />)}
|
|
759
|
+
</div>
|
|
760
|
+
</div>
|
|
761
|
+
)}
|
|
762
|
+
|
|
763
|
+
{/* Original Layer */}
|
|
764
|
+
{original.length > 0 && (
|
|
765
|
+
<div className="layer-section layer-original">
|
|
766
|
+
<div className="section-label" style={{ color: 'var(--text-dim)' }}>
|
|
767
|
+
Original Skills
|
|
768
|
+
<span style={{ fontWeight: 400, fontSize: 11, textTransform: 'none', letterSpacing: 0 }}>
|
|
769
|
+
— not yet evolved
|
|
770
|
+
</span>
|
|
771
|
+
</div>
|
|
772
|
+
<div className="grid-auto">
|
|
773
|
+
{original.map(n => <SkillCard key={n.id} node={n} category="original" />)}
|
|
774
|
+
</div>
|
|
775
|
+
{generalized.length > 0 && <div className="flow-arrow">▲ common patterns generalize upward ▲</div>}
|
|
776
|
+
</div>
|
|
777
|
+
)}
|
|
778
|
+
</div>
|
|
779
|
+
)
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
783
|
+
// SUB-VIEW: Project Skills
|
|
784
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
785
|
+
|
|
786
|
+
function ProjectsView({
|
|
787
|
+
projects, allNodes, onSelect, selected,
|
|
788
|
+
}: {
|
|
789
|
+
projects: ProjectData[]; allNodes: SkillNode[]
|
|
790
|
+
onSelect: (id: string) => void; selected: string | null
|
|
791
|
+
}) {
|
|
792
|
+
if (projects.length === 0) {
|
|
793
|
+
return (
|
|
794
|
+
<div className="card">
|
|
795
|
+
<div className="empty-state">
|
|
796
|
+
<div className="empty-state-icon">
|
|
797
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
|
|
798
|
+
<path d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
|
|
799
|
+
</svg>
|
|
800
|
+
</div>
|
|
801
|
+
<div className="empty-state-title">No projects detected</div>
|
|
802
|
+
<div className="empty-state-desc">
|
|
803
|
+
Projects are auto-detected from captured failures. Use <code>helixevo capture --project name</code> to tag failures.
|
|
804
|
+
</div>
|
|
805
|
+
</div>
|
|
806
|
+
</div>
|
|
807
|
+
)
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
return (
|
|
811
|
+
<div style={{ display: 'grid', gap: 16 }}>
|
|
812
|
+
{projects.map(project => {
|
|
813
|
+
const unresolved = project.failures.filter((f: any) => !f.resolved)
|
|
814
|
+
const resolvedPct = project.failures.length > 0 ? ((project.failures.length - unresolved.length) / project.failures.length * 100).toFixed(0) : '0'
|
|
815
|
+
|
|
816
|
+
return (
|
|
817
|
+
<div key={project.name} className="card" style={{ overflow: 'hidden' }}>
|
|
818
|
+
{/* Project Header */}
|
|
819
|
+
<div style={{
|
|
820
|
+
padding: '16px 20px',
|
|
821
|
+
background: 'linear-gradient(135deg, #fffbeb, #fef3c7)',
|
|
822
|
+
borderBottom: '1px solid var(--yellow-border)',
|
|
823
|
+
}}>
|
|
824
|
+
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
825
|
+
<div>
|
|
826
|
+
<div style={{ fontSize: 17, fontWeight: 700, marginBottom: 6 }}>{project.name}</div>
|
|
827
|
+
<div style={{ display: 'flex', gap: 6 }}>
|
|
828
|
+
<span className="badge badge-yellow">{project.failures.length} failures</span>
|
|
829
|
+
<span className="badge badge-blue">{project.skills.length} skills</span>
|
|
830
|
+
<span className="badge badge-green">{resolvedPct}% resolved</span>
|
|
831
|
+
</div>
|
|
832
|
+
</div>
|
|
833
|
+
{unresolved.length >= 3 && (
|
|
834
|
+
<div style={{
|
|
835
|
+
padding: '6px 14px', borderRadius: 8,
|
|
836
|
+
background: 'var(--green-light)', color: 'var(--green)',
|
|
837
|
+
fontSize: 11, fontWeight: 600,
|
|
838
|
+
}}>
|
|
839
|
+
Ready to specialize →
|
|
840
|
+
</div>
|
|
841
|
+
)}
|
|
842
|
+
</div>
|
|
843
|
+
</div>
|
|
844
|
+
|
|
845
|
+
<div className="card-body">
|
|
846
|
+
{/* Skills */}
|
|
847
|
+
<div className="section-label">Skills Involved</div>
|
|
848
|
+
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap', marginBottom: 16 }}>
|
|
849
|
+
{project.skills.map(s => {
|
|
850
|
+
const node = allNodes.find(n => n.id === s)
|
|
851
|
+
return (
|
|
852
|
+
<div key={s} onClick={() => onSelect(s)} style={{
|
|
853
|
+
padding: '6px 12px', borderRadius: 8, background: 'var(--bg-section)',
|
|
854
|
+
border: `1px solid ${selected === s ? 'var(--purple)' : 'var(--border)'}`,
|
|
855
|
+
fontSize: 12, display: 'flex', alignItems: 'center', gap: 6, cursor: 'pointer',
|
|
856
|
+
transition: 'all 0.15s',
|
|
857
|
+
}}>
|
|
858
|
+
<span style={{ fontWeight: 500 }}>{s}</span>
|
|
859
|
+
{node && (
|
|
860
|
+
<span style={{ fontSize: 11, fontWeight: 700, color: node.score >= 0.8 ? 'var(--green)' : 'var(--yellow)' }}>
|
|
861
|
+
{(node.score * 100).toFixed(0)}
|
|
862
|
+
</span>
|
|
863
|
+
)}
|
|
864
|
+
{node?.generation ? <span className="badge badge-green" style={{ fontSize: 9 }}>gen {node.generation}</span> : null}
|
|
865
|
+
</div>
|
|
866
|
+
)
|
|
867
|
+
})}
|
|
868
|
+
</div>
|
|
869
|
+
|
|
870
|
+
{/* Failures */}
|
|
871
|
+
<div className="section-label">Recent Failures</div>
|
|
872
|
+
{project.failures.slice(0, 5).map((f: any) => (
|
|
873
|
+
<div key={f.id} style={{
|
|
874
|
+
padding: '8px 12px', marginBottom: 4, borderRadius: 8,
|
|
875
|
+
background: 'var(--bg-section)', borderLeft: '3px solid var(--red)',
|
|
876
|
+
fontSize: 12,
|
|
877
|
+
}}>
|
|
878
|
+
<div style={{ fontWeight: 500, color: 'var(--text)', marginBottom: 2 }}>{f.userRequest.slice(0, 80)}</div>
|
|
879
|
+
<div style={{ color: 'var(--red)', fontSize: 11 }}>→ {f.correction.slice(0, 80)}</div>
|
|
880
|
+
</div>
|
|
881
|
+
))}
|
|
882
|
+
</div>
|
|
883
|
+
</div>
|
|
884
|
+
)
|
|
885
|
+
})}
|
|
886
|
+
</div>
|
|
887
|
+
)
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
891
|
+
// SUB-VIEW: Co-Evolution
|
|
892
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
893
|
+
|
|
894
|
+
function CoEvolutionView({
|
|
895
|
+
generalized, evolved, original, edges, evolutionBySkill, projects,
|
|
896
|
+
}: {
|
|
897
|
+
generalized: SkillNode[]; evolved: SkillNode[]; original: SkillNode[]
|
|
898
|
+
edges: GraphEdge[]; evolutionBySkill: Record<string, EvolutionEntry[]>; projects: ProjectData[]
|
|
899
|
+
}) {
|
|
900
|
+
const allNodes = [...generalized, ...evolved, ...original]
|
|
901
|
+
|
|
902
|
+
// Inheritance chains: generalized → children
|
|
903
|
+
const inheritanceChains = generalized.map(g => {
|
|
904
|
+
const children = edges.filter(e => e.type === 'inherits' && e.from === g.id).map(e => allNodes.find(n => n.id === e.to)).filter(Boolean)
|
|
905
|
+
return { parent: g, children: children as SkillNode[] }
|
|
906
|
+
})
|
|
907
|
+
|
|
908
|
+
// Enhancement clusters
|
|
909
|
+
const enhanceEdges = edges.filter(e => e.type === 'enhances')
|
|
910
|
+
const enhancePairs = enhanceEdges.map(e => ({
|
|
911
|
+
from: allNodes.find(n => n.id === e.from),
|
|
912
|
+
to: allNodes.find(n => n.id === e.to),
|
|
913
|
+
strength: e.strength,
|
|
914
|
+
})).filter(p => p.from && p.to) as { from: SkillNode; to: SkillNode; strength: number }[]
|
|
915
|
+
|
|
916
|
+
// Conflict edges
|
|
917
|
+
const conflictEdges = edges.filter(e => e.type === 'conflicts')
|
|
918
|
+
const conflictPairs = conflictEdges.map(e => ({
|
|
919
|
+
from: allNodes.find(n => n.id === e.from),
|
|
920
|
+
to: allNodes.find(n => n.id === e.to),
|
|
921
|
+
strength: e.strength,
|
|
922
|
+
})).filter(p => p.from && p.to) as { from: SkillNode; to: SkillNode; strength: number }[]
|
|
923
|
+
|
|
924
|
+
// Cross-project knowledge: which skills are shared across projects
|
|
925
|
+
const sharedSkills: Record<string, string[]> = {}
|
|
926
|
+
for (const proj of projects) {
|
|
927
|
+
for (const s of proj.skills) {
|
|
928
|
+
if (!sharedSkills[s]) sharedSkills[s] = []
|
|
929
|
+
sharedSkills[s].push(proj.name)
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
const crossProjectSkills = Object.entries(sharedSkills).filter(([, projs]) => projs.length > 1)
|
|
933
|
+
|
|
934
|
+
return (
|
|
935
|
+
<div style={{ display: 'grid', gap: 20 }}>
|
|
936
|
+
{/* Knowledge Flow: Generalize ↑ / Specialize ↓ */}
|
|
937
|
+
{inheritanceChains.length > 0 && (
|
|
938
|
+
<div className="card">
|
|
939
|
+
<div className="card-body">
|
|
940
|
+
<div className="card-header-label">Knowledge Flow — Generalize ↑ Specialize ↓</div>
|
|
941
|
+
<div style={{ display: 'grid', gap: 12 }}>
|
|
942
|
+
{inheritanceChains.map(chain => (
|
|
943
|
+
<div key={chain.parent.id} style={{ padding: '14px 16px', background: 'var(--bg-section)', borderRadius: 10 }}>
|
|
944
|
+
{/* Parent */}
|
|
945
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 10 }}>
|
|
946
|
+
<div style={{
|
|
947
|
+
width: 8, height: 8, borderRadius: '50%',
|
|
948
|
+
background: 'var(--purple)', flexShrink: 0,
|
|
949
|
+
}} />
|
|
950
|
+
<span style={{ fontWeight: 700, fontSize: 14 }}>{chain.parent.name}</span>
|
|
951
|
+
<span style={{ fontSize: 22, fontWeight: 800, color: scoreColor(chain.parent.score), marginLeft: 'auto' }}>
|
|
952
|
+
{(chain.parent.score * 100).toFixed(0)}
|
|
953
|
+
</span>
|
|
954
|
+
</div>
|
|
955
|
+
|
|
956
|
+
{/* Children */}
|
|
957
|
+
{chain.children.length > 0 && (
|
|
958
|
+
<div style={{ paddingLeft: 18, borderLeft: '2px solid var(--purple-border)' }}>
|
|
959
|
+
{chain.children.map(child => (
|
|
960
|
+
<div key={child.id} style={{
|
|
961
|
+
display: 'flex', alignItems: 'center', gap: 10,
|
|
962
|
+
padding: '6px 0',
|
|
963
|
+
}}>
|
|
964
|
+
<span style={{ fontSize: 10, color: 'var(--purple)' }}>↳</span>
|
|
965
|
+
<span style={{ fontWeight: 500, fontSize: 13 }}>{child.name}</span>
|
|
966
|
+
<span className="badge badge-green" style={{ fontSize: 9 }}>gen {child.generation}</span>
|
|
967
|
+
<span style={{ marginLeft: 'auto', fontSize: 13, fontWeight: 700, color: scoreColor(child.score) }}>
|
|
968
|
+
{(child.score * 100).toFixed(0)}
|
|
969
|
+
</span>
|
|
970
|
+
</div>
|
|
971
|
+
))}
|
|
972
|
+
</div>
|
|
973
|
+
)}
|
|
974
|
+
</div>
|
|
975
|
+
))}
|
|
976
|
+
</div>
|
|
977
|
+
</div>
|
|
978
|
+
</div>
|
|
979
|
+
)}
|
|
980
|
+
|
|
981
|
+
{/* Enhancement Pairs */}
|
|
982
|
+
<div className="grid-2">
|
|
983
|
+
<div className="card">
|
|
984
|
+
<div className="card-body">
|
|
985
|
+
<div className="card-header-label">Synergies — Skills that enhance each other</div>
|
|
986
|
+
{enhancePairs.length === 0 ? (
|
|
987
|
+
<div style={{ color: 'var(--text-dim)', fontSize: 13, textAlign: 'center', padding: 20 }}>No enhancement relationships detected</div>
|
|
988
|
+
) : (
|
|
989
|
+
enhancePairs.map((pair, i) => (
|
|
990
|
+
<div key={i} style={{
|
|
991
|
+
display: 'flex', alignItems: 'center', gap: 10,
|
|
992
|
+
padding: '8px 0', borderBottom: i < enhancePairs.length - 1 ? '1px solid var(--border)' : 'none',
|
|
993
|
+
}}>
|
|
994
|
+
<span style={{ fontWeight: 500, fontSize: 13 }}>{pair.from.name}</span>
|
|
995
|
+
<span style={{ color: 'var(--green)', fontSize: 12 }}>↔</span>
|
|
996
|
+
<span style={{ fontWeight: 500, fontSize: 13 }}>{pair.to.name}</span>
|
|
997
|
+
<span style={{ marginLeft: 'auto', fontSize: 11, color: 'var(--text-dim)' }}>
|
|
998
|
+
strength: {(pair.strength * 100).toFixed(0)}%
|
|
999
|
+
</span>
|
|
1000
|
+
</div>
|
|
1001
|
+
))
|
|
1002
|
+
)}
|
|
1003
|
+
</div>
|
|
1004
|
+
</div>
|
|
1005
|
+
|
|
1006
|
+
<div className="card">
|
|
1007
|
+
<div className="card-body">
|
|
1008
|
+
<div className="card-header-label">Conflicts — Skills with tension</div>
|
|
1009
|
+
{conflictPairs.length === 0 ? (
|
|
1010
|
+
<div style={{ color: 'var(--text-dim)', fontSize: 13, textAlign: 'center', padding: 20 }}>No conflicts detected</div>
|
|
1011
|
+
) : (
|
|
1012
|
+
conflictPairs.map((pair, i) => (
|
|
1013
|
+
<div key={i} style={{
|
|
1014
|
+
display: 'flex', alignItems: 'center', gap: 10,
|
|
1015
|
+
padding: '8px 0', borderBottom: i < conflictPairs.length - 1 ? '1px solid var(--border)' : 'none',
|
|
1016
|
+
}}>
|
|
1017
|
+
<span style={{ fontWeight: 500, fontSize: 13 }}>{pair.from.name}</span>
|
|
1018
|
+
<span style={{ color: 'var(--red)', fontSize: 12 }}>⚡</span>
|
|
1019
|
+
<span style={{ fontWeight: 500, fontSize: 13 }}>{pair.to.name}</span>
|
|
1020
|
+
<span style={{ marginLeft: 'auto', fontSize: 11, color: 'var(--text-dim)' }}>
|
|
1021
|
+
strength: {(pair.strength * 100).toFixed(0)}%
|
|
1022
|
+
</span>
|
|
1023
|
+
</div>
|
|
1024
|
+
))
|
|
1025
|
+
)}
|
|
1026
|
+
</div>
|
|
1027
|
+
</div>
|
|
1028
|
+
</div>
|
|
1029
|
+
|
|
1030
|
+
{/* Cross-Project Knowledge Transfer */}
|
|
1031
|
+
{crossProjectSkills.length > 0 && (
|
|
1032
|
+
<div className="card">
|
|
1033
|
+
<div className="card-body">
|
|
1034
|
+
<div className="card-header-label">Cross-Project Knowledge Transfer</div>
|
|
1035
|
+
<div style={{ display: 'grid', gap: 8 }}>
|
|
1036
|
+
{crossProjectSkills.map(([skillId, projs]) => {
|
|
1037
|
+
const node = allNodes.find(n => n.id === skillId)
|
|
1038
|
+
return (
|
|
1039
|
+
<div key={skillId} style={{
|
|
1040
|
+
display: 'flex', alignItems: 'center', gap: 12,
|
|
1041
|
+
padding: '10px 14px', background: 'var(--bg-section)', borderRadius: 8,
|
|
1042
|
+
}}>
|
|
1043
|
+
<span style={{ fontWeight: 600, fontSize: 13 }}>{node?.name ?? skillId}</span>
|
|
1044
|
+
<div style={{ display: 'flex', gap: 4, marginLeft: 'auto' }}>
|
|
1045
|
+
{projs.map(p => <span key={p} className="badge badge-yellow">{p}</span>)}
|
|
1046
|
+
</div>
|
|
1047
|
+
</div>
|
|
1048
|
+
)
|
|
1049
|
+
})}
|
|
1050
|
+
</div>
|
|
1051
|
+
</div>
|
|
1052
|
+
</div>
|
|
1053
|
+
)}
|
|
1054
|
+
|
|
1055
|
+
{/* Evolution Activity by Skill */}
|
|
1056
|
+
<div className="card">
|
|
1057
|
+
<div className="card-body">
|
|
1058
|
+
<div className="card-header-label">Evolution Activity</div>
|
|
1059
|
+
<div style={{ display: 'grid', gap: 6 }}>
|
|
1060
|
+
{allNodes.sort((a, b) => (evolutionBySkill[b.id]?.length ?? 0) - (evolutionBySkill[a.id]?.length ?? 0)).map(node => {
|
|
1061
|
+
const evo = evolutionBySkill[node.id] ?? []
|
|
1062
|
+
if (evo.length === 0) return null
|
|
1063
|
+
const accepted = evo.filter(e => e.outcome === 'accepted').length
|
|
1064
|
+
return (
|
|
1065
|
+
<div key={node.id} style={{
|
|
1066
|
+
display: 'flex', alignItems: 'center', gap: 12,
|
|
1067
|
+
padding: '8px 14px', background: 'var(--bg-section)', borderRadius: 8,
|
|
1068
|
+
}}>
|
|
1069
|
+
<span style={{ fontWeight: 500, fontSize: 13, minWidth: 160 }}>{node.name}</span>
|
|
1070
|
+
<div style={{ flex: 1, display: 'flex', gap: 3 }}>
|
|
1071
|
+
{evo.map((e, i) => (
|
|
1072
|
+
<div key={i} style={{
|
|
1073
|
+
width: 16, height: 16, borderRadius: 4,
|
|
1074
|
+
background: e.outcome === 'accepted' ? 'var(--green-light)' : 'var(--red-light)',
|
|
1075
|
+
border: `1px solid ${e.outcome === 'accepted' ? 'var(--green-border)' : 'var(--red-border)'}`,
|
|
1076
|
+
}} title={`${e.action}: ${e.outcome}`} />
|
|
1077
|
+
))}
|
|
1078
|
+
</div>
|
|
1079
|
+
<span style={{ fontSize: 11, color: 'var(--text-dim)', whiteSpace: 'nowrap' }}>
|
|
1080
|
+
{accepted}/{evo.length} accepted
|
|
1081
|
+
</span>
|
|
1082
|
+
</div>
|
|
1083
|
+
)
|
|
1084
|
+
})}
|
|
1085
|
+
</div>
|
|
1086
|
+
</div>
|
|
1087
|
+
</div>
|
|
1088
|
+
</div>
|
|
1089
|
+
)
|
|
1090
|
+
}
|