helixevo 0.2.38 → 0.2.40

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.
@@ -242,19 +242,24 @@ export default function NetworkClient({
242
242
  <div>
243
243
  {/* Page Header */}
244
244
  <div className="page-header">
245
- <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
245
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: 18, flexWrap: 'wrap' }}>
246
246
  <div>
247
247
  <h1 className="page-title">Skill Network</h1>
248
248
  <p className="page-desc">
249
- {stats.nodes} skills · {stats.edges} relationships · {stats.clusters} clusters
250
- {stats.generalized > 0 && ` · ${stats.generalized} generalized`}
249
+ Visualize how skills generalize, specialize, and interact across the living HelixEvo network.
251
250
  </p>
251
+ <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', marginTop: 16 }}>
252
+ <span className="hero-chip hero-chip-purple">{stats.nodes} skills</span>
253
+ <span className="hero-chip hero-chip-blue">{stats.edges} relationships</span>
254
+ <span className="hero-chip hero-chip-neutral">{stats.clusters} clusters</span>
255
+ {stats.generalized > 0 ? <span className="hero-chip hero-chip-green">{stats.generalized} generalized</span> : null}
256
+ </div>
252
257
  </div>
253
258
  <button onClick={() => setCreating(!creating)} style={{
254
- background: creating ? 'var(--bg-section)' : 'linear-gradient(135deg, var(--purple), #4f46e5)',
259
+ background: creating ? 'rgba(97,93,86,0.08)' : 'linear-gradient(135deg, var(--purple), #4f46e5)',
255
260
  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)',
261
+ border: creating ? '1px solid var(--border)' : 'none', borderRadius: 999, padding: '11px 18px', fontSize: 13, fontWeight: 700,
262
+ cursor: 'pointer', display: 'flex', alignItems: 'center', gap: 8, fontFamily: 'var(--font)', boxShadow: creating ? 'none' : 'var(--shadow-md)',
258
263
  }}>{creating ? '✕ Cancel' : '+ New Skill'}</button>
259
264
  </div>
260
265
  </div>
