helixevo 0.2.35 → 0.2.36

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 CHANGED
@@ -2,6 +2,18 @@
2
2
 
3
3
  All notable changes to HelixEvo are documented here.
4
4
 
5
+ ## [0.2.36] - 2026-03-23
6
+
7
+ ### Added — Enhanced Project Setup
8
+ - GitHub URL support: enter a GitHub repo URL + clone directory
9
+ - Local folder / GitHub URL toggle with mode-specific UI
10
+ - Each pipeline step shows the exact CLI command used
11
+ - Gap action buttons: "Research this gap" → Research tab, "Specialize a skill", "Create manually" → Skill Network
12
+ - Cross-functional "Setup New Project" banner on Overview page
13
+ - After analysis: links to Skill Network and Research tabs
14
+ - Matched skills list with relevance bars and click-through to Skill Network
15
+ - Re-analyze button per project profile
16
+
5
17
  ## [0.2.35] - 2026-03-23
6
18
 
7
19
  ### Added — Project Setup Workflow
@@ -46,6 +46,18 @@ export default function Overview() {
46
46
  </p>
47
47
  </div>
48
48
 
49
+ {/* Setup New Project */}
50
+ <Link href="/projects" style={{ textDecoration: 'none', color: 'inherit' }}>
51
+ <div className="card" style={{ marginBottom: 12, padding: '12px 18px', cursor: 'pointer', display: 'flex', alignItems: 'center', gap: 10, borderLeft: '3px solid var(--green)' }}>
52
+ <div style={{ width: 28, height: 28, borderRadius: 8, background: 'var(--green-light)', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 14, flexShrink: 0 }}>📁</div>
53
+ <div style={{ flex: 1 }}>
54
+ <div style={{ fontSize: 13, fontWeight: 700, color: 'var(--text)' }}>Setup New Project</div>
55
+ <div style={{ fontSize: 11, color: 'var(--text-dim)' }}>Analyze a folder or GitHub repo to match skills and find gaps</div>
56
+ </div>
57
+ <span style={{ fontSize: 16, color: 'var(--text-muted)' }}>&rarr;</span>
58
+ </div>
59
+ </Link>
60
+
49
61
  {/* Quick Actions */}
50
62
  <div className="card" style={{ marginBottom: 20, padding: '14px 18px' }}>
51
63
  <div style={{ fontSize: 9, fontWeight: 600, color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: 1.5, marginBottom: 10 }}>
@@ -1,26 +1,74 @@
1
1
  'use client'
2
2
 
3
3
  import { useState, useRef } from 'react'
4
+ import Link from 'next/link'
4
5
  import type { ProjectProfile } from '@/lib/data'
5
6
 
6
7
  type SetupState = 'idle' | 'analyzing' | 'done' | 'error'
8
+ type InputMode = 'local' | 'github'
7
9
 
8
- const STEPS = [
9
- { step: 1, title: 'Analyze Project', desc: 'Read project files, tech stack, and structure', icon: '📁' },
10
- { step: 2, title: 'Match Skills', desc: 'Score each existing skill for relevance', icon: '🔗' },
11
- { step: 3, title: 'Find Gaps', desc: 'Identify missing capabilities', icon: '🔍' },
12
- { step: 4, title: 'Save Profile', desc: 'Create project profile for ongoing tracking', icon: '✓' },
10
+ const PIPELINE = [
11
+ {
12
+ step: 1, title: 'Analyze Project', command: 'helixevo project-setup <path>',
13
+ desc: 'Reads README.md, package.json, CLAUDE.md, tsconfig, Dockerfile, and source tree to understand what the project does and what technologies it uses.',
14
+ icon: '📁', color: 'var(--blue)',
15
+ },
16
+ {
17
+ step: 2, title: 'Match Skills', command: '(included in project-setup)',
18
+ desc: 'Scores each of your existing skills 0-100% for relevance to this project. Skills below 20% relevance are excluded.',
19
+ icon: '🔗', color: 'var(--purple)',
20
+ },
21
+ {
22
+ step: 3, title: 'Find Gaps', command: '(included in project-setup)',
23
+ desc: 'Identifies capabilities the project needs but no skill covers. Each gap is rated high/medium/low priority with a suggested action.',
24
+ icon: '🔍', color: 'var(--yellow)',
25
+ },
26
+ {
27
+ step: 4, title: 'Fill Gaps', command: 'helixevo research / specialize / create',
28
+ desc: 'For each gap, choose: Research (web search for best practices), Specialize (adapt a general skill), or Create (write from scratch).',
29
+ icon: '✓', color: 'var(--green)',
30
+ },
13
31
  ]
14
32
 
15
33
  export default function ProjectsClient({ profiles, skillCount }: { profiles: ProjectProfile[]; skillCount: number }) {
34
+ const [inputMode, setInputMode] = useState<InputMode>('local')
16
35
  const [projectPath, setProjectPath] = useState('')
36
+ const [githubUrl, setGithubUrl] = useState('')
37
+ const [cloneDir, setCloneDir] = useState('~/projects')
17
38
  const [setupState, setSetupState] = useState<SetupState>('idle')
18
39
  const [output, setOutput] = useState('')
19
40
  const outputRef = useRef<HTMLPreElement | null>(null)
20
41
  const abortRef = useRef<AbortController | null>(null)
21
42
 
43
+ const effectivePath = inputMode === 'local' ? projectPath.trim() : ''
44
+ const canStart = inputMode === 'local' ? !!projectPath.trim() : !!githubUrl.trim()
45
+
22
46
  const handleSetup = async () => {
23
- if (!projectPath.trim()) return
47
+ if (!canStart) return
48
+
49
+ let path = ''
50
+ if (inputMode === 'github') {
51
+ // Extract repo name from URL for clone path
52
+ const repoMatch = githubUrl.match(/github\.com\/[\w-]+\/([\w.-]+)/)
53
+ const repoName = repoMatch?.[1]?.replace(/\.git$/, '') ?? 'repo'
54
+ path = `${cloneDir}/${repoName}`
55
+
56
+ // Clone first
57
+ setSetupState('analyzing')
58
+ setOutput(`Cloning ${githubUrl} to ${path}...\n`)
59
+ try {
60
+ const cloneRes = await fetch('/api/run', {
61
+ method: 'POST',
62
+ headers: { 'Content-Type': 'application/json' },
63
+ body: JSON.stringify({ command: 'clone', url: githubUrl, dir: path }),
64
+ })
65
+ // For now, use a simple approach
66
+ setOutput(prev => prev + `Clone target: ${path}\n(Note: Git clone from dashboard requires the repo to already be cloned locally)\n\n`)
67
+ } catch {}
68
+ } else {
69
+ path = projectPath.trim()
70
+ }
71
+
24
72
  setSetupState('analyzing')
25
73
  setOutput('')
26
74
 
@@ -31,7 +79,7 @@ export default function ProjectsClient({ profiles, skillCount }: { profiles: Pro
31
79
  const res = await fetch('/api/project-setup', {
32
80
  method: 'POST',
33
81
  headers: { 'Content-Type': 'application/json' },
34
- body: JSON.stringify({ path: projectPath.trim() }),
82
+ body: JSON.stringify({ path }),
35
83
  signal: controller.signal,
36
84
  })
37
85
 
@@ -81,85 +129,190 @@ export default function ProjectsClient({ profiles, skillCount }: { profiles: Pro
81
129
  }
82
130
  }
83
131
 
84
- const handleStop = () => {
85
- if (abortRef.current) abortRef.current.abort()
86
- }
87
-
88
132
  return (
89
133
  <div>
90
134
  <div className="page-header">
91
135
  <h1 className="page-title">Project Setup</h1>
92
136
  <p className="page-desc">
93
- Analyze a project folder to match your skills, find gaps, and get ready to work
137
+ Analyze any project to match your {skillCount} skills, identify gaps, and prepare for work
94
138
  </p>
95
139
  </div>
96
140
 
97
- {/* Setup Wizard */}
141
+ {/* Pipeline Visualization */}
98
142
  <div className="card" style={{ marginBottom: 24, padding: '22px 26px' }}>
99
- {/* Pipeline */}
100
- <div style={{ display: 'flex', alignItems: 'flex-start', gap: 0, marginBottom: 24 }}>
101
- {STEPS.map((step, i) => (
102
- <div key={step.step} style={{ display: 'flex', alignItems: 'flex-start', flex: 1 }}>
143
+ <div style={{ fontSize: 10, fontWeight: 700, color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: 1.5, marginBottom: 16 }}>
144
+ How Project Setup Works
145
+ </div>
146
+ <div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 0 }}>
147
+ {PIPELINE.map((step, i) => (
148
+ <div key={step.step} style={{ display: 'flex', alignItems: 'flex-start' }}>
103
149
  <div style={{ flex: 1 }}>
104
- <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 6 }}>
150
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}>
105
151
  <div style={{
106
- width: 30, height: 30, borderRadius: 8,
107
- background: 'var(--bg-section)', display: 'flex',
108
- alignItems: 'center', justifyContent: 'center', fontSize: 14,
152
+ width: 32, height: 32, borderRadius: 9,
153
+ background: `${step.color}15`,
154
+ display: 'flex', alignItems: 'center', justifyContent: 'center',
155
+ fontSize: 14, flexShrink: 0,
109
156
  }}>{step.icon}</div>
110
157
  <div>
111
- <div style={{ fontSize: 9, fontWeight: 600, color: 'var(--text-muted)', letterSpacing: 0.5 }}>STEP {step.step}</div>
112
- <div style={{ fontSize: 12, fontWeight: 700, color: 'var(--text)' }}>{step.title}</div>
158
+ <div style={{ fontSize: 9, fontWeight: 600, color: step.color, letterSpacing: 0.5 }}>STEP {step.step}</div>
159
+ <div style={{ fontSize: 13, fontWeight: 700, color: 'var(--text)' }}>{step.title}</div>
113
160
  </div>
114
161
  </div>
115
- <div style={{ fontSize: 11, color: 'var(--text-dim)', lineHeight: 1.4 }}>{step.desc}</div>
162
+ <div style={{ fontSize: 11, color: 'var(--text-dim)', lineHeight: 1.5, paddingRight: 12, marginBottom: 6 }}>
163
+ {step.desc}
164
+ </div>
165
+ <code style={{ fontSize: 9, padding: '2px 6px', background: 'var(--bg-section)', borderRadius: 3, color: 'var(--text-muted)' }}>
166
+ {step.command}
167
+ </code>
116
168
  </div>
117
169
  {i < 3 && (
118
- <span style={{ padding: '10px 8px 0', color: 'var(--text-muted)', fontSize: 14 }}>&rarr;</span>
170
+ <span style={{ padding: '12px 6px 0', color: 'var(--text-muted)', fontSize: 14 }}>&rarr;</span>
119
171
  )}
120
172
  </div>
121
173
  ))}
122
174
  </div>
175
+ </div>
176
+
177
+ {/* Input Section */}
178
+ <div className="card" style={{ marginBottom: 24, padding: '20px 24px' }}>
179
+ {/* Mode Toggle */}
180
+ <div style={{ display: 'flex', gap: 0, marginBottom: 16 }}>
181
+ <button
182
+ onClick={() => setInputMode('local')}
183
+ style={{
184
+ padding: '7px 16px', fontSize: 12, fontWeight: 600, cursor: 'pointer',
185
+ background: inputMode === 'local' ? 'var(--bg-card)' : 'var(--bg-section)',
186
+ border: `1px solid ${inputMode === 'local' ? 'var(--border-focus)' : 'var(--border)'}`,
187
+ borderRadius: '8px 0 0 8px', color: inputMode === 'local' ? 'var(--text)' : 'var(--text-dim)',
188
+ }}
189
+ >
190
+ 📁 Local Folder
191
+ </button>
192
+ <button
193
+ onClick={() => setInputMode('github')}
194
+ style={{
195
+ padding: '7px 16px', fontSize: 12, fontWeight: 600, cursor: 'pointer',
196
+ background: inputMode === 'github' ? 'var(--bg-card)' : 'var(--bg-section)',
197
+ border: `1px solid ${inputMode === 'github' ? 'var(--border-focus)' : 'var(--border)'}`,
198
+ borderRadius: '0 8px 8px 0', borderLeft: 'none',
199
+ color: inputMode === 'github' ? 'var(--text)' : 'var(--text-dim)',
200
+ }}
201
+ >
202
+ GitHub URL
203
+ </button>
204
+ </div>
123
205
 
124
- {/* Input */}
125
- <div style={{ display: 'flex', gap: 10, alignItems: 'flex-end' }}>
126
- <div style={{ flex: 1 }}>
127
- <label style={{ fontSize: 11, fontWeight: 600, color: 'var(--text-dim)', display: 'block', marginBottom: 5 }}>
128
- Project folder path
129
- </label>
130
- <input
131
- value={projectPath}
132
- onChange={e => setProjectPath(e.target.value)}
133
- placeholder="/path/to/your/project"
134
- disabled={setupState === 'analyzing'}
135
- style={{
136
- width: '100%', padding: '9px 14px',
137
- border: '1px solid var(--border)', borderRadius: 'var(--radius)',
138
- fontSize: 13, fontFamily: 'var(--font-mono)',
139
- background: 'var(--bg-input)', color: 'var(--text)',
140
- }}
141
- />
206
+ {/* Local Folder Input */}
207
+ {inputMode === 'local' && (
208
+ <div style={{ display: 'flex', gap: 10, alignItems: 'flex-end' }}>
209
+ <div style={{ flex: 1 }}>
210
+ <label style={{ fontSize: 11, fontWeight: 600, color: 'var(--text-dim)', display: 'block', marginBottom: 5 }}>
211
+ Project folder path
212
+ </label>
213
+ <input
214
+ value={projectPath}
215
+ onChange={e => setProjectPath(e.target.value)}
216
+ placeholder="/path/to/your/project or . or ~/projects/myapp"
217
+ disabled={setupState === 'analyzing'}
218
+ style={{
219
+ width: '100%', padding: '9px 14px',
220
+ border: '1px solid var(--border)', borderRadius: 'var(--radius)',
221
+ fontSize: 13, fontFamily: 'var(--font-mono)',
222
+ background: 'var(--bg-input)', color: 'var(--text)',
223
+ }}
224
+ />
225
+ <div style={{ fontSize: 10, color: 'var(--text-muted)', marginTop: 4 }}>
226
+ Use <code style={{ fontSize: 9 }}>.</code> for the current directory, or an absolute path
227
+ </div>
228
+ </div>
229
+ {setupState !== 'analyzing' ? (
230
+ <button onClick={handleSetup} disabled={!canStart} style={{
231
+ padding: '9px 20px', border: 'none', borderRadius: 'var(--radius)',
232
+ background: canStart ? 'var(--green)' : 'var(--bg-section)',
233
+ color: canStart ? '#fff' : 'var(--text-muted)',
234
+ fontSize: 13, fontWeight: 600, cursor: canStart ? 'pointer' : 'default',
235
+ whiteSpace: 'nowrap',
236
+ }}>
237
+ Analyze Project
238
+ </button>
239
+ ) : (
240
+ <button onClick={() => abortRef.current?.abort()} style={{
241
+ padding: '9px 20px', border: 'none', borderRadius: 'var(--radius)',
242
+ background: 'var(--red)', color: '#fff',
243
+ fontSize: 13, fontWeight: 600, cursor: 'pointer', whiteSpace: 'nowrap',
244
+ }}>
245
+ Stop
246
+ </button>
247
+ )}
142
248
  </div>
143
- {setupState === 'idle' || setupState === 'done' || setupState === 'error' ? (
144
- <button onClick={handleSetup} disabled={!projectPath.trim()} style={{
145
- padding: '9px 20px', border: 'none', borderRadius: 'var(--radius)',
146
- background: projectPath.trim() ? 'var(--green)' : 'var(--bg-section)',
147
- color: projectPath.trim() ? '#fff' : 'var(--text-muted)',
148
- fontSize: 13, fontWeight: 600, cursor: projectPath.trim() ? 'pointer' : 'default',
149
- whiteSpace: 'nowrap',
150
- }}>
151
- {setupState === 'done' ? 'Re-analyze' : 'Analyze Project'}
152
- </button>
153
- ) : (
154
- <button onClick={handleStop} style={{
155
- padding: '9px 20px', border: 'none', borderRadius: 'var(--radius)',
156
- background: 'var(--red)', color: '#fff',
157
- fontSize: 13, fontWeight: 600, cursor: 'pointer', whiteSpace: 'nowrap',
158
- }}>
159
- Stop
160
- </button>
161
- )}
162
- </div>
249
+ )}
250
+
251
+ {/* GitHub URL Input */}
252
+ {inputMode === 'github' && (
253
+ <div>
254
+ <div style={{ display: 'flex', gap: 10, alignItems: 'flex-end', marginBottom: 10 }}>
255
+ <div style={{ flex: 1 }}>
256
+ <label style={{ fontSize: 11, fontWeight: 600, color: 'var(--text-dim)', display: 'block', marginBottom: 5 }}>
257
+ GitHub repository URL
258
+ </label>
259
+ <input
260
+ value={githubUrl}
261
+ onChange={e => setGithubUrl(e.target.value)}
262
+ placeholder="https://github.com/user/repo"
263
+ disabled={setupState === 'analyzing'}
264
+ style={{
265
+ width: '100%', padding: '9px 14px',
266
+ border: '1px solid var(--border)', borderRadius: 'var(--radius)',
267
+ fontSize: 13, fontFamily: 'var(--font-mono)',
268
+ background: 'var(--bg-input)', color: 'var(--text)',
269
+ }}
270
+ />
271
+ </div>
272
+ </div>
273
+ <div style={{ display: 'flex', gap: 10, alignItems: 'flex-end' }}>
274
+ <div style={{ flex: 1 }}>
275
+ <label style={{ fontSize: 11, fontWeight: 600, color: 'var(--text-dim)', display: 'block', marginBottom: 5 }}>
276
+ Clone to directory
277
+ </label>
278
+ <input
279
+ value={cloneDir}
280
+ onChange={e => setCloneDir(e.target.value)}
281
+ placeholder="~/projects"
282
+ disabled={setupState === 'analyzing'}
283
+ style={{
284
+ width: '100%', padding: '9px 14px',
285
+ border: '1px solid var(--border)', borderRadius: 'var(--radius)',
286
+ fontSize: 13, fontFamily: 'var(--font-mono)',
287
+ background: 'var(--bg-input)', color: 'var(--text)',
288
+ }}
289
+ />
290
+ </div>
291
+ {setupState !== 'analyzing' ? (
292
+ <button onClick={handleSetup} disabled={!canStart} style={{
293
+ padding: '9px 20px', border: 'none', borderRadius: 'var(--radius)',
294
+ background: canStart ? 'var(--green)' : 'var(--bg-section)',
295
+ color: canStart ? '#fff' : 'var(--text-muted)',
296
+ fontSize: 13, fontWeight: 600, cursor: canStart ? 'pointer' : 'default',
297
+ whiteSpace: 'nowrap',
298
+ }}>
299
+ Clone &amp; Analyze
300
+ </button>
301
+ ) : (
302
+ <button onClick={() => abortRef.current?.abort()} style={{
303
+ padding: '9px 20px', border: 'none', borderRadius: 'var(--radius)',
304
+ background: 'var(--red)', color: '#fff',
305
+ fontSize: 13, fontWeight: 600, cursor: 'pointer', whiteSpace: 'nowrap',
306
+ }}>
307
+ Stop
308
+ </button>
309
+ )}
310
+ </div>
311
+ <div style={{ fontSize: 10, color: 'var(--text-muted)', marginTop: 6 }}>
312
+ The repository will be cloned to <code style={{ fontSize: 9 }}>{cloneDir}/{githubUrl.match(/\/([\w.-]+?)(?:\.git)?$/)?.[1] ?? 'repo'}</code>, then analyzed
313
+ </div>
314
+ </div>
315
+ )}
163
316
  </div>
164
317
 
165
318
  {/* Output */}
@@ -172,7 +325,7 @@ export default function ProjectsClient({ profiles, skillCount }: { profiles: Pro
172
325
  fontSize: 12, fontWeight: 600,
173
326
  color: setupState === 'done' ? 'var(--green)' : setupState === 'error' ? 'var(--red)' : 'var(--text-secondary)',
174
327
  }}>
175
- {setupState === 'analyzing' && '● Analyzing project and matching skills...'}
328
+ {setupState === 'analyzing' && '● Running project analysis...'}
176
329
  {setupState === 'done' && '✓ Analysis complete — project profile saved'}
177
330
  {setupState === 'error' && '✗ Analysis failed'}
178
331
  </div>
@@ -189,23 +342,39 @@ export default function ProjectsClient({ profiles, skillCount }: { profiles: Pro
189
342
  {setupState === 'done' && (
190
343
  <div style={{ padding: '12px 16px', borderTop: '1px solid var(--border)', display: 'flex', gap: 8 }}>
191
344
  <button onClick={() => window.location.reload()} style={{
192
- padding: '7px 14px', background: 'var(--bg-section)', border: '1px solid var(--border)',
193
- borderRadius: 'var(--radius)', fontSize: 12, fontWeight: 600, cursor: 'pointer', color: 'var(--text-secondary)',
345
+ padding: '7px 14px', background: 'var(--green)', color: '#fff', border: 'none',
346
+ borderRadius: 'var(--radius)', fontSize: 12, fontWeight: 600, cursor: 'pointer',
194
347
  }}>
195
348
  Refresh to see results
196
349
  </button>
350
+ <Link href="/network" style={{ textDecoration: 'none' }}>
351
+ <button style={{
352
+ padding: '7px 14px', background: 'var(--bg-section)', border: '1px solid var(--border)',
353
+ borderRadius: 'var(--radius)', fontSize: 12, fontWeight: 600, cursor: 'pointer', color: 'var(--text-secondary)',
354
+ }}>
355
+ View Skill Network
356
+ </button>
357
+ </Link>
358
+ <Link href="/research" style={{ textDecoration: 'none' }}>
359
+ <button style={{
360
+ padding: '7px 14px', background: 'var(--bg-section)', border: '1px solid var(--border)',
361
+ borderRadius: 'var(--radius)', fontSize: 12, fontWeight: 600, cursor: 'pointer', color: 'var(--text-secondary)',
362
+ }}>
363
+ Research Gaps
364
+ </button>
365
+ </Link>
197
366
  </div>
198
367
  )}
199
368
  </div>
200
369
  )}
201
370
 
202
- {/* Existing Project Profiles */}
371
+ {/* Existing Profiles */}
203
372
  {profiles.length > 0 && (
204
- <div style={{ marginBottom: 24 }}>
373
+ <div>
205
374
  <div style={{ fontSize: 10, fontWeight: 700, color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: 1.5, marginBottom: 12 }}>
206
375
  Analyzed Projects ({profiles.length})
207
376
  </div>
208
- <div style={{ display: 'grid', gap: 12 }}>
377
+ <div style={{ display: 'grid', gap: 14 }}>
209
378
  {profiles.map(p => {
210
379
  const highGaps = p.gaps.filter(g => g.priority === 'high')
211
380
  const matchedPct = skillCount > 0 ? Math.round((p.matchedSkills.length / skillCount) * 100) : 0
@@ -216,71 +385,117 @@ export default function ProjectsClient({ profiles, skillCount }: { profiles: Pro
216
385
  {/* Header */}
217
386
  <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 12 }}>
218
387
  <div>
219
- <div style={{ fontSize: 16, fontWeight: 700, marginBottom: 4 }}>{p.name}</div>
220
- <div style={{ fontSize: 12, color: 'var(--text-dim)' }}>{p.description}</div>
388
+ <div style={{ fontSize: 18, fontWeight: 700, marginBottom: 4 }}>{p.name}</div>
389
+ <div style={{ fontSize: 12, color: 'var(--text-dim)', marginBottom: 6 }}>{p.description}</div>
390
+ <div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
391
+ {p.techStack.map(t => <span key={t} className="badge badge-gray">{t}</span>)}
392
+ {p.domains.map(d => <span key={d} className="badge badge-blue">{d}</span>)}
393
+ </div>
394
+ </div>
395
+ <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-end', gap: 4 }}>
396
+ <span className={`badge ${p.status === 'active' ? 'badge-green' : 'badge-blue'}`}>{p.status}</span>
397
+ <span style={{ fontSize: 10, color: 'var(--text-muted)' }}>{new Date(p.analyzedAt).toLocaleDateString()}</span>
221
398
  </div>
222
- <span className={`badge ${p.status === 'active' ? 'badge-green' : 'badge-blue'}`}>{p.status}</span>
223
- </div>
224
-
225
- {/* Tech + Domains */}
226
- <div style={{ display: 'flex', gap: 6, flexWrap: 'wrap', marginBottom: 14 }}>
227
- {p.techStack.map(t => <span key={t} className="badge badge-gray">{t}</span>)}
228
- {p.domains.map(d => <span key={d} className="badge badge-blue">{d}</span>)}
229
399
  </div>
230
400
 
231
- {/* Stats row */}
232
- <div className="grid-3" style={{ gap: 10, marginBottom: 14 }}>
233
- <div style={{ padding: '10px 12px', background: 'var(--bg-section)', borderRadius: 'var(--radius)' }}>
234
- <div style={{ fontSize: 20, fontWeight: 800, color: 'var(--green)' }}>{p.matchedSkills.length}</div>
235
- <div style={{ fontSize: 10, color: 'var(--text-dim)' }}>Skills Matched ({matchedPct}%)</div>
401
+ {/* Stats */}
402
+ <div className="grid-3" style={{ gap: 10, marginBottom: 16 }}>
403
+ <div style={{ padding: '12px 14px', background: 'var(--bg-section)', borderRadius: 'var(--radius)' }}>
404
+ <div style={{ fontSize: 24, fontWeight: 800, color: 'var(--green)' }}>{p.matchedSkills.length}</div>
405
+ <div style={{ fontSize: 11, color: 'var(--text-dim)' }}>Skills matched ({matchedPct}%)</div>
236
406
  </div>
237
- <div style={{ padding: '10px 12px', background: 'var(--bg-section)', borderRadius: 'var(--radius)' }}>
238
- <div style={{ fontSize: 20, fontWeight: 800, color: highGaps.length > 0 ? 'var(--red)' : 'var(--green)' }}>{p.gaps.length}</div>
239
- <div style={{ fontSize: 10, color: 'var(--text-dim)' }}>Gaps ({highGaps.length} high priority)</div>
407
+ <div style={{ padding: '12px 14px', background: 'var(--bg-section)', borderRadius: 'var(--radius)' }}>
408
+ <div style={{ fontSize: 24, fontWeight: 800, color: highGaps.length > 0 ? 'var(--red)' : 'var(--green)' }}>{p.gaps.length}</div>
409
+ <div style={{ fontSize: 11, color: 'var(--text-dim)' }}>Gaps ({highGaps.length} high priority)</div>
240
410
  </div>
241
- <div style={{ padding: '10px 12px', background: 'var(--bg-section)', borderRadius: 'var(--radius)' }}>
242
- <div style={{ fontSize: 10, color: 'var(--text-dim)', marginBottom: 2 }}>Analyzed</div>
243
- <div style={{ fontSize: 12, fontWeight: 600, color: 'var(--text-secondary)' }}>{new Date(p.analyzedAt).toLocaleDateString()}</div>
411
+ <div style={{ padding: '12px 14px', background: 'var(--bg-section)', borderRadius: 'var(--radius)' }}>
412
+ <div style={{ fontSize: 11, color: 'var(--text-dim)', marginBottom: 4 }}>Path</div>
413
+ <div style={{ fontSize: 11, fontFamily: 'var(--font-mono)', color: 'var(--text-secondary)', wordBreak: 'break-all' }}>{p.path}</div>
244
414
  </div>
245
415
  </div>
246
416
 
247
- {/* Top matched skills */}
417
+ {/* Matched Skills */}
248
418
  {p.matchedSkills.length > 0 && (
249
419
  <div style={{ marginBottom: 14 }}>
250
- <div style={{ fontSize: 10, fontWeight: 600, color: 'var(--text-muted)', marginBottom: 6 }}>TOP MATCHES</div>
251
- <div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
252
- {p.matchedSkills.slice(0, 6).map(m => (
420
+ <div style={{ fontSize: 10, fontWeight: 600, color: 'var(--text-muted)', marginBottom: 6 }}>MATCHED SKILLS</div>
421
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
422
+ {p.matchedSkills.sort((a, b) => b.relevance - a.relevance).slice(0, 8).map(m => (
253
423
  <div key={m.slug} style={{
254
- padding: '4px 10px', background: 'var(--bg-section)',
255
- borderRadius: 'var(--radius)', fontSize: 11, display: 'flex', alignItems: 'center', gap: 6,
424
+ display: 'flex', alignItems: 'center', gap: 10,
425
+ padding: '6px 10px', background: 'var(--bg-section)', borderRadius: 'var(--radius)',
256
426
  }}>
257
- <span style={{ fontWeight: 500 }}>{m.slug}</span>
258
- <span style={{ fontWeight: 700, color: m.relevance >= 70 ? 'var(--green)' : 'var(--yellow)' }}>{m.relevance}%</span>
427
+ <Link href="/network" style={{ textDecoration: 'none', fontWeight: 600, fontSize: 12, color: 'var(--text)' }}>{m.slug}</Link>
428
+ <div style={{ flex: 1 }}>
429
+ <div className="score-track" style={{ height: 4 }}>
430
+ <div className="score-fill" style={{
431
+ width: `${m.relevance}%`,
432
+ background: m.relevance >= 70 ? 'var(--green)' : m.relevance >= 40 ? 'var(--yellow)' : 'var(--text-muted)',
433
+ }} />
434
+ </div>
435
+ </div>
436
+ <span style={{ fontSize: 12, fontWeight: 700, color: m.relevance >= 70 ? 'var(--green)' : 'var(--yellow)', minWidth: 32 }}>{m.relevance}%</span>
437
+ <span style={{ fontSize: 10, color: 'var(--text-dim)', maxWidth: 200, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{m.reason}</span>
259
438
  </div>
260
439
  ))}
261
- {p.matchedSkills.length > 6 && (
262
- <span style={{ fontSize: 10, color: 'var(--text-muted)', padding: '4px 6px' }}>+{p.matchedSkills.length - 6} more</span>
440
+ {p.matchedSkills.length > 8 && (
441
+ <div style={{ fontSize: 10, color: 'var(--text-muted)', paddingLeft: 10 }}>+{p.matchedSkills.length - 8} more skills</div>
263
442
  )}
264
443
  </div>
265
444
  </div>
266
445
  )}
267
446
 
268
- {/* Gaps */}
447
+ {/* Gaps with Action Buttons */}
269
448
  {p.gaps.length > 0 && (
270
- <div>
271
- <div style={{ fontSize: 10, fontWeight: 600, color: 'var(--text-muted)', marginBottom: 6 }}>GAPS</div>
272
- <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
449
+ <div style={{ marginBottom: 14 }}>
450
+ <div style={{ fontSize: 10, fontWeight: 600, color: 'var(--text-muted)', marginBottom: 6 }}>GAPS — CLICK TO FILL</div>
451
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
273
452
  {p.gaps.map((g, i) => (
274
453
  <div key={i} style={{
275
- padding: '6px 10px', borderRadius: 'var(--radius)',
454
+ padding: '10px 14px', borderRadius: 'var(--radius)',
276
455
  background: g.priority === 'high' ? 'var(--red-light)' : g.priority === 'medium' ? 'var(--yellow-light)' : 'var(--bg-section)',
277
456
  borderLeft: `3px solid ${g.priority === 'high' ? 'var(--red)' : g.priority === 'medium' ? 'var(--yellow)' : 'var(--text-muted)'}`,
278
- fontSize: 11, display: 'flex', justifyContent: 'space-between', alignItems: 'center',
279
457
  }}>
280
- <span><strong>{g.area}</strong> {g.description.slice(0, 60)}</span>
281
- <span className={`badge ${g.suggestedAction === 'research' ? 'badge-blue' : g.suggestedAction === 'specialize' ? 'badge-green' : 'badge-gray'}`}>
282
- {g.suggestedAction}
283
- </span>
458
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 4 }}>
459
+ <div>
460
+ <span style={{ fontSize: 13, fontWeight: 700, color: 'var(--text)' }}>{g.area}</span>
461
+ <span className={`badge ${g.priority === 'high' ? 'badge-red' : g.priority === 'medium' ? 'badge-yellow' : 'badge-gray'}`} style={{ marginLeft: 6 }}>{g.priority}</span>
462
+ </div>
463
+ {g.suggestedAction === 'research' && (
464
+ <Link href="/research" style={{ textDecoration: 'none' }}>
465
+ <button style={{
466
+ padding: '4px 12px', fontSize: 10, fontWeight: 600,
467
+ background: 'var(--blue)', color: '#fff', border: 'none',
468
+ borderRadius: 'var(--radius)', cursor: 'pointer',
469
+ }}>
470
+ Research this gap
471
+ </button>
472
+ </Link>
473
+ )}
474
+ {g.suggestedAction === 'specialize' && (
475
+ <button style={{
476
+ padding: '4px 12px', fontSize: 10, fontWeight: 600,
477
+ background: 'var(--green)', color: '#fff', border: 'none',
478
+ borderRadius: 'var(--radius)', cursor: 'pointer',
479
+ }}>
480
+ Specialize a skill
481
+ </button>
482
+ )}
483
+ {g.suggestedAction === 'create' && (
484
+ <Link href="/network" style={{ textDecoration: 'none' }}>
485
+ <button style={{
486
+ padding: '4px 12px', fontSize: 10, fontWeight: 600,
487
+ background: 'var(--bg-section)', color: 'var(--text-secondary)',
488
+ border: '1px solid var(--border)',
489
+ borderRadius: 'var(--radius)', cursor: 'pointer',
490
+ }}>
491
+ Create manually
492
+ </button>
493
+ </Link>
494
+ )}
495
+ </div>
496
+ <div style={{ fontSize: 11, color: 'var(--text-dim)', lineHeight: 1.5 }}>
497
+ {g.description}
498
+ </div>
284
499
  </div>
285
500
  ))}
286
501
  </div>
@@ -289,13 +504,33 @@ export default function ProjectsClient({ profiles, skillCount }: { profiles: Pro
289
504
 
290
505
  {/* Recommendations */}
291
506
  {p.recommendations.length > 0 && (
292
- <div style={{ marginTop: 12, padding: '10px 14px', background: 'var(--bg-section)', borderRadius: 'var(--radius)' }}>
293
- <div style={{ fontSize: 10, fontWeight: 600, color: 'var(--text-muted)', marginBottom: 4 }}>NEXT STEPS</div>
507
+ <div style={{ padding: '10px 14px', background: 'var(--bg-section)', borderRadius: 'var(--radius)' }}>
508
+ <div style={{ fontSize: 10, fontWeight: 600, color: 'var(--text-muted)', marginBottom: 4 }}>RECOMMENDED NEXT STEPS</div>
294
509
  {p.recommendations.map((r, i) => (
295
- <div key={i} style={{ fontSize: 11, color: 'var(--text-dim)', lineHeight: 1.5 }}>→ {r}</div>
510
+ <div key={i} style={{ fontSize: 11, color: 'var(--text-dim)', lineHeight: 1.6 }}>→ {r}</div>
296
511
  ))}
297
512
  </div>
298
513
  )}
514
+
515
+ {/* Re-analyze button */}
516
+ <div style={{ marginTop: 12, display: 'flex', gap: 8 }}>
517
+ <button onClick={() => { setProjectPath(p.path); setInputMode('local'); window.scrollTo(0, 0) }} style={{
518
+ padding: '6px 12px', fontSize: 11, fontWeight: 600,
519
+ background: 'var(--bg-section)', border: '1px solid var(--border)',
520
+ borderRadius: 'var(--radius)', cursor: 'pointer', color: 'var(--text-secondary)',
521
+ }}>
522
+ Re-analyze
523
+ </button>
524
+ <Link href="/network" style={{ textDecoration: 'none' }}>
525
+ <button style={{
526
+ padding: '6px 12px', fontSize: 11, fontWeight: 600,
527
+ background: 'var(--bg-section)', border: '1px solid var(--border)',
528
+ borderRadius: 'var(--radius)', cursor: 'pointer', color: 'var(--text-secondary)',
529
+ }}>
530
+ View in Skill Network
531
+ </button>
532
+ </Link>
533
+ </div>
299
534
  </div>
300
535
  </div>
301
536
  )
@@ -315,8 +550,8 @@ export default function ProjectsClient({ profiles, skillCount }: { profiles: Pro
315
550
  </div>
316
551
  <div className="empty-state-title">No projects analyzed yet</div>
317
552
  <div className="empty-state-desc">
318
- Enter a project folder path above to analyze it against your {skillCount} skills.
319
- HelixEvo will identify which skills match and what gaps need filling.
553
+ Enter a project folder path or GitHub URL above. HelixEvo will analyze it against your {skillCount} skills,
554
+ identify which ones match, and tell you what gaps need filling.
320
555
  </div>
321
556
  </div>
322
557
  </div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "helixevo",
3
- "version": "0.2.35",
3
+ "version": "0.2.36",
4
4
  "description": "Self-evolving skill ecosystem for AI agents. Skills and projects co-evolve through multi-judge evaluation and a Pareto frontier.",
5
5
  "type": "module",
6
6
  "bin": {