helixevo 0.2.39 → 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.
- package/CHANGELOG.md +16 -0
- package/dashboard/app/changelog/page.tsx +179 -54
- package/dashboard/app/commands/page.tsx +232 -155
- package/dashboard/app/evolution/page.tsx +105 -102
- package/dashboard/app/frontier/page.tsx +103 -100
- package/dashboard/app/globals.css +1105 -403
- package/dashboard/app/guide/page.tsx +46 -2
- package/dashboard/app/layout.tsx +28 -57
- package/dashboard/app/network/client.tsx +453 -269
- package/dashboard/app/network/page.tsx +12 -2
- package/dashboard/app/page.tsx +166 -185
- package/dashboard/app/projects/client.tsx +891 -509
- package/dashboard/app/research/client.tsx +180 -248
- package/dashboard/components/SkillFlowNode.tsx +86 -128
- package/dashboard/components/console-panel.tsx +25 -0
- package/dashboard/components/metric-card.tsx +45 -0
- package/dashboard/components/overview-actions.tsx +29 -40
- package/dashboard/components/page-hero.tsx +44 -0
- package/dashboard/components/quick-actions.tsx +93 -167
- package/dashboard/components/section-frame.tsx +35 -0
- package/dashboard/components/sidebar-nav.tsx +75 -0
- package/dashboard/components/update-banner.tsx +101 -145
- package/dashboard/lib/data.ts +2 -2
- package/package.json +1 -1
|
@@ -19,7 +19,17 @@ export default function SkillNetworkPage() {
|
|
|
19
19
|
)
|
|
20
20
|
|
|
21
21
|
// Build evolution history per skill
|
|
22
|
-
const evolutionBySkill
|
|
22
|
+
const evolutionBySkill: Record<string, Array<{
|
|
23
|
+
id: string
|
|
24
|
+
timestamp: string
|
|
25
|
+
action: string
|
|
26
|
+
description: string
|
|
27
|
+
outcome: string
|
|
28
|
+
outcomeReason: string
|
|
29
|
+
task: number
|
|
30
|
+
align: number
|
|
31
|
+
sideEffect: number
|
|
32
|
+
}>> = {}
|
|
23
33
|
for (const iter of history.iterations) {
|
|
24
34
|
for (const p of iter.proposals) {
|
|
25
35
|
if (!evolutionBySkill[p.targetSkill]) evolutionBySkill[p.targetSkill] = []
|
|
@@ -33,7 +43,7 @@ export default function SkillNetworkPage() {
|
|
|
33
43
|
}
|
|
34
44
|
|
|
35
45
|
// Load skill contents
|
|
36
|
-
const skillContents = {}
|
|
46
|
+
const skillContents: Record<string, string> = {}
|
|
37
47
|
for (const n of graph.nodes) {
|
|
38
48
|
const raw = loadSkillContent(n.id)
|
|
39
49
|
if (raw) skillContents[n.id] = raw.match(/^---\n[\s\S]*?\n---\n([\s\S]*)$/)?.[1]?.trim() ?? raw
|
package/dashboard/app/page.tsx
CHANGED
|
@@ -1,230 +1,211 @@
|
|
|
1
1
|
import Link from 'next/link'
|
|
2
|
-
import { getDashboardSummary,
|
|
2
|
+
import { getDashboardSummary, loadFailures, loadFrontier, loadGraph, loadHistory, listProjects } from '@/lib/data'
|
|
3
3
|
import { OverviewActions } from '@/components/overview-actions'
|
|
4
|
+
import { PageHero } from '@/components/page-hero'
|
|
5
|
+
import { MetricCard } from '@/components/metric-card'
|
|
6
|
+
import { SectionFrame } from '@/components/section-frame'
|
|
4
7
|
|
|
5
8
|
export const dynamic = 'force-dynamic'
|
|
6
9
|
|
|
7
|
-
function
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
return (
|
|
11
|
-
<Link href={href} style={{ textDecoration: 'none', color: 'inherit' }}>
|
|
12
|
-
<div className="stat-card" style={{ cursor: 'pointer' }}>
|
|
13
|
-
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
|
|
14
|
-
<div>
|
|
15
|
-
<div className="stat-value" style={{ color }}>{value}</div>
|
|
16
|
-
<div className="stat-label">{label}</div>
|
|
17
|
-
{sub && <div style={{ fontSize: 10, color: 'var(--text-muted)', marginTop: 2 }}>{sub}</div>}
|
|
18
|
-
</div>
|
|
19
|
-
{accent && (
|
|
20
|
-
<div style={{
|
|
21
|
-
width: 36, height: 36, borderRadius: 10,
|
|
22
|
-
background: `${color}12`, display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
23
|
-
color, fontSize: 16,
|
|
24
|
-
}}>{accent}</div>
|
|
25
|
-
)}
|
|
26
|
-
</div>
|
|
27
|
-
</div>
|
|
28
|
-
</Link>
|
|
29
|
-
)
|
|
10
|
+
function scoreColor(score: number) {
|
|
11
|
+
if (score >= 0.8) return 'var(--green)'
|
|
12
|
+
if (score >= 0.6) return 'var(--yellow)'
|
|
13
|
+
return 'var(--red)'
|
|
30
14
|
}
|
|
31
15
|
|
|
32
16
|
export default function Overview() {
|
|
33
|
-
const
|
|
17
|
+
const summary = getDashboardSummary()
|
|
34
18
|
const frontier = loadFrontier()
|
|
35
19
|
const history = loadHistory()
|
|
36
20
|
const graph = loadGraph()
|
|
37
21
|
const failures = loadFailures()
|
|
38
|
-
const
|
|
22
|
+
const unresolved = failures.filter((failure) => !failure.resolved)
|
|
23
|
+
const recentRuns = history.iterations.slice(-4).reverse()
|
|
24
|
+
const topSkills = [...graph.nodes].sort((a, b) => b.score - a.score).slice(0, 10)
|
|
39
25
|
|
|
40
26
|
return (
|
|
41
|
-
<div>
|
|
42
|
-
<
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
27
|
+
<div className="overview-grid">
|
|
28
|
+
<PageHero
|
|
29
|
+
eyebrow="System cockpit"
|
|
30
|
+
title="Dashboard"
|
|
31
|
+
description="Monitor the live state of your self-evolving skill ecosystem, launch high-value actions, and trace how failures turn into frontier-worthy improvements."
|
|
32
|
+
chips={[
|
|
33
|
+
{ label: `${summary.skills.total} skills`, tone: 'purple' },
|
|
34
|
+
{ label: `${summary.failures.unresolved} unresolved corrections`, tone: summary.failures.unresolved > 0 ? 'yellow' : 'green' },
|
|
35
|
+
{ label: `${summary.evolution.runs} evolution runs`, tone: 'blue' },
|
|
36
|
+
{ label: `${frontier.programs.length}/${frontier.capacity} frontier slots`, tone: 'green' },
|
|
37
|
+
]}
|
|
38
|
+
actions={
|
|
39
|
+
<Link href="/projects" className="metric-card-anchor" style={{ minWidth: 240, display: 'block' }}>
|
|
40
|
+
<div className="metric-card metric-card-green metric-card-link">
|
|
41
|
+
<div className="metric-card-header">
|
|
42
|
+
<div>
|
|
43
|
+
<div className="metric-card-label">Next workflow</div>
|
|
44
|
+
<div className="metric-card-value" style={{ fontSize: 24 }}>Setup a project</div>
|
|
45
|
+
</div>
|
|
46
|
+
<div className="metric-card-icon">↗</div>
|
|
47
|
+
</div>
|
|
48
|
+
<div className="metric-card-sublabel">Analyze a folder or GitHub repo, match skills, and identify capability gaps.</div>
|
|
49
|
+
</div>
|
|
50
|
+
</Link>
|
|
51
|
+
}
|
|
52
|
+
/>
|
|
48
53
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
<
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
</div>
|
|
57
|
-
<span style={{ fontSize: 16, color: 'var(--text-muted)' }}>→</span>
|
|
58
|
-
</div>
|
|
59
|
-
</Link>
|
|
54
|
+
<div className="grid-5">
|
|
55
|
+
<MetricCard label="Total skills" value={summary.skills.total} sublabel={`${summary.skills.evolved} evolved • ${summary.skillTests} skill tests`} tone="purple" href="/network" icon="◆" />
|
|
56
|
+
<MetricCard label="Accepted proposals" value={summary.evolution.accepted} sublabel={`${summary.evolution.rejected} rejected`} tone="green" href="/evolution" icon="✓" />
|
|
57
|
+
<MetricCard label="Unresolved corrections" value={summary.failures.unresolved} sublabel={`out of ${summary.failures.total} captured failures`} tone={summary.failures.unresolved > 0 ? 'yellow' : 'green'} href={summary.failures.unresolved > 0 ? '#attention' : '/evolution'} icon="!" />
|
|
58
|
+
<MetricCard label="Discoveries" value={summary.buffer.discoveries} sublabel={`${summary.buffer.drafts} drafts in progress`} tone="blue" href="/research" icon="◎" />
|
|
59
|
+
<MetricCard label="Frontier candidates" value={frontier.programs.length} sublabel={`${summary.canaries} active canaries`} tone="neutral" href="/frontier" icon="▲" />
|
|
60
|
+
</div>
|
|
60
61
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
62
|
+
<SectionFrame
|
|
63
|
+
eyebrow="Execution layer"
|
|
64
|
+
title="Quick actions"
|
|
65
|
+
description="Run the highest-leverage HelixEvo workflows from one place. Each action flows from signal to judgment to applied change."
|
|
66
|
+
>
|
|
66
67
|
<OverviewActions
|
|
67
68
|
hasSkills={graph.nodes.length > 0}
|
|
68
69
|
hasFailures={failures.length > 0}
|
|
69
70
|
hasEdges={graph.edges.length > 0}
|
|
70
|
-
unresolvedCount={
|
|
71
|
+
unresolvedCount={summary.failures.unresolved}
|
|
71
72
|
skillCount={graph.nodes.length}
|
|
72
73
|
projectList={listProjects()}
|
|
73
74
|
/>
|
|
74
|
-
</
|
|
75
|
-
|
|
76
|
-
{/* Stats — all clickable, linking to relevant pages */}
|
|
77
|
-
<div className="grid-5" style={{ marginBottom: 28 }}>
|
|
78
|
-
<StatCard value={s.skills.total} label="Total Skills" color="var(--purple)" sub={`${s.skills.evolved} evolved`} accent="◆" href="/network" />
|
|
79
|
-
<StatCard value={s.evolution.accepted} label="Accepted" color="var(--green)" sub={`${s.evolution.rejected} rejected`} accent="✓" href="/evolution" />
|
|
80
|
-
<StatCard value={s.failures.unresolved} label="Unresolved" color="var(--yellow)" sub={`of ${s.failures.total} total`} accent="!" href="#unresolved" />
|
|
81
|
-
<StatCard value={s.buffer.discoveries} label="Discoveries" color="var(--blue)" sub={`${s.buffer.drafts} drafts`} accent="◎" href="/research" />
|
|
82
|
-
<StatCard value={frontier.programs.length} label="Frontier" color="var(--text-secondary)" sub={`/${frontier.capacity} capacity`} accent="▲" href="/frontier" />
|
|
83
|
-
</div>
|
|
75
|
+
</SectionFrame>
|
|
84
76
|
|
|
85
|
-
{
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
borderLeft: '3px solid var(--yellow)',
|
|
102
|
-
}}>
|
|
103
|
-
<div style={{ fontSize: 12, fontWeight: 600, color: 'var(--text)', marginBottom: 3 }}>
|
|
104
|
-
{f.userRequest.slice(0, 100)}{f.userRequest.length > 100 ? '...' : ''}
|
|
77
|
+
{unresolved.length > 0 ? (
|
|
78
|
+
<SectionFrame
|
|
79
|
+
eyebrow="Attention"
|
|
80
|
+
title="Unresolved corrections"
|
|
81
|
+
description="These user corrections have been captured but not yet folded back into the skill network."
|
|
82
|
+
tone="yellow"
|
|
83
|
+
className="anchor-target"
|
|
84
|
+
>
|
|
85
|
+
<div id="attention" className="signal-list">
|
|
86
|
+
{unresolved.slice(0, 5).map((failure, index) => (
|
|
87
|
+
<div key={`${failure.id}-${index}`} className="signal-row signal-row-attention">
|
|
88
|
+
<div className="signal-dot" />
|
|
89
|
+
<div style={{ flex: 1 }}>
|
|
90
|
+
<div className="signal-title">
|
|
91
|
+
{failure.userRequest.slice(0, 120)}
|
|
92
|
+
{failure.userRequest.length > 120 ? '…' : ''}
|
|
105
93
|
</div>
|
|
106
|
-
<div
|
|
107
|
-
Correction: {
|
|
94
|
+
<div className="signal-text">
|
|
95
|
+
Correction: {failure.correction.slice(0, 180)}
|
|
96
|
+
{failure.correction.length > 180 ? '…' : ''}
|
|
108
97
|
</div>
|
|
109
|
-
<div style={{ display: 'flex', gap:
|
|
110
|
-
<span className="badge badge-yellow">{
|
|
111
|
-
{
|
|
112
|
-
<span
|
|
113
|
-
{new Date(f.timestamp).toLocaleDateString()}
|
|
114
|
-
</span>
|
|
98
|
+
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', marginTop: 10 }}>
|
|
99
|
+
<span className="badge badge-yellow">{failure.correctionType}</span>
|
|
100
|
+
{failure.project ? <span className="badge badge-gray">{failure.project}</span> : null}
|
|
101
|
+
<span className="badge badge-gray">{new Date(failure.timestamp).toLocaleDateString()}</span>
|
|
115
102
|
</div>
|
|
116
103
|
</div>
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
)}
|
|
123
|
-
</div>
|
|
104
|
+
</div>
|
|
105
|
+
))}
|
|
106
|
+
{unresolved.length > 5 ? (
|
|
107
|
+
<div className="signal-text" style={{ textAlign: 'center' }}>+{unresolved.length - 5} more unresolved corrections</div>
|
|
108
|
+
) : null}
|
|
124
109
|
</div>
|
|
125
|
-
</
|
|
126
|
-
)}
|
|
110
|
+
</SectionFrame>
|
|
111
|
+
) : null}
|
|
127
112
|
|
|
128
113
|
<div className="grid-2">
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
<div
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
}}>
|
|
145
|
-
{(p.score * 100).toFixed(0)}
|
|
146
|
-
</span>
|
|
147
|
-
</div>
|
|
148
|
-
<div style={{ display: 'flex', gap: 6, fontSize: 11 }}>
|
|
149
|
-
<span className="score-pill" style={{ color: 'var(--green)' }}>T:{(p.scores.taskCompletion * 10).toFixed(0)}</span>
|
|
150
|
-
<span className="score-pill" style={{ color: 'var(--blue)' }}>A:{(p.scores.correctionAlignment * 10).toFixed(0)}</span>
|
|
151
|
-
<span className="score-pill" style={{ color: 'var(--purple)' }}>S:{(p.scores.sideEffectFree * 10).toFixed(0)}</span>
|
|
152
|
-
</div>
|
|
153
|
-
<div style={{ fontSize: 11, color: 'var(--text-dim)', marginTop: 4, lineHeight: 1.4 }}>
|
|
154
|
-
{p.changesDescription.slice(0, 80)}
|
|
114
|
+
<SectionFrame
|
|
115
|
+
eyebrow="Frontier"
|
|
116
|
+
title="Current leaders"
|
|
117
|
+
description="Top-performing configurations ranked by composite strength across task completion, correction alignment, and side-effect safety."
|
|
118
|
+
actions={<Link href="/frontier" className="badge badge-gray">Open frontier</Link>}
|
|
119
|
+
>
|
|
120
|
+
<div className="summary-list">
|
|
121
|
+
{frontier.programs.map((program, index) => (
|
|
122
|
+
<Link key={`${program.id}-${index}`} href="/frontier" className="summary-row">
|
|
123
|
+
<div className="summary-row-main">
|
|
124
|
+
<div className="summary-row-title">{program.id}</div>
|
|
125
|
+
<div className="summary-row-meta">{program.changesDescription.slice(0, 90)}{program.changesDescription.length > 90 ? '…' : ''}</div>
|
|
126
|
+
<div style={{ display: 'flex', gap: 6, marginTop: 8, flexWrap: 'wrap' }}>
|
|
127
|
+
<span className="score-pill" style={{ color: 'var(--green)' }}>T {(program.scores.taskCompletion * 10).toFixed(0)}</span>
|
|
128
|
+
<span className="score-pill" style={{ color: 'var(--blue)' }}>A {(program.scores.correctionAlignment * 10).toFixed(0)}</span>
|
|
129
|
+
<span className="score-pill" style={{ color: 'var(--purple)' }}>S {(program.scores.sideEffectFree * 10).toFixed(0)}</span>
|
|
155
130
|
</div>
|
|
156
131
|
</div>
|
|
157
|
-
|
|
158
|
-
|
|
132
|
+
<div style={{ fontSize: 26, fontWeight: 850, color: scoreColor(program.score), lineHeight: 1 }}>
|
|
133
|
+
{(program.score * 100).toFixed(0)}
|
|
134
|
+
</div>
|
|
135
|
+
</Link>
|
|
136
|
+
))}
|
|
137
|
+
{frontier.programs.length === 0 ? (
|
|
138
|
+
<div className="empty-state" style={{ padding: 24 }}>
|
|
139
|
+
<div className="empty-state-title">Frontier is empty</div>
|
|
140
|
+
<div className="empty-state-desc">Run <code>helixevo evolve</code> to populate candidate configurations.</div>
|
|
141
|
+
</div>
|
|
142
|
+
) : null}
|
|
159
143
|
</div>
|
|
160
|
-
</
|
|
144
|
+
</SectionFrame>
|
|
161
145
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
<div
|
|
173
|
-
|
|
174
|
-
|
|
146
|
+
<SectionFrame
|
|
147
|
+
eyebrow="Evolution"
|
|
148
|
+
title="Recent runs"
|
|
149
|
+
description="A compact chronicle of the latest evolution loops and the proposals they generated."
|
|
150
|
+
actions={<Link href="/evolution" className="badge badge-gray">Open timeline</Link>}
|
|
151
|
+
>
|
|
152
|
+
<div className="summary-list">
|
|
153
|
+
{recentRuns.map((iteration, index) => (
|
|
154
|
+
<Link key={`${iteration.id}-${index}`} href="/evolution" className="summary-row" style={{ alignItems: 'flex-start' }}>
|
|
155
|
+
<div className="summary-row-main">
|
|
156
|
+
<div className="summary-row-title">{iteration.id}</div>
|
|
157
|
+
<div className="summary-row-meta">
|
|
158
|
+
{new Date(iteration.timestamp).toLocaleDateString()} • {iteration.trigger} • {iteration.failureCount} failures
|
|
159
|
+
</div>
|
|
160
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 6, marginTop: 10 }}>
|
|
161
|
+
{iteration.proposals.slice(0, 3).map((proposal) => (
|
|
162
|
+
<div key={proposal.id} style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
|
163
|
+
<span style={{ width: 8, height: 8, borderRadius: 999, background: proposal.outcome === 'accepted' ? 'var(--green)' : 'var(--red)', flexShrink: 0 }} />
|
|
164
|
+
<span style={{ fontSize: 12, fontWeight: 700, color: 'var(--text)' }}>{proposal.targetSkill}</span>
|
|
165
|
+
<span className="badge badge-gray">{proposal.action}</span>
|
|
166
|
+
</div>
|
|
167
|
+
))}
|
|
175
168
|
</div>
|
|
176
|
-
{iter.proposals.map(p => (
|
|
177
|
-
<div key={p.id} style={{
|
|
178
|
-
display: 'flex', alignItems: 'center', gap: 8, fontSize: 12, marginBottom: 4,
|
|
179
|
-
padding: '4px 0',
|
|
180
|
-
}}>
|
|
181
|
-
<span style={{
|
|
182
|
-
width: 8, height: 8, borderRadius: '50%', flexShrink: 0,
|
|
183
|
-
background: p.outcome === 'accepted' ? 'var(--green)' : 'var(--red)',
|
|
184
|
-
}} />
|
|
185
|
-
<span style={{ fontWeight: 500 }}>{p.targetSkill}</span>
|
|
186
|
-
<span className="badge badge-gray" style={{ fontSize: 9 }}>{p.action}</span>
|
|
187
|
-
</div>
|
|
188
|
-
))}
|
|
189
169
|
</div>
|
|
190
|
-
|
|
191
|
-
|
|
170
|
+
</Link>
|
|
171
|
+
))}
|
|
172
|
+
{recentRuns.length === 0 ? (
|
|
173
|
+
<div className="empty-state" style={{ padding: 24 }}>
|
|
174
|
+
<div className="empty-state-title">No evolution runs yet</div>
|
|
175
|
+
<div className="empty-state-desc">Start with <code>helixevo evolve</code> to create your first chronicle entry.</div>
|
|
176
|
+
</div>
|
|
177
|
+
) : null}
|
|
192
178
|
</div>
|
|
193
|
-
</
|
|
179
|
+
</SectionFrame>
|
|
194
180
|
</div>
|
|
195
181
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
<
|
|
208
|
-
|
|
209
|
-
{n.generation > 0 && <span className="badge badge-green" style={{ fontSize: 9 }}>gen {n.generation}</span>}
|
|
210
|
-
</div>
|
|
211
|
-
<div style={{ display: 'flex', alignItems: 'center', gap: 6, minWidth: 90, flexShrink: 0 }}>
|
|
212
|
-
<div className="score-track" style={{ width: 50 }}>
|
|
213
|
-
<div className="score-fill" style={{
|
|
214
|
-
width: `${n.score * 100}%`,
|
|
215
|
-
background: n.score >= 0.8 ? 'var(--green)' : n.score >= 0.6 ? 'var(--yellow)' : 'var(--red)',
|
|
216
|
-
}} />
|
|
217
|
-
</div>
|
|
218
|
-
<span style={{ fontSize: 12, fontWeight: 600, color: n.score >= 0.8 ? 'var(--green)' : 'var(--yellow)' }}>
|
|
219
|
-
{(n.score * 100).toFixed(0)}
|
|
220
|
-
</span>
|
|
221
|
-
</div>
|
|
182
|
+
<SectionFrame
|
|
183
|
+
eyebrow="Network health"
|
|
184
|
+
title="Top skill signals"
|
|
185
|
+
description="Highest-scoring skills right now, ordered by current quality score and generation."
|
|
186
|
+
actions={<Link href="/network" className="badge badge-gray">Open skill network</Link>}
|
|
187
|
+
>
|
|
188
|
+
<div className="skill-score-grid">
|
|
189
|
+
{topSkills.map((skill) => (
|
|
190
|
+
<Link key={skill.id} href="/network" className="skill-score-row">
|
|
191
|
+
<div className="skill-score-main">
|
|
192
|
+
<div className="skill-score-name">
|
|
193
|
+
<span>{skill.name}</span>
|
|
194
|
+
{skill.generation > 0 ? <span className="badge badge-green">gen {skill.generation}</span> : null}
|
|
222
195
|
</div>
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
196
|
+
</div>
|
|
197
|
+
<div className="skill-score-value">
|
|
198
|
+
<div className="score-track" style={{ width: 72 }}>
|
|
199
|
+
<div className="score-fill" style={{ width: `${skill.score * 100}%`, background: scoreColor(skill.score) }} />
|
|
200
|
+
</div>
|
|
201
|
+
<span style={{ minWidth: 34, textAlign: 'right', fontWeight: 800, color: scoreColor(skill.score) }}>
|
|
202
|
+
{(skill.score * 100).toFixed(0)}
|
|
203
|
+
</span>
|
|
204
|
+
</div>
|
|
205
|
+
</Link>
|
|
206
|
+
))}
|
|
226
207
|
</div>
|
|
227
|
-
</
|
|
208
|
+
</SectionFrame>
|
|
228
209
|
</div>
|
|
229
210
|
)
|
|
230
211
|
}
|