@@ -314,8 +319,12 @@ export default function NetworkClient({
314
319
  key={sv.key}
315
320
  className={`tab-item ${view === sv.key ? 'active' : ''}`}
316
321
  onClick={() => { setView(sv.key) }}
322
+ style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start', gap: 2, minWidth: 0 }}
317
323
  >
318
- {sv.label}
324
+ <span style={{ fontSize: 12.5, fontWeight: 700 }}>{sv.label}</span>
325
+ <span style={{ fontSize: 10.5, color: view === sv.key ? 'rgba(62,53,46,0.74)' : 'var(--text-muted)', fontWeight: 500, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', maxWidth: '100%' }}>
326
+ {sv.desc}
327
+ </span>
319
328
  </button>
320
329
  ))}
321
330
  </div>
@@ -346,256 +355,82 @@ export default function NetworkClient({
346
355
 
347
356
  {/* Detail Panel */}
348
357
  {selectedSkill && selectedNode && (
349
- <div className="detail-panel">
350
- <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 16 }}>
351
- <div>
352
- <div style={{ fontSize: 17, fontWeight: 700, marginBottom: 4 }}>{selectedNode.name}</div>
353
- <div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
354
- <span className="badge badge-gray">{selectedNode.layer}</span>
355
- {selectedNode.generation > 0 && <span className="badge badge-green">gen {selectedNode.generation}</span>}
356
- {selectedNode.tags.map(t => <span key={t} className="badge badge-gray">{t}</span>)}
357
- </div>
358
- </div>
359
- <button onClick={() => setSelectedSkill(null)} style={{
360
- background: 'var(--bg-section)', border: 'none', borderRadius: 6, width: 28, height: 28,
361
- cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center',
362
- color: 'var(--text-dim)', fontSize: 14,
363
- }}>✕</button>
364
- </div>
365
-
366
- {/* Score */}
367
- <div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 20, padding: '12px 14px', background: 'var(--bg-section)', borderRadius: 10 }}>
368
- <div style={{ fontSize: 32, fontWeight: 800, color: scoreColor(selectedNode.score), lineHeight: 1 }}>
369
- {(selectedNode.score * 100).toFixed(0)}
370
- </div>
371
- <div style={{ flex: 1 }}>
372
- <div style={{ fontSize: 11, color: 'var(--text-dim)', marginBottom: 4 }}>Quality Score</div>
373
- <div className="score-track" style={{ height: 6 }}>
374
- <div className="score-fill" style={{
375
- width: `${selectedNode.score * 100}%`,
376
- background: `linear-gradient(90deg, var(--blue), ${scoreColor(selectedNode.score)})`,
377
- }} />
378
- </div>
379
- </div>
380
- </div>
381
-
382
- {/* Connections */}
383
- {selectedEdges && (selectedEdges.inheritsFrom.length > 0 || selectedEdges.children.length > 0 || selectedEdges.enhances.length > 0 || selectedEdges.conflicts.length > 0) && (
384
- <div style={{ marginBottom: 20 }}>
385
- <div className="section-label">Connections</div>
386
- <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
387
- {selectedEdges.inheritsFrom.length > 0 && (
388
- <div style={{ fontSize: 12 }}>
389
- <span style={{ color: 'var(--text-dim)' }}>Inherits from: </span>
390
- {selectedEdges.inheritsFrom.map(e => <span key={e.from} className="badge badge-purple" style={{ marginRight: 3, cursor: 'pointer' }} onClick={() => setSelectedSkill(e.from)}>{e.from}</span>)}
391
- </div>
392
- )}
393
- {selectedEdges.children.length > 0 && (
394
- <div style={{ fontSize: 12 }}>
395
- <span style={{ color: 'var(--text-dim)' }}>Children: </span>
396
- {selectedEdges.children.map(e => <span key={e.to} className="badge badge-blue" style={{ marginRight: 3, cursor: 'pointer' }} onClick={() => setSelectedSkill(e.to)}>{e.to}</span>)}
397
- </div>
398
- )}
399
- {selectedEdges.enhances.length > 0 && (
400
- <div style={{ fontSize: 12 }}>
401
- <span style={{ color: 'var(--text-dim)' }}>Enhances: </span>
402
- {[...new Set(selectedEdges.enhances.map(e => e.from === selectedSkill ? e.to : e.from))].map(id =>
403
- <span key={id} className="badge badge-green" style={{ marginRight: 3, cursor: 'pointer' }} onClick={() => setSelectedSkill(id)}>{id}</span>
404
- )}
405
- </div>
406
- )}
407
- {selectedEdges.conflicts.length > 0 && (
408
- <div style={{ fontSize: 12 }}>
409
- <span style={{ color: 'var(--text-dim)' }}>Conflicts: </span>
410
- {[...new Set(selectedEdges.conflicts.map(e => e.from === selectedSkill ? e.to : e.from))].map(id =>
411
- <span key={id} className="badge badge-red" style={{ marginRight: 3, cursor: 'pointer' }} onClick={() => setSelectedSkill(id)}>{id}</span>
412
- )}
413
- </div>
414
- )}
415
- </div>
416
- </div>
417
- )}
418
-
419
- {/* Evolution History */}
420
- {selectedEvo.length > 0 && (
421
- <div style={{ marginBottom: 20 }}>
422
- <div className="section-label">Evolution History</div>
423
- {selectedEvo.map((e, i) => (
424
- <div key={i} style={{
425
- padding: '8px 12px', marginBottom: 6, borderRadius: 8,
426
- background: 'var(--bg-section)',
427
- borderLeft: `3px solid ${e.outcome === 'accepted' ? 'var(--green)' : 'var(--red)'}`,
428
- }}>
429
- <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 10, marginBottom: 3 }}>
430
- <span className={`badge ${e.outcome === 'accepted' ? 'badge-green' : 'badge-red'}`}>{e.outcome}</span>
431
- <span style={{ color: 'var(--text-muted)' }}>{new Date(e.timestamp).toLocaleDateString()}</span>
432
- </div>
433
- <div style={{ fontSize: 12, color: 'var(--text-secondary)', lineHeight: 1.5, marginBottom: 4 }}>{e.description.slice(0, 150)}</div>
434
- <div style={{ display: 'flex', gap: 8, fontSize: 10 }}>
435
- <span className="score-pill">T:{e.task}</span>
436
- <span className="score-pill">A:{e.align}</span>
437
- <span className="score-pill">S:{e.sideEffect}</span>
438
- </div>
439
- </div>
440
- ))}
441
- </div>
442
- )}
443
-
444
- {/* ─── Cross-Links ─── */}
445
- <div style={{ marginBottom: 16 }}>
446
- <div className="section-label">Navigate</div>
447
- <div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
448
- {view !== 'graph' && (
449
- <button onClick={() => setView('graph')} style={actionBtnStyle('var(--blue)')}>
450
- ◎ View in Graph
451
- </button>
452
- )}
453
- {view !== 'general' && (
454
- <button onClick={() => setView('general')} style={actionBtnStyle()}>
455
- ▤ View in Skills
456
- </button>
457
- )}
458
- {view !== 'coevolution' && (
459
- <button onClick={() => setView('coevolution')} style={actionBtnStyle()}>
460
- ↕ Co-Evolution
461
- </button>
462
- )}
463
- </div>
464
- </div>
465
-
466
- {/* ─── Management Actions ─── */}
467
- <div style={{ marginBottom: 20 }}>
468
- <div className="section-label">Actions</div>
469
- <div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
470
- <button onClick={() => { setEditing(!editing); setEditContent(skillContents[selectedSkill!] ?? ''); setAdaptation(null) }}
471
- style={actionBtnStyle(editing ? 'var(--purple)' : undefined)}>
472
- {editing ? '✕ Cancel' : '✎ Edit'}
473
- </button>
474
- <button onClick={async () => {
475
- if (!selectedNode) return
476
- const layers = ['project', 'domain', 'system'] as const
477
- const currentIdx = layers.indexOf(selectedNode.layer as typeof layers[number])
478
- const nextLayer = layers[Math.min(currentIdx + 1, 2)]
479
- if (nextLayer === selectedNode.layer) return
480
- setActionLoading(true)
481
- const res = await fetch(`/api/skills`, { method: 'PUT', headers: { 'Content-Type': 'application/json' },
482
- body: JSON.stringify({ slug: selectedSkill, layer: nextLayer }) })
483
- const data = await res.json()
484
- setAdaptation(data.adaptation)
485
- setActionLoading(false)
486
- }} style={actionBtnStyle('var(--blue)')}>
487
- ↑ Promote
488
- </button>
489
- <button onClick={async () => {
490
- if (!selectedNode) return
491
- const layers = ['project', 'domain', 'system'] as const
492
- const currentIdx = layers.indexOf(selectedNode.layer as typeof layers[number])
493
- const nextLayer = layers[Math.max(currentIdx - 1, 0)]
494
- if (nextLayer === selectedNode.layer) return
495
- setActionLoading(true)
496
- const res = await fetch(`/api/skills`, { method: 'PUT', headers: { 'Content-Type': 'application/json' },
497
- body: JSON.stringify({ slug: selectedSkill, layer: nextLayer }) })
498
- const data = await res.json()
499
- setAdaptation(data.adaptation)
500
- setActionLoading(false)
501
- }} style={actionBtnStyle('var(--yellow)')}>
502
- ↓ Demote
503
- </button>
504
- <button onClick={async () => {
505
- if (!confirm(`Delete "${selectedNode?.name}"? This cannot be undone.`)) return
506
- setActionLoading(true)
507
- const res = await fetch(`/api/skills?slug=${selectedSkill}`, { method: 'DELETE' })
508
- const data = await res.json()
509
- setAdaptation(data.adaptation)
510
- setActionLoading(false)
511
- if (data.success) setTimeout(() => window.location.reload(), 1500)
512
- }} style={actionBtnStyle('var(--red)')}>
513
- ✕ Delete
514
- </button>
515
- </div>
516
- </div>
517
-
518
- {/* ─── Edit Mode ─── */}
519
- {editing && (
520
- <div style={{ marginBottom: 20 }}>
521
- <div className="section-label">Edit Skill Content</div>
522
- <textarea
523
- value={editContent}
524
- onChange={e => setEditContent(e.target.value)}
525
- style={{
526
- width: '100%', height: 250, fontFamily: 'var(--font-mono)', fontSize: 11,
527
- background: '#1e1e2e', color: '#cdd6f4', border: '1px solid var(--border)',
528
- borderRadius: 8, padding: 12, resize: 'vertical', lineHeight: 1.6,
529
- }}
530
- />
531
- <button onClick={async () => {
532
- setActionLoading(true)
533
- const res = await fetch(`/api/skills`, { method: 'PUT', headers: { 'Content-Type': 'application/json' },
534
- 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}` }) })
535
- const data = await res.json()
536
- setAdaptation(data.adaptation)
537
- setEditing(false)
538
- setActionLoading(false)
539
- setTimeout(() => window.location.reload(), 1000)
540
- }} disabled={actionLoading}
541
- style={{ ...actionBtnStyle('var(--green)'), width: '100%', marginTop: 8 }}>
542
- {actionLoading ? 'Saving...' : '✓ Save Changes'}
543
- </button>
544
- </div>
545
- )}
546
-
547
- {/* ─── Network Adaptation Feedback ─── */}
548
- {adaptation && (
549
- <div style={{ marginBottom: 20 }}>
550
- <div className="section-label">
551
- Network Adaptation
552
- <span style={{
553
- marginLeft: 8, fontSize: 10, fontWeight: 600, padding: '2px 8px', borderRadius: 6,
554
- background: adaptation.status === 'ok' ? 'var(--green-light)' : adaptation.status === 'warning' ? 'var(--yellow-light)' : 'var(--red-light)',
555
- color: adaptation.status === 'ok' ? 'var(--green)' : adaptation.status === 'warning' ? 'var(--yellow)' : 'var(--red)',
556
- textTransform: 'none', letterSpacing: 0,
557
- }}>{adaptation.status}</span>
558
- </div>
559
- {adaptation.messages.map((m, i) => (
560
- <div key={i} style={{
561
- padding: '8px 12px', marginBottom: 4, borderRadius: 6, fontSize: 12, lineHeight: 1.5,
562
- background: m.type === 'warning' ? 'var(--yellow-light)' : m.type === 'action' ? 'var(--red-light)' : 'var(--bg-section)',
563
- color: m.type === 'warning' ? 'var(--yellow)' : m.type === 'action' ? 'var(--red)' : 'var(--text-secondary)',
564
- border: `1px solid ${m.type === 'warning' ? 'var(--yellow-border)' : m.type === 'action' ? 'var(--red-border)' : 'var(--border)'}`,
565
- }}>{m.type === 'warning' ? '⚠ ' : m.type === 'action' ? '⚡ ' : 'ℹ '}{m.text}</div>
566
- ))}
567
- {adaptation.suggestions.length > 0 && (
568
- <div style={{ marginTop: 8 }}>
569
- <div style={{ fontSize: 10, fontWeight: 600, color: 'var(--text-muted)', marginBottom: 4 }}>SUGGESTIONS</div>
570
- {adaptation.suggestions.map((s, i) => (
571
- <div key={i} style={{
572
- padding: '6px 10px', fontSize: 11, color: 'var(--text-dim)', lineHeight: 1.5,
573
- borderLeft: '2px solid var(--purple)', marginBottom: 4, paddingLeft: 10,
574
- }}>
575
- <span className="badge badge-purple" style={{ marginRight: 6 }}>{s.type}</span>
576
- {s.description}
577
- </div>
578
- ))}
579
- </div>
580
- )}
581
- </div>
582
- )}
583
-
584
- {/* Skill Content (read-only when not editing) */}
585
- {selectedContent && !editing && (
586
- <div>
587
- <div className="section-label">Skill Content</div>
588
- <pre style={{
589
- background: 'var(--bg-section)', borderRadius: 8, padding: 12,
590
- whiteSpace: 'pre-wrap', wordBreak: 'break-word',
591
- color: 'var(--text-secondary)', lineHeight: 1.6, maxHeight: 300, overflow: 'auto',
592
- fontSize: 11, border: '1px solid var(--border)',
593
- }}>
594
- {selectedContent.slice(0, 1500)}
595
- </pre>
596
- </div>
597
- )}
598
- </div>
358
+ <InspectorPanel
359
+ selectedSkill={selectedSkill}
360
+ selectedNode={selectedNode}
361
+ selectedEdges={selectedEdges}
362
+ selectedEvo={selectedEvo}
363
+ selectedContent={selectedContent}
364
+ view={view}
365
+ editing={editing}
366
+ editContent={editContent}
367
+ adaptation={adaptation}
368
+ actionLoading={actionLoading}
369
+ onClose={() => setSelectedSkill(null)}
370
+ onSelectSkill={setSelectedSkill}
371
+ onViewChange={setView}
372
+ onToggleEdit={() => {
373
+ setEditing(!editing)
374
+ setEditContent(skillContents[selectedSkill] ?? '')
375
+ setAdaptation(null)
376
+ }}
377
+ onEditContentChange={setEditContent}
378
+ onPromote={async () => {
379
+ const layers = ['project', 'domain', 'system'] as const
380
+ const currentIdx = layers.indexOf(selectedNode.layer as typeof layers[number])
381
+ const nextLayer = layers[Math.min(currentIdx + 1, 2)]
382
+ if (nextLayer === selectedNode.layer) return
383
+ setActionLoading(true)
384
+ const res = await fetch(`/api/skills`, {
385
+ method: 'PUT',
386
+ headers: { 'Content-Type': 'application/json' },
387
+ body: JSON.stringify({ slug: selectedSkill, layer: nextLayer }),
388
+ })
389
+ const data = await res.json()
390
+ setAdaptation(data.adaptation)
391
+ setActionLoading(false)
392
+ }}
393
+ onDemote={async () => {
394
+ const layers = ['project', 'domain', 'system'] as const
395
+ const currentIdx = layers.indexOf(selectedNode.layer as typeof layers[number])
396
+ const nextLayer = layers[Math.max(currentIdx - 1, 0)]
397
+ if (nextLayer === selectedNode.layer) return
398
+ setActionLoading(true)
399
+ const res = await fetch(`/api/skills`, {
400
+ method: 'PUT',
401
+ headers: { 'Content-Type': 'application/json' },
402
+ body: JSON.stringify({ slug: selectedSkill, layer: nextLayer }),
403
+ })
404
+ const data = await res.json()
405
+ setAdaptation(data.adaptation)
406
+ setActionLoading(false)
407
+ }}
408
+ onDelete={async () => {
409
+ if (!confirm(`Delete "${selectedNode.name}"? This cannot be undone.`)) return
410
+ setActionLoading(true)
411
+ const res = await fetch(`/api/skills?slug=${selectedSkill}`, { method: 'DELETE' })
412
+ const data = await res.json()
413
+ setAdaptation(data.adaptation)
414
+ setActionLoading(false)
415
+ if (data.success) setTimeout(() => window.location.reload(), 1500)
416
+ }}
417
+ onSave={async () => {
418
+ setActionLoading(true)
419
+ const res = await fetch(`/api/skills`, {
420
+ method: 'PUT',
421
+ headers: { 'Content-Type': 'application/json' },
422
+ body: JSON.stringify({
423
+ slug: selectedSkill,
424
+ content: `---\n${Object.entries({ name: selectedNode.name, description: '', layer: selectedNode.layer }).map(([k, v]) => `${k}: ${v}`).join('\n')}\n---\n\n${editContent}`,
425
+ }),
426
+ })
427
+ const data = await res.json()
428
+ setAdaptation(data.adaptation)
429
+ setEditing(false)
430
+ setActionLoading(false)
431
+ setTimeout(() => window.location.reload(), 1000)
432
+ }}
433
+ />
599
434
  )}
600
435
  </div>
601
436
  </div>
@@ -608,7 +443,44 @@ export default function NetworkClient({
608
443
 
609
444
  function GraphView({ flowNodes, flowEdges, stats, onNodeClick }: { flowNodes: Node[]; flowEdges: Edge[]; stats: Props['stats']; onNodeClick: (id: string) => void }) {
610
445
  return (
611
- <div style={{ position: 'relative', height: 'calc(100vh - 200px)', width: '100%', borderRadius: 12, overflow: 'hidden', border: '1px solid var(--border)', background: 'var(--bg)' }}>
446
+ <div style={{
447
+ position: 'relative',
448
+ height: 'calc(100vh - 200px)',
449
+ width: '100%',
450
+ borderRadius: 22,
451
+ overflow: 'hidden',
452
+ border: '1px solid var(--border)',
453
+ background: 'radial-gradient(circle at top right, rgba(107,73,223,0.08), transparent 24%), radial-gradient(circle at left center, rgba(18,121,109,0.08), transparent 22%), linear-gradient(180deg, rgba(255,255,255,0.92), rgba(247,244,239,0.96))',
454
+ boxShadow: 'var(--shadow-lg)',
455
+ }}>
456
+ <div style={{ position: 'absolute', top: 14, left: 16, zIndex: 8, display: 'flex', gap: 8, flexWrap: 'wrap' }}>
457
+ <span className="hero-chip hero-chip-purple">Generalized</span>
458
+ <span className="hero-chip hero-chip-green">Evolved</span>
459
+ <span className="hero-chip hero-chip-neutral">Original</span>
460
+ <span className="hero-chip hero-chip-yellow">Projects</span>
461
+ </div>
462
+
463
+ <div style={{
464
+ position: 'absolute',
465
+ top: 14,
466
+ right: 16,
467
+ zIndex: 8,
468
+ padding: '10px 12px',
469
+ borderRadius: 18,
470
+ background: 'rgba(255,255,255,0.88)',
471
+ border: '1px solid var(--border)',
472
+ boxShadow: 'var(--shadow-sm)',
473
+ display: 'grid',
474
+ gap: 4,
475
+ minWidth: 180,
476
+ }}>
477
+ <div style={{ fontSize: 10, fontWeight: 700, letterSpacing: 0.5, textTransform: 'uppercase', color: 'var(--text-muted)' }}>Live network</div>
478
+ <div style={{ fontSize: 12.5, fontWeight: 700, color: 'var(--text)' }}>{stats.nodes} active skill nodes</div>
479
+ <div style={{ fontSize: 11, color: 'var(--text-dim)', lineHeight: 1.5 }}>
480
+ {stats.edges} relationships spanning {stats.clusters} clusters{stats.generalized > 0 ? ` • ${stats.generalized} generalized anchors` : ''}
481
+ </div>
482
+ </div>
483
+
612
484
  <ReactFlow
613
485
  nodes={flowNodes}
614
486
  edges={flowEdges}
@@ -618,26 +490,338 @@ function GraphView({ flowNodes, flowEdges, stats, onNodeClick }: { flowNodes: No
618
490
  fitView
619
491
  fitViewOptions={{ padding: 0.2 }}
620
492
  proOptions={{ hideAttribution: true }}
621
- style={{ background: 'var(--bg)' }}
493
+ style={{ background: 'transparent' }}
622
494
  minZoom={0.3}
623
495
  maxZoom={1.5}
624
496
  >
625
- <Background color="#d5d2ca" gap={20} size={1} />
497
+ <Background color="rgba(112,104,94,0.22)" gap={20} size={1} />
626
498
  <Controls position="bottom-left" />
627
499
  </ReactFlow>
628
500
 
629
501
  {/* Legend */}
630
502
  <div style={{
631
- position: 'absolute', bottom: 12, right: 12, zIndex: 10,
632
- background: 'var(--bg-card)', border: '1px solid var(--border)', borderRadius: 8,
633
- padding: '8px 14px', display: 'flex', gap: 14, fontSize: 10, color: 'var(--text-dim)',
634
- boxShadow: 'var(--shadow-sm)',
503
+ position: 'absolute', bottom: 14, right: 14, zIndex: 10,
504
+ background: 'rgba(255,255,255,0.9)', border: '1px solid var(--border)', borderRadius: 18,
505
+ padding: '10px 14px', display: 'flex', gap: 14, fontSize: 10.5, color: 'var(--text-dim)',
506
+ boxShadow: 'var(--shadow-md)', flexWrap: 'wrap', maxWidth: 'min(720px, calc(100% - 28px))',
635
507
  }}>
636
- <span><span style={{ display: 'inline-block', width: 12, height: 2, background: '#818cf8', marginRight: 4, verticalAlign: 'middle' }} /> inherits</span>
637
- <span><span style={{ display: 'inline-block', width: 12, height: 2, background: '#22c55e', marginRight: 4, verticalAlign: 'middle' }} /> enhances</span>
638
- <span><span style={{ display: 'inline-block', width: 12, height: 2, background: '#ef4444', marginRight: 4, verticalAlign: 'middle', borderTop: '1px dashed #ef4444' }} /> conflicts</span>
639
- <span><span style={{ display: 'inline-block', width: 12, height: 2, background: '#fbbf24', marginRight: 4, verticalAlign: 'middle' }} /> project</span>
640
- <span style={{ color: '#b0b5c8' }}>Scroll to zoom · Drag to pan</span>
508
+ <span><span style={{ display: 'inline-block', width: 12, height: 2, background: '#818cf8', marginRight: 5, verticalAlign: 'middle' }} /> inherits</span>
509
+ <span><span style={{ display: 'inline-block', width: 12, height: 2, background: '#22c55e', marginRight: 5, verticalAlign: 'middle' }} /> enhances</span>
510
+ <span><span style={{ display: 'inline-block', width: 12, height: 2, background: '#ef4444', marginRight: 5, verticalAlign: 'middle', borderTop: '1px dashed #ef4444' }} /> conflicts</span>
511
+ <span><span style={{ display: 'inline-block', width: 12, height: 2, background: '#fbbf24', marginRight: 5, verticalAlign: 'middle' }} /> project link</span>
512
+ <span style={{ color: 'var(--text-muted)' }}>Scroll to zoom · Drag to pan · Click a node to inspect it</span>
513
+ </div>
514
+ </div>
515
+ )
516
+ }
517
+
518
+ function InspectorPanel({
519
+ selectedSkill,
520
+ selectedNode,
521
+ selectedEdges,
522
+ selectedEvo,
523
+ selectedContent,
524
+ view,
525
+ editing,
526
+ editContent,
527
+ adaptation,
528
+ actionLoading,
529
+ onClose,
530
+ onSelectSkill,
531
+ onViewChange,
532
+ onToggleEdit,
533
+ onEditContentChange,
534
+ onPromote,
535
+ onDemote,
536
+ onDelete,
537
+ onSave,
538
+ }: {
539
+ selectedSkill: string
540
+ selectedNode: SkillNode
541
+ selectedEdges: {
542
+ inheritsFrom: GraphEdge[]
543
+ children: GraphEdge[]
544
+ enhances: GraphEdge[]
545
+ conflicts: GraphEdge[]
546
+ } | null
547
+ selectedEvo: EvolutionEntry[]
548
+ selectedContent: string
549
+ view: SubView
550
+ editing: boolean
551
+ editContent: string
552
+ adaptation: { status: string; messages: { type: string; text: string }[]; suggestions: { type: string; description: string }[] } | null
553
+ actionLoading: boolean
554
+ onClose: () => void
555
+ onSelectSkill: (id: string) => void
556
+ onViewChange: (view: SubView) => void
557
+ onToggleEdit: () => void
558
+ onEditContentChange: (value: string) => void
559
+ onPromote: () => void | Promise<void>
560
+ onDemote: () => void | Promise<void>
561
+ onDelete: () => void | Promise<void>
562
+ onSave: () => void | Promise<void>
563
+ }) {
564
+ const acceptedCount = selectedEvo.filter(entry => entry.outcome === 'accepted').length
565
+ const rejectedCount = selectedEvo.filter(entry => entry.outcome !== 'accepted').length
566
+ const connectionGroups = selectedEdges ? [
567
+ { label: 'Inherits from', tone: 'purple', ids: selectedEdges.inheritsFrom.map(edge => edge.from) },
568
+ { label: 'Children', tone: 'blue', ids: selectedEdges.children.map(edge => edge.to) },
569
+ { label: 'Enhances', tone: 'green', ids: [...new Set(selectedEdges.enhances.map(edge => edge.from === selectedSkill ? edge.to : edge.from))] },
570
+ { label: 'Conflicts', tone: 'red', ids: [...new Set(selectedEdges.conflicts.map(edge => edge.from === selectedSkill ? edge.to : edge.from))] },
571
+ ].filter(group => group.ids.length > 0) : []
572
+
573
+ return (
574
+ <div className="detail-panel" style={{
575
+ background: 'linear-gradient(180deg, rgba(255,255,255,0.94), rgba(245,241,236,0.98))',
576
+ boxShadow: 'inset 1px 0 0 rgba(255,255,255,0.45)',
577
+ }}>
578
+ <div style={{
579
+ padding: '18px 18px 16px',
580
+ borderBottom: '1px solid var(--border)',
581
+ background: 'linear-gradient(180deg, rgba(255,255,255,0.82), rgba(255,255,255,0.2))',
582
+ }}>
583
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: 12, marginBottom: 14 }}>
584
+ <div>
585
+ <div style={{ fontSize: 10, fontWeight: 700, color: 'var(--text-muted)', letterSpacing: 0.6, textTransform: 'uppercase', marginBottom: 6 }}>
586
+ Skill inspector
587
+ </div>
588
+ <div style={{ fontSize: 21, fontWeight: 800, color: 'var(--text)', lineHeight: 1.2, marginBottom: 8 }}>{selectedNode.name}</div>
589
+ <div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
590
+ <span className="hero-chip hero-chip-neutral">{selectedNode.layer}</span>
591
+ {selectedNode.generation > 0 ? <span className="hero-chip hero-chip-green">gen {selectedNode.generation}</span> : <span className="hero-chip hero-chip-blue">original</span>}
592
+ {selectedNode.tags.slice(0, 4).map(tag => <span key={tag} className="hero-chip hero-chip-neutral">{tag}</span>)}
593
+ </div>
594
+ </div>
595
+ <button onClick={onClose} style={{
596
+ width: 34,
597
+ height: 34,
598
+ borderRadius: 999,
599
+ border: '1px solid var(--border)',
600
+ background: 'rgba(97,93,86,0.08)',
601
+ color: 'var(--text-dim)',
602
+ cursor: 'pointer',
603
+ fontSize: 16,
604
+ fontWeight: 700,
605
+ }}>×</button>
606
+ </div>
607
+
608
+ <div style={{
609
+ padding: '16px',
610
+ borderRadius: 20,
611
+ border: '1px solid var(--border)',
612
+ background: 'linear-gradient(135deg, rgba(255,255,255,0.92), rgba(245,241,236,0.96))',
613
+ boxShadow: 'var(--shadow-sm)',
614
+ }}>
615
+ <div style={{ display: 'flex', alignItems: 'flex-end', gap: 12, marginBottom: 10 }}>
616
+ <div style={{ fontSize: 40, fontWeight: 800, color: scoreColor(selectedNode.score), lineHeight: 1 }}>{(selectedNode.score * 100).toFixed(0)}</div>
617
+ <div style={{ paddingBottom: 6 }}>
618
+ <div style={{ fontSize: 11, fontWeight: 700, color: 'var(--text-muted)', letterSpacing: 0.4, textTransform: 'uppercase' }}>Quality score</div>
619
+ <div style={{ fontSize: 12, color: 'var(--text-dim)' }}>Current confidence in this skill’s network fit and maturity.</div>
620
+ </div>
621
+ </div>
622
+ <div className="score-track" style={{ height: 7, marginBottom: 12 }}>
623
+ <div className="score-fill" style={{ width: `${selectedNode.score * 100}%`, background: `linear-gradient(90deg, var(--blue), ${scoreColor(selectedNode.score)})` }} />
624
+ </div>
625
+ <div className="grid-2" style={{ gap: 10 }}>
626
+ <div style={{ padding: '12px 14px', borderRadius: 16, background: 'rgba(107,73,223,0.08)', border: '1px solid rgba(107,73,223,0.14)' }}>
627
+ <div style={{ fontSize: 10, color: 'var(--purple)', fontWeight: 700, textTransform: 'uppercase', letterSpacing: 0.4, marginBottom: 4 }}>Failures mapped</div>
628
+ <div style={{ fontSize: 22, fontWeight: 800, color: 'var(--purple)' }}>{selectedNode.failureCount}</div>
629
+ </div>
630
+ <div style={{ padding: '12px 14px', borderRadius: 16, background: 'rgba(16,185,129,0.08)', border: '1px solid rgba(16,185,129,0.14)' }}>
631
+ <div style={{ fontSize: 10, color: 'var(--green)', fontWeight: 700, textTransform: 'uppercase', letterSpacing: 0.4, marginBottom: 4 }}>Last evolved</div>
632
+ <div style={{ fontSize: 13, fontWeight: 700, color: 'var(--text)' }}>{selectedNode.lastEvolved ? new Date(selectedNode.lastEvolved).toLocaleDateString() : 'Not yet evolved'}</div>
633
+ </div>
634
+ </div>
635
+ </div>
636
+ </div>
637
+
638
+ <div style={{ padding: '18px', display: 'grid', gap: 18 }}>
639
+ {connectionGroups.length > 0 ? (
640
+ <div>
641
+ <div className="section-label">Connections</div>
642
+ <div style={{ display: 'grid', gap: 10 }}>
643
+ {connectionGroups.map(group => (
644
+ <div key={group.label} style={{ padding: '12px 14px', borderRadius: 18, background: 'rgba(255,255,255,0.74)', border: '1px solid var(--border)' }}>
645
+ <div style={{ fontSize: 11, fontWeight: 700, color: 'var(--text-muted)', letterSpacing: 0.4, textTransform: 'uppercase', marginBottom: 8 }}>{group.label}</div>
646
+ <div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
647
+ {group.ids.map(id => (
648
+ <button
649
+ key={`${group.label}-${id}`}
650
+ onClick={() => onSelectSkill(id)}
651
+ style={actionBtnStyle(
652
+ group.tone === 'purple'
653
+ ? 'var(--purple)'
654
+ : group.tone === 'green'
655
+ ? 'var(--green)'
656
+ : group.tone === 'red'
657
+ ? 'var(--red)'
658
+ : 'var(--blue)'
659
+ )}
660
+ >
661
+ {id}
662
+ </button>
663
+ ))}
664
+ </div>
665
+ </div>
666
+ ))}
667
+ </div>
668
+ </div>
669
+ ) : null}
670
+
671
+ {selectedEvo.length > 0 ? (
672
+ <div>
673
+ <div className="section-label">Evolution record</div>
674
+ <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', marginBottom: 10 }}>
675
+ <span className="hero-chip hero-chip-green">{acceptedCount} accepted</span>
676
+ {rejectedCount > 0 ? <span className="hero-chip hero-chip-red">{rejectedCount} rejected</span> : null}
677
+ <span className="hero-chip hero-chip-neutral">{selectedEvo.length} total revisions</span>
678
+ </div>
679
+ <div style={{ display: 'grid', gap: 8 }}>
680
+ {selectedEvo.map((entry, index) => (
681
+ <div key={`${entry.id}-${index}`} style={{
682
+ padding: '12px 14px',
683
+ borderRadius: 16,
684
+ background: 'rgba(255,255,255,0.74)',
685
+ border: `1px solid ${entry.outcome === 'accepted' ? 'rgba(16,185,129,0.18)' : 'rgba(239,68,68,0.18)'}`,
686
+ boxShadow: 'var(--shadow-xs)',
687
+ }}>
688
+ <div style={{ display: 'flex', justifyContent: 'space-between', gap: 10, alignItems: 'center', marginBottom: 6, flexWrap: 'wrap' }}>
689
+ <div style={{ display: 'flex', gap: 6, alignItems: 'center', flexWrap: 'wrap' }}>
690
+ <span className={`hero-chip hero-chip-${entry.outcome === 'accepted' ? 'green' : 'red'}`}>{entry.outcome}</span>
691
+ <span className="hero-chip hero-chip-neutral">{entry.action}</span>
692
+ </div>
693
+ <span style={{ fontSize: 11, color: 'var(--text-muted)' }}>{new Date(entry.timestamp).toLocaleDateString()}</span>
694
+ </div>
695
+ <div style={{ fontSize: 12.5, color: 'var(--text-secondary)', lineHeight: 1.6, marginBottom: 8 }}>{entry.description}</div>
696
+ <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
697
+ <span className="score-pill">T:{entry.task}</span>
698
+ <span className="score-pill">A:{entry.align}</span>
699
+ <span className="score-pill">S:{entry.sideEffect}</span>
700
+ </div>
701
+ </div>
702
+ ))}
703
+ </div>
704
+ </div>
705
+ ) : null}
706
+
707
+ <div>
708
+ <div className="section-label">Navigate</div>
709
+ <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
710
+ {view !== 'graph' ? <button onClick={() => onViewChange('graph')} style={actionBtnStyle('var(--blue)')}>◎ View in graph</button> : null}
711
+ {view !== 'general' ? <button onClick={() => onViewChange('general')} style={actionBtnStyle()}>▤ View in skills</button> : null}
712
+ {view !== 'coevolution' ? <button onClick={() => onViewChange('coevolution')} style={actionBtnStyle()}>↕ Co-evolution</button> : null}
713
+ {view !== 'projects' ? <button onClick={() => onViewChange('projects')} style={actionBtnStyle('var(--yellow)')}>◇ Project usage</button> : null}
714
+ </div>
715
+ </div>
716
+
717
+ <div>
718
+ <div className="section-label">Manage skill</div>
719
+ <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
720
+ <button onClick={onToggleEdit} style={actionBtnStyle(editing ? 'var(--purple)' : undefined)}>
721
+ {editing ? '✕ Cancel edit' : '✎ Edit content'}
722
+ </button>
723
+ <button onClick={() => void onPromote()} disabled={actionLoading} style={{ ...actionBtnStyle('var(--blue)'), opacity: actionLoading ? 0.7 : 1 }}>
724
+ ↑ Promote
725
+ </button>
726
+ <button onClick={() => void onDemote()} disabled={actionLoading} style={{ ...actionBtnStyle('var(--yellow)'), opacity: actionLoading ? 0.7 : 1 }}>
727
+ ↓ Demote
728
+ </button>
729
+ <button onClick={() => void onDelete()} disabled={actionLoading} style={{ ...actionBtnStyle('var(--red)'), opacity: actionLoading ? 0.7 : 1 }}>
730
+ ✕ Delete
731
+ </button>
732
+ </div>
733
+ </div>
734
+
735
+ {editing ? (
736
+ <div>
737
+ <div className="section-label">Edit skill content</div>
738
+ <textarea
739
+ value={editContent}
740
+ onChange={event => onEditContentChange(event.target.value)}
741
+ style={{
742
+ width: '100%',
743
+ height: 260,
744
+ fontFamily: 'var(--font-mono)',
745
+ fontSize: 11.5,
746
+ background: '#1e1e2e',
747
+ color: '#cdd6f4',
748
+ border: '1px solid var(--border)',
749
+ borderRadius: 14,
750
+ padding: 14,
751
+ resize: 'vertical',
752
+ lineHeight: 1.65,
753
+ }}
754
+ />
755
+ <button onClick={() => void onSave()} disabled={actionLoading} style={{ ...actionBtnStyle('var(--green)'), width: '100%', marginTop: 10, justifyContent: 'center', opacity: actionLoading ? 0.7 : 1 }}>
756
+ {actionLoading ? 'Saving changes…' : '✓ Save changes'}
757
+ </button>
758
+ </div>
759
+ ) : null}
760
+
761
+ {adaptation ? (
762
+ <div>
763
+ <div className="section-label">Network adaptation</div>
764
+ <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', marginBottom: 10 }}>
765
+ <span className={`hero-chip hero-chip-${adaptation.status === 'ok' ? 'green' : adaptation.status === 'warning' ? 'yellow' : 'red'}`}>{adaptation.status}</span>
766
+ <span className="hero-chip hero-chip-neutral">post-change network response</span>
767
+ </div>
768
+ <div style={{ display: 'grid', gap: 8 }}>
769
+ {adaptation.messages.map((message, index) => (
770
+ <div key={`${message.type}-${index}`} style={{
771
+ padding: '10px 12px',
772
+ borderRadius: 14,
773
+ border: `1px solid ${message.type === 'warning' ? 'var(--yellow-border)' : message.type === 'action' ? 'var(--red-border)' : 'var(--border)'}`,
774
+ background: message.type === 'warning' ? 'var(--yellow-light)' : message.type === 'action' ? 'var(--red-light)' : 'rgba(255,255,255,0.72)',
775
+ color: message.type === 'warning' ? 'var(--yellow)' : message.type === 'action' ? 'var(--red)' : 'var(--text-secondary)',
776
+ fontSize: 12,
777
+ lineHeight: 1.6,
778
+ }}>
779
+ {message.type === 'warning' ? '⚠ ' : message.type === 'action' ? '⚡ ' : 'ℹ '}{message.text}
780
+ </div>
781
+ ))}
782
+ </div>
783
+ {adaptation.suggestions.length > 0 ? (
784
+ <div style={{ marginTop: 10, display: 'grid', gap: 6 }}>
785
+ {adaptation.suggestions.map((suggestion, index) => (
786
+ <div key={`${suggestion.type}-${index}`} style={{
787
+ padding: '10px 12px',
788
+ borderRadius: 14,
789
+ background: 'rgba(107,73,223,0.08)',
790
+ border: '1px solid rgba(107,73,223,0.14)',
791
+ fontSize: 11.5,
792
+ color: 'var(--text-secondary)',
793
+ lineHeight: 1.6,
794
+ }}>
795
+ <span className="badge badge-purple" style={{ marginRight: 8 }}>{suggestion.type}</span>
796
+ {suggestion.description}
797
+ </div>
798
+ ))}
799
+ </div>
800
+ ) : null}
801
+ </div>
802
+ ) : null}
803
+
804
+ {selectedContent && !editing ? (
805
+ <div>
806
+ <div className="section-label">Skill content</div>
807
+ <pre style={{
808
+ margin: 0,
809
+ padding: '14px',
810
+ borderRadius: 16,
811
+ border: '1px solid var(--border)',
812
+ background: 'rgba(255,255,255,0.72)',
813
+ whiteSpace: 'pre-wrap',
814
+ wordBreak: 'break-word',
815
+ color: 'var(--text-secondary)',
816
+ lineHeight: 1.7,
817
+ maxHeight: 340,
818
+ overflow: 'auto',
819
+ fontSize: 11.5,
820
+ }}>
821
+ {selectedContent.slice(0, 2200)}
822
+ </pre>
823
+ </div>
824
+ ) : null}
641
825
  </div>
642
826
  </div>
643
827
  )