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 +12 -0
- package/dashboard/app/page.tsx +12 -0
- package/dashboard/app/projects/client.tsx +349 -114
- package/package.json +1 -1
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
|
package/dashboard/app/page.tsx
CHANGED
|
@@ -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)' }}>→</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
|
|
9
|
-
{
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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 (!
|
|
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
|
|
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
|
|
137
|
+
Analyze any project to match your {skillCount} skills, identify gaps, and prepare for work
|
|
94
138
|
</p>
|
|
95
139
|
</div>
|
|
96
140
|
|
|
97
|
-
{/*
|
|
141
|
+
{/* Pipeline Visualization */}
|
|
98
142
|
<div className="card" style={{ marginBottom: 24, padding: '22px 26px' }}>
|
|
99
|
-
{
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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:
|
|
150
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}>
|
|
105
151
|
<div style={{
|
|
106
|
-
width:
|
|
107
|
-
background:
|
|
108
|
-
|
|
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:
|
|
112
|
-
<div style={{ fontSize:
|
|
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.
|
|
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: '
|
|
170
|
+
<span style={{ padding: '12px 6px 0', color: 'var(--text-muted)', fontSize: 14 }}>→</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
|
-
|
|
126
|
-
<div style={{ flex:
|
|
127
|
-
<
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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 & 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' && '●
|
|
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(--
|
|
193
|
-
borderRadius: 'var(--radius)', fontSize: 12, fontWeight: 600, cursor: 'pointer',
|
|
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
|
|
371
|
+
{/* Existing Profiles */}
|
|
203
372
|
{profiles.length > 0 && (
|
|
204
|
-
<div
|
|
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:
|
|
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:
|
|
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
|
|
232
|
-
<div className="grid-3" style={{ gap: 10, marginBottom:
|
|
233
|
-
<div style={{ padding: '
|
|
234
|
-
<div style={{ fontSize:
|
|
235
|
-
<div style={{ fontSize:
|
|
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: '
|
|
238
|
-
<div style={{ fontSize:
|
|
239
|
-
<div style={{ fontSize:
|
|
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: '
|
|
242
|
-
<div style={{ fontSize:
|
|
243
|
-
<div style={{ fontSize:
|
|
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
|
-
{/*
|
|
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 }}>
|
|
251
|
-
<div style={{ display: 'flex',
|
|
252
|
-
{p.matchedSkills.slice(0,
|
|
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
|
-
|
|
255
|
-
|
|
424
|
+
display: 'flex', alignItems: 'center', gap: 10,
|
|
425
|
+
padding: '6px 10px', background: 'var(--bg-section)', borderRadius: 'var(--radius)',
|
|
256
426
|
}}>
|
|
257
|
-
<
|
|
258
|
-
<
|
|
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 >
|
|
262
|
-
<
|
|
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:
|
|
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: '
|
|
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
|
-
<
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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={{
|
|
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.
|
|
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
|
|
319
|
-
|
|
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