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.
- package/CHANGELOG.md +31 -0
- package/dashboard/app/api/browse/route.ts +66 -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 +923 -400
- 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
|
@@ -1,35 +1,124 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { useMemo, useRef, useState, type CSSProperties } from 'react'
|
|
4
4
|
import Link from 'next/link'
|
|
5
|
+
import { ConsolePanel } from '@/components/console-panel'
|
|
6
|
+
import { MetricCard } from '@/components/metric-card'
|
|
7
|
+
import { PageHero } from '@/components/page-hero'
|
|
8
|
+
import { SectionFrame } from '@/components/section-frame'
|
|
5
9
|
import type { ProjectProfile } from '@/lib/data'
|
|
6
10
|
|
|
7
|
-
type SetupState = 'idle' | 'analyzing' | 'done' | 'error'
|
|
11
|
+
type SetupState = 'idle' | 'analyzing' | 'done' | 'error' | 'cancelled'
|
|
8
12
|
type InputMode = 'local' | 'github'
|
|
13
|
+
type StatusTone = 'neutral' | 'green' | 'red' | 'yellow' | 'blue' | 'purple'
|
|
9
14
|
|
|
10
15
|
const PIPELINE = [
|
|
11
16
|
{
|
|
12
|
-
step: 1,
|
|
13
|
-
|
|
14
|
-
|
|
17
|
+
step: 1,
|
|
18
|
+
title: 'Analyze structure',
|
|
19
|
+
command: 'helixevo project-setup <path>',
|
|
20
|
+
desc: 'Read README, package manifests, config files, and the source tree to understand what the project is and how it is built.',
|
|
21
|
+
tone: 'blue' as const,
|
|
15
22
|
},
|
|
16
23
|
{
|
|
17
|
-
step: 2,
|
|
18
|
-
|
|
19
|
-
|
|
24
|
+
step: 2,
|
|
25
|
+
title: 'Match capabilities',
|
|
26
|
+
command: 'match existing skills',
|
|
27
|
+
desc: 'Score your current skill network against the project so only relevant skills remain in the working set.',
|
|
28
|
+
tone: 'purple' as const,
|
|
20
29
|
},
|
|
21
30
|
{
|
|
22
|
-
step: 3,
|
|
23
|
-
|
|
24
|
-
|
|
31
|
+
step: 3,
|
|
32
|
+
title: 'Expose gaps',
|
|
33
|
+
command: 'detect uncovered needs',
|
|
34
|
+
desc: 'Surface capabilities the project requires but your current skills do not yet cover, with clear priority levels.',
|
|
35
|
+
tone: 'yellow' as const,
|
|
25
36
|
},
|
|
26
37
|
{
|
|
27
|
-
step: 4,
|
|
28
|
-
|
|
29
|
-
|
|
38
|
+
step: 4,
|
|
39
|
+
title: 'Route next action',
|
|
40
|
+
command: 'research β’ specialize β’ create',
|
|
41
|
+
desc: 'Turn each gap into the next best move so you can research, specialize a skill, or create a new one with intent.',
|
|
42
|
+
tone: 'green' as const,
|
|
30
43
|
},
|
|
31
44
|
]
|
|
32
45
|
|
|
46
|
+
const DEFAULT_PATHS = ['.', '~/Documents/GitHub', '~/Desktop', '~/HelixEvo']
|
|
47
|
+
|
|
48
|
+
interface BrowseItem {
|
|
49
|
+
name: string
|
|
50
|
+
isDirectory: boolean
|
|
51
|
+
isProject: boolean
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface BrowseResult {
|
|
55
|
+
path: string
|
|
56
|
+
displayPath: string
|
|
57
|
+
parent: string | null
|
|
58
|
+
items: BrowseItem[]
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function buttonStyle(tone: StatusTone = 'neutral', filled = false): CSSProperties {
|
|
62
|
+
const tones: Record<StatusTone, { border: string; bg: string; text: string; strong: string }> = {
|
|
63
|
+
neutral: { border: 'var(--border)', bg: 'rgba(97,93,86,0.08)', text: 'var(--text-secondary)', strong: 'var(--text)' },
|
|
64
|
+
green: { border: 'var(--green-border)', bg: 'var(--green-light)', text: 'var(--green)', strong: 'var(--green)' },
|
|
65
|
+
red: { border: 'var(--red-border)', bg: 'var(--red-light)', text: 'var(--red)', strong: 'var(--red)' },
|
|
66
|
+
yellow: { border: 'var(--yellow-border)', bg: 'var(--yellow-light)', text: 'var(--yellow)', strong: 'var(--yellow)' },
|
|
67
|
+
blue: { border: 'rgba(59,130,246,0.24)', bg: 'rgba(59,130,246,0.12)', text: 'var(--blue)', strong: 'var(--blue)' },
|
|
68
|
+
purple: { border: 'rgba(107,73,223,0.24)', bg: 'rgba(107,73,223,0.12)', text: 'var(--purple)', strong: 'var(--purple)' },
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const palette = tones[tone]
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
display: 'inline-flex',
|
|
75
|
+
alignItems: 'center',
|
|
76
|
+
justifyContent: 'center',
|
|
77
|
+
gap: 8,
|
|
78
|
+
padding: '9px 14px',
|
|
79
|
+
borderRadius: 999,
|
|
80
|
+
border: filled ? 'none' : `1px solid ${palette.border}`,
|
|
81
|
+
background: filled ? palette.strong : palette.bg,
|
|
82
|
+
color: filled ? '#fff' : palette.text,
|
|
83
|
+
fontSize: 12,
|
|
84
|
+
fontWeight: 700,
|
|
85
|
+
textDecoration: 'none',
|
|
86
|
+
cursor: 'pointer',
|
|
87
|
+
whiteSpace: 'nowrap',
|
|
88
|
+
boxShadow: filled ? 'var(--shadow-sm)' : 'none',
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function statusTone(state: SetupState): StatusTone {
|
|
93
|
+
if (state === 'done') return 'green'
|
|
94
|
+
if (state === 'error') return 'red'
|
|
95
|
+
if (state === 'cancelled') return 'yellow'
|
|
96
|
+
if (state === 'analyzing') return 'blue'
|
|
97
|
+
return 'neutral'
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function statusLabel(state: SetupState) {
|
|
101
|
+
if (state === 'analyzing') return 'Running project analysisβ¦'
|
|
102
|
+
if (state === 'done') return 'Analysis complete β project profile saved'
|
|
103
|
+
if (state === 'error') return 'Analysis failed'
|
|
104
|
+
if (state === 'cancelled') return 'Analysis cancelled'
|
|
105
|
+
return 'Awaiting input'
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function priorityTone(priority: 'high' | 'medium' | 'low'): StatusTone {
|
|
109
|
+
if (priority === 'high') return 'red'
|
|
110
|
+
if (priority === 'medium') return 'yellow'
|
|
111
|
+
return 'neutral'
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function uniquePaths(paths: string[]) {
|
|
115
|
+
return Array.from(new Set(paths.filter(Boolean)))
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function formatDate(value: string) {
|
|
119
|
+
return new Date(value).toLocaleDateString()
|
|
120
|
+
}
|
|
121
|
+
|
|
33
122
|
export default function ProjectsClient({ profiles, skillCount }: { profiles: ProjectProfile[]; skillCount: number }) {
|
|
34
123
|
const [inputMode, setInputMode] = useState<InputMode>('local')
|
|
35
124
|
const [projectPath, setProjectPath] = useState('')
|
|
@@ -37,41 +126,110 @@ export default function ProjectsClient({ profiles, skillCount }: { profiles: Pro
|
|
|
37
126
|
const [cloneDir, setCloneDir] = useState('~/HelixEvo')
|
|
38
127
|
const [setupState, setSetupState] = useState<SetupState>('idle')
|
|
39
128
|
const [output, setOutput] = useState('')
|
|
129
|
+
const [projectActionMessage, setProjectActionMessage] = useState<{ tone: StatusTone; text: string } | null>(null)
|
|
130
|
+
const [specializingProject, setSpecializingProject] = useState<string | null>(null)
|
|
40
131
|
const outputRef = useRef<HTMLPreElement | null>(null)
|
|
41
132
|
const abortRef = useRef<AbortController | null>(null)
|
|
42
133
|
|
|
43
|
-
const
|
|
134
|
+
const [showBrowser, setShowBrowser] = useState(false)
|
|
135
|
+
const [browseData, setBrowseData] = useState<BrowseResult | null>(null)
|
|
136
|
+
const [browseLoading, setBrowseLoading] = useState(false)
|
|
137
|
+
|
|
138
|
+
const quickPaths = useMemo(() => uniquePaths([...DEFAULT_PATHS, ...profiles.map(profile => profile.path)]), [profiles])
|
|
139
|
+
|
|
140
|
+
const aggregate = useMemo(() => {
|
|
141
|
+
const activeProjects = profiles.filter(profile => profile.status === 'active').length
|
|
142
|
+
const totalHighGaps = profiles.reduce((sum, profile) => sum + profile.gaps.filter(gap => gap.priority === 'high').length, 0)
|
|
143
|
+
const totalRecommendations = profiles.reduce((sum, profile) => sum + profile.recommendations.length, 0)
|
|
144
|
+
const avgCoverage = profiles.length > 0 && skillCount > 0
|
|
145
|
+
? Math.round(profiles.reduce((sum, profile) => sum + ((profile.matchedSkills.length / skillCount) * 100), 0) / profiles.length)
|
|
146
|
+
: 0
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
activeProjects,
|
|
150
|
+
totalHighGaps,
|
|
151
|
+
totalRecommendations,
|
|
152
|
+
avgCoverage,
|
|
153
|
+
}
|
|
154
|
+
}, [profiles, skillCount])
|
|
155
|
+
|
|
156
|
+
const repoName = githubUrl.match(/github\.com\/[\w-]+\/([\w.-]+)/)?.[1]?.replace(/\.git$/, '') ?? 'repo'
|
|
157
|
+
const cloneTarget = `${cloneDir}/${repoName}`
|
|
44
158
|
const canStart = inputMode === 'local' ? !!projectPath.trim() : !!githubUrl.trim()
|
|
45
159
|
|
|
160
|
+
const browseTo = async (dir: string) => {
|
|
161
|
+
setBrowseLoading(true)
|
|
162
|
+
try {
|
|
163
|
+
const res = await fetch(`/api/browse?dir=${encodeURIComponent(dir)}`)
|
|
164
|
+
const data = await res.json() as BrowseResult & { error?: string }
|
|
165
|
+
if (!data.error) setBrowseData(data)
|
|
166
|
+
} catch {
|
|
167
|
+
setProjectActionMessage({ tone: 'red', text: 'Could not load folder browser contents.' })
|
|
168
|
+
} finally {
|
|
169
|
+
setBrowseLoading(false)
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const openBrowser = () => {
|
|
174
|
+
setShowBrowser(true)
|
|
175
|
+
void browseTo('~')
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const selectFolder = (path: string) => {
|
|
179
|
+
setProjectPath(path)
|
|
180
|
+
setShowBrowser(false)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const triggerSpecialize = async (projectName: string) => {
|
|
184
|
+
setSpecializingProject(projectName)
|
|
185
|
+
setProjectActionMessage({ tone: 'blue', text: `Requesting specialization workflow for ${projectName}β¦` })
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
const res = await fetch('/api/run', {
|
|
189
|
+
method: 'POST',
|
|
190
|
+
headers: { 'Content-Type': 'application/json' },
|
|
191
|
+
body: JSON.stringify({ command: 'specialize', project: projectName }),
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
if (!res.ok) throw new Error('Specialize request failed')
|
|
195
|
+
setProjectActionMessage({ tone: 'green', text: `Specialization workflow requested for ${projectName}.` })
|
|
196
|
+
} catch {
|
|
197
|
+
setProjectActionMessage({ tone: 'red', text: `Could not start specialization for ${projectName}.` })
|
|
198
|
+
} finally {
|
|
199
|
+
setSpecializingProject(null)
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
46
203
|
const handleSetup = async () => {
|
|
47
204
|
if (!canStart) return
|
|
48
205
|
|
|
206
|
+
setProjectActionMessage(null)
|
|
49
207
|
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
208
|
|
|
56
|
-
|
|
209
|
+
if (inputMode === 'github') {
|
|
210
|
+
path = cloneTarget
|
|
57
211
|
setSetupState('analyzing')
|
|
58
|
-
setOutput(`
|
|
212
|
+
setOutput(`Repository source: ${githubUrl}\nClone target: ${path}\n\n`)
|
|
213
|
+
|
|
59
214
|
try {
|
|
60
215
|
const cloneRes = await fetch('/api/run', {
|
|
61
216
|
method: 'POST',
|
|
62
217
|
headers: { 'Content-Type': 'application/json' },
|
|
63
218
|
body: JSON.stringify({ command: 'clone', url: githubUrl, dir: path }),
|
|
64
219
|
})
|
|
65
|
-
|
|
66
|
-
setOutput(prev => prev +
|
|
67
|
-
|
|
220
|
+
|
|
221
|
+
setOutput(prev => prev + (cloneRes.ok
|
|
222
|
+
? 'Clone request sent successfully. Continuing into project analysisβ¦\n\n'
|
|
223
|
+
: 'Clone request returned a non-success status. Continuing into project analysisβ¦\n\n'))
|
|
224
|
+
} catch {
|
|
225
|
+
setOutput(prev => prev + 'Clone request could not be confirmed. Continuing into project analysisβ¦\n\n')
|
|
226
|
+
}
|
|
68
227
|
} else {
|
|
69
228
|
path = projectPath.trim()
|
|
229
|
+
setSetupState('analyzing')
|
|
230
|
+
setOutput(`Project source: ${path}\n\nLaunching project analysisβ¦\n\n`)
|
|
70
231
|
}
|
|
71
232
|
|
|
72
|
-
setSetupState('analyzing')
|
|
73
|
-
setOutput('')
|
|
74
|
-
|
|
75
233
|
const controller = new AbortController()
|
|
76
234
|
abortRef.current = controller
|
|
77
235
|
|
|
@@ -83,47 +241,67 @@ export default function ProjectsClient({ profiles, skillCount }: { profiles: Pro
|
|
|
83
241
|
signal: controller.signal,
|
|
84
242
|
})
|
|
85
243
|
|
|
86
|
-
if (!res.body) {
|
|
244
|
+
if (!res.body) {
|
|
245
|
+
setOutput(prev => prev ? `${prev}\nNo response received from analysis endpoint.` : 'No response received from analysis endpoint.')
|
|
246
|
+
setSetupState('error')
|
|
247
|
+
return
|
|
248
|
+
}
|
|
87
249
|
|
|
88
250
|
const reader = res.body.getReader()
|
|
89
251
|
const decoder = new TextDecoder()
|
|
90
252
|
let sseBuffer = ''
|
|
253
|
+
let sawDoneEvent = false
|
|
91
254
|
|
|
92
255
|
while (true) {
|
|
93
256
|
const { done, value } = await reader.read()
|
|
94
257
|
if (done) break
|
|
258
|
+
|
|
95
259
|
sseBuffer += decoder.decode(value, { stream: true })
|
|
96
260
|
const events = sseBuffer.split('\n\n')
|
|
97
261
|
sseBuffer = events.pop() ?? ''
|
|
262
|
+
|
|
98
263
|
for (const event of events) {
|
|
99
264
|
const lines = event.split('\n')
|
|
100
|
-
let eventType = ''
|
|
265
|
+
let eventType = ''
|
|
266
|
+
let eventData = ''
|
|
267
|
+
|
|
101
268
|
for (const line of lines) {
|
|
102
269
|
if (line.startsWith('event: ')) eventType = line.slice(7)
|
|
103
270
|
if (line.startsWith('data: ')) eventData = line.slice(6)
|
|
104
271
|
}
|
|
272
|
+
|
|
105
273
|
if (eventType === 'output' && eventData) {
|
|
106
274
|
try {
|
|
107
275
|
setOutput(prev => prev + (JSON.parse(eventData) as string))
|
|
108
|
-
setTimeout(() => {
|
|
109
|
-
|
|
276
|
+
setTimeout(() => {
|
|
277
|
+
if (outputRef.current) outputRef.current.scrollTop = outputRef.current.scrollHeight
|
|
278
|
+
}, 10)
|
|
279
|
+
} catch {
|
|
280
|
+
// ignore malformed output events
|
|
281
|
+
}
|
|
110
282
|
}
|
|
283
|
+
|
|
111
284
|
if (eventType === 'done' && eventData) {
|
|
285
|
+
sawDoneEvent = true
|
|
112
286
|
try {
|
|
113
|
-
const
|
|
114
|
-
setSetupState(
|
|
115
|
-
} catch {
|
|
287
|
+
const result = JSON.parse(JSON.parse(eventData) as string) as { success: boolean }
|
|
288
|
+
setSetupState(result.success ? 'done' : 'error')
|
|
289
|
+
} catch {
|
|
290
|
+
setSetupState('done')
|
|
291
|
+
}
|
|
116
292
|
}
|
|
117
293
|
}
|
|
118
294
|
}
|
|
119
|
-
|
|
295
|
+
|
|
296
|
+
if (!sawDoneEvent) setSetupState('done')
|
|
120
297
|
} catch (err: unknown) {
|
|
121
298
|
if (err instanceof Error && err.name === 'AbortError') {
|
|
122
|
-
setOutput(prev => prev + '\n\n[
|
|
299
|
+
setOutput(prev => prev + '\n\n[Analysis cancelled by user]')
|
|
300
|
+
setSetupState('cancelled')
|
|
123
301
|
} else {
|
|
124
302
|
setOutput(prev => prev || 'Network error')
|
|
303
|
+
setSetupState('error')
|
|
125
304
|
}
|
|
126
|
-
setSetupState('error')
|
|
127
305
|
} finally {
|
|
128
306
|
abortRef.current = null
|
|
129
307
|
}
|
|
@@ -131,439 +309,784 @@ export default function ProjectsClient({ profiles, skillCount }: { profiles: Pro
|
|
|
131
309
|
|
|
132
310
|
return (
|
|
133
311
|
<div>
|
|
134
|
-
<
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
<div
|
|
150
|
-
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}>
|
|
151
|
-
<div style={{
|
|
152
|
-
width: 32, height: 32, borderRadius: 9,
|
|
153
|
-
background: `${step.color}15`,
|
|
154
|
-
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
155
|
-
fontSize: 14, flexShrink: 0,
|
|
156
|
-
}}>{step.icon}</div>
|
|
157
|
-
<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>
|
|
160
|
-
</div>
|
|
161
|
-
</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>
|
|
168
|
-
</div>
|
|
169
|
-
{i < 3 && (
|
|
170
|
-
<span style={{ padding: '12px 6px 0', color: 'var(--text-muted)', fontSize: 14 }}>→</span>
|
|
171
|
-
)}
|
|
312
|
+
<PageHero
|
|
313
|
+
eyebrow="Project orchestration"
|
|
314
|
+
title="Project Setup"
|
|
315
|
+
description={`Analyze local folders or GitHub repositories, map them against your ${skillCount} skills, and turn uncovered capability gaps into an execution plan.`}
|
|
316
|
+
chips={[
|
|
317
|
+
{ label: `${profiles.length} analyzed project${profiles.length === 1 ? '' : 's'}`, tone: 'blue' },
|
|
318
|
+
{ label: `${aggregate.activeProjects} active`, tone: 'green' },
|
|
319
|
+
{ label: `${aggregate.totalHighGaps} high-priority gap${aggregate.totalHighGaps === 1 ? '' : 's'}`, tone: aggregate.totalHighGaps > 0 ? 'red' : 'neutral' },
|
|
320
|
+
{ label: `${aggregate.avgCoverage}% avg coverage`, tone: 'purple' },
|
|
321
|
+
]}
|
|
322
|
+
actions={
|
|
323
|
+
<div style={{ display: 'grid', gap: 12 }}>
|
|
324
|
+
<div className="hero-note-card">
|
|
325
|
+
<div className="hero-note-label">Current routing</div>
|
|
326
|
+
<div className="hero-note-title">Analyze β Match β Gap scan β Action</div>
|
|
327
|
+
<div className="hero-note-copy">Run a profile, review the skill fit, then send uncovered needs into Research or the Skill Network.</div>
|
|
172
328
|
</div>
|
|
173
|
-
|
|
174
|
-
|
|
329
|
+
<div style={{ display: 'flex', gap: 10, flexWrap: 'wrap', justifyContent: 'flex-end' }}>
|
|
330
|
+
<Link href="/network" style={buttonStyle('purple')}>
|
|
331
|
+
Open Skill Network
|
|
332
|
+
</Link>
|
|
333
|
+
<Link href="/research" style={buttonStyle('blue')}>
|
|
334
|
+
Open Research
|
|
335
|
+
</Link>
|
|
336
|
+
</div>
|
|
337
|
+
</div>
|
|
338
|
+
}
|
|
339
|
+
/>
|
|
340
|
+
|
|
341
|
+
<div className="grid-4" style={{ marginBottom: 24 }}>
|
|
342
|
+
<MetricCard
|
|
343
|
+
label="Portfolio"
|
|
344
|
+
value={profiles.length}
|
|
345
|
+
sublabel={`${aggregate.activeProjects} active, ${Math.max(0, profiles.length - aggregate.activeProjects)} archived/analyzed`}
|
|
346
|
+
tone="blue"
|
|
347
|
+
/>
|
|
348
|
+
<MetricCard
|
|
349
|
+
label="Coverage"
|
|
350
|
+
value={`${aggregate.avgCoverage}%`}
|
|
351
|
+
sublabel={`Average matched-skill coverage across ${profiles.length || 1} project${profiles.length === 1 ? '' : 's'}`}
|
|
352
|
+
tone="purple"
|
|
353
|
+
/>
|
|
354
|
+
<MetricCard
|
|
355
|
+
label="High-priority gaps"
|
|
356
|
+
value={aggregate.totalHighGaps}
|
|
357
|
+
sublabel={aggregate.totalHighGaps > 0 ? 'Capability needs requiring near-term attention' : 'No critical gaps recorded right now'}
|
|
358
|
+
tone={aggregate.totalHighGaps > 0 ? 'red' : 'green'}
|
|
359
|
+
/>
|
|
360
|
+
<MetricCard
|
|
361
|
+
label="Recommended next steps"
|
|
362
|
+
value={aggregate.totalRecommendations}
|
|
363
|
+
sublabel="Follow-on actions generated from prior project analysis runs"
|
|
364
|
+
tone="yellow"
|
|
365
|
+
/>
|
|
175
366
|
</div>
|
|
176
367
|
|
|
177
|
-
{
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
>
|
|
202
|
-
GitHub URL
|
|
203
|
-
</button>
|
|
368
|
+
{projectActionMessage ? (
|
|
369
|
+
<div style={{
|
|
370
|
+
marginBottom: 20,
|
|
371
|
+
padding: '12px 16px',
|
|
372
|
+
borderRadius: 18,
|
|
373
|
+
border: `1px solid ${buttonStyle(projectActionMessage.tone).border ?? 'var(--border)'}`,
|
|
374
|
+
background: projectActionMessage.tone === 'green'
|
|
375
|
+
? 'var(--green-light)'
|
|
376
|
+
: projectActionMessage.tone === 'red'
|
|
377
|
+
? 'var(--red-light)'
|
|
378
|
+
: projectActionMessage.tone === 'blue'
|
|
379
|
+
? 'rgba(59,130,246,0.12)'
|
|
380
|
+
: 'var(--bg-card)',
|
|
381
|
+
color: projectActionMessage.tone === 'green'
|
|
382
|
+
? 'var(--green)'
|
|
383
|
+
: projectActionMessage.tone === 'red'
|
|
384
|
+
? 'var(--red)'
|
|
385
|
+
: projectActionMessage.tone === 'blue'
|
|
386
|
+
? 'var(--blue)'
|
|
387
|
+
: 'var(--text-secondary)',
|
|
388
|
+
fontSize: 12.5,
|
|
389
|
+
fontWeight: 600,
|
|
390
|
+
}}>
|
|
391
|
+
{projectActionMessage.text}
|
|
204
392
|
</div>
|
|
393
|
+
) : null}
|
|
205
394
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
disabled={setupState === 'analyzing'}
|
|
395
|
+
<div className="grid-2" style={{ alignItems: 'start', marginBottom: 24 }}>
|
|
396
|
+
<SectionFrame
|
|
397
|
+
eyebrow="Workspace intake"
|
|
398
|
+
title="Project intake studio"
|
|
399
|
+
description="Choose a local folder or GitHub repository, then run a profile pass that streams live analysis output below."
|
|
400
|
+
tone="purple"
|
|
401
|
+
>
|
|
402
|
+
<div style={{ display: 'grid', gap: 18 }}>
|
|
403
|
+
<div style={{ display: 'inline-flex', padding: 4, borderRadius: 999, background: 'rgba(97,93,86,0.08)', border: '1px solid var(--border)', width: 'fit-content' }}>
|
|
404
|
+
<button
|
|
405
|
+
onClick={() => setInputMode('local')}
|
|
218
406
|
style={{
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
background: 'var(--bg-input)', color: 'var(--text)',
|
|
407
|
+
...buttonStyle(inputMode === 'local' ? 'purple' : 'neutral', inputMode === 'local'),
|
|
408
|
+
padding: '9px 16px',
|
|
409
|
+
minWidth: 130,
|
|
223
410
|
}}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
<span>Quick select:</span>
|
|
227
|
-
{['.', '~/Documents/GitHub', '~/Desktop', '~/HelixEvo', ...profiles.map(p => p.path).filter(p => !['.', '~/Documents/GitHub', '~/Desktop', '~/HelixEvo'].includes(p))].map(p => (
|
|
228
|
-
<button key={p} onClick={() => setProjectPath(p)} style={{
|
|
229
|
-
padding: '1px 8px', fontSize: 10, background: projectPath === p ? 'var(--green-light)' : 'var(--bg-section)',
|
|
230
|
-
border: `1px solid ${projectPath === p ? 'var(--green-border)' : 'var(--border-subtle)'}`,
|
|
231
|
-
borderRadius: 4, cursor: 'pointer', color: projectPath === p ? 'var(--green)' : 'var(--text-dim)',
|
|
232
|
-
fontFamily: 'var(--font-mono)',
|
|
233
|
-
}}>{p}</button>
|
|
234
|
-
))}
|
|
235
|
-
</div>
|
|
236
|
-
</div>
|
|
237
|
-
{setupState !== 'analyzing' ? (
|
|
238
|
-
<button onClick={handleSetup} disabled={!canStart} style={{
|
|
239
|
-
padding: '9px 20px', border: 'none', borderRadius: 'var(--radius)',
|
|
240
|
-
background: canStart ? 'var(--green)' : 'var(--bg-section)',
|
|
241
|
-
color: canStart ? '#fff' : 'var(--text-muted)',
|
|
242
|
-
fontSize: 13, fontWeight: 600, cursor: canStart ? 'pointer' : 'default',
|
|
243
|
-
whiteSpace: 'nowrap',
|
|
244
|
-
}}>
|
|
245
|
-
Analyze Project
|
|
411
|
+
>
|
|
412
|
+
Local folder
|
|
246
413
|
</button>
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
414
|
+
<button
|
|
415
|
+
onClick={() => setInputMode('github')}
|
|
416
|
+
style={{
|
|
417
|
+
...buttonStyle(inputMode === 'github' ? 'blue' : 'neutral', inputMode === 'github'),
|
|
418
|
+
padding: '9px 16px',
|
|
419
|
+
minWidth: 130,
|
|
420
|
+
}}
|
|
421
|
+
>
|
|
422
|
+
GitHub URL
|
|
254
423
|
</button>
|
|
255
|
-
|
|
256
|
-
</div>
|
|
257
|
-
)}
|
|
424
|
+
</div>
|
|
258
425
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
426
|
+
{inputMode === 'local' ? (
|
|
427
|
+
<div style={{ display: 'grid', gap: 14 }}>
|
|
428
|
+
<div>
|
|
429
|
+
<label style={{ fontSize: 11, fontWeight: 700, color: 'var(--text-muted)', display: 'block', marginBottom: 6, letterSpacing: 0.3 }}>
|
|
430
|
+
Project folder path
|
|
431
|
+
</label>
|
|
432
|
+
<div style={{ display: 'flex', gap: 10, alignItems: 'stretch' }}>
|
|
433
|
+
<input
|
|
434
|
+
value={projectPath}
|
|
435
|
+
onChange={event => setProjectPath(event.target.value)}
|
|
436
|
+
placeholder="/path/to/your/project"
|
|
437
|
+
disabled={setupState === 'analyzing'}
|
|
438
|
+
style={{
|
|
439
|
+
flex: 1,
|
|
440
|
+
padding: '12px 16px',
|
|
441
|
+
border: '1px solid var(--border)',
|
|
442
|
+
borderRadius: 18,
|
|
443
|
+
fontSize: 13,
|
|
444
|
+
fontFamily: 'var(--font-mono)',
|
|
445
|
+
background: 'var(--bg-input)',
|
|
446
|
+
color: 'var(--text)',
|
|
447
|
+
boxShadow: 'inset 0 1px 0 rgba(255,255,255,0.3)',
|
|
448
|
+
}}
|
|
449
|
+
/>
|
|
450
|
+
<button
|
|
451
|
+
onClick={openBrowser}
|
|
452
|
+
disabled={setupState === 'analyzing'}
|
|
453
|
+
style={{ ...buttonStyle('neutral'), minWidth: 116 }}
|
|
454
|
+
>
|
|
455
|
+
Browse
|
|
456
|
+
</button>
|
|
457
|
+
</div>
|
|
458
|
+
<div style={{ fontSize: 11, color: 'var(--text-dim)', marginTop: 8, lineHeight: 1.5 }}>
|
|
459
|
+
Use the folder browser to jump directly into likely project roots. Project directories are highlighted for faster intake.
|
|
460
|
+
</div>
|
|
461
|
+
</div>
|
|
462
|
+
|
|
463
|
+
<div>
|
|
464
|
+
<div style={{ fontSize: 11, fontWeight: 700, color: 'var(--text-muted)', marginBottom: 8, letterSpacing: 0.3 }}>
|
|
465
|
+
Quick select
|
|
466
|
+
</div>
|
|
467
|
+
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
|
|
468
|
+
{quickPaths.map(path => {
|
|
469
|
+
const active = projectPath === path
|
|
470
|
+
return (
|
|
471
|
+
<button
|
|
472
|
+
key={path}
|
|
473
|
+
onClick={() => setProjectPath(path)}
|
|
474
|
+
disabled={setupState === 'analyzing'}
|
|
475
|
+
style={{
|
|
476
|
+
padding: '7px 11px',
|
|
477
|
+
borderRadius: 999,
|
|
478
|
+
border: `1px solid ${active ? 'rgba(16,185,129,0.26)' : 'var(--border)'}`,
|
|
479
|
+
background: active ? 'var(--green-light)' : 'rgba(97,93,86,0.07)',
|
|
480
|
+
color: active ? 'var(--green)' : 'var(--text-dim)',
|
|
481
|
+
fontSize: 11,
|
|
482
|
+
fontWeight: 600,
|
|
483
|
+
fontFamily: 'var(--font-mono)',
|
|
484
|
+
cursor: 'pointer',
|
|
485
|
+
}}
|
|
486
|
+
>
|
|
487
|
+
{path}
|
|
488
|
+
</button>
|
|
489
|
+
)
|
|
490
|
+
})}
|
|
491
|
+
</div>
|
|
492
|
+
</div>
|
|
279
493
|
</div>
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
494
|
+
) : (
|
|
495
|
+
<div style={{ display: 'grid', gap: 14 }}>
|
|
496
|
+
<div>
|
|
497
|
+
<label style={{ fontSize: 11, fontWeight: 700, color: 'var(--text-muted)', display: 'block', marginBottom: 6, letterSpacing: 0.3 }}>
|
|
498
|
+
GitHub repository URL
|
|
499
|
+
</label>
|
|
500
|
+
<input
|
|
501
|
+
value={githubUrl}
|
|
502
|
+
onChange={event => setGithubUrl(event.target.value)}
|
|
503
|
+
placeholder="https://github.com/user/repo"
|
|
504
|
+
disabled={setupState === 'analyzing'}
|
|
505
|
+
style={{
|
|
506
|
+
width: '100%',
|
|
507
|
+
padding: '12px 16px',
|
|
508
|
+
border: '1px solid var(--border)',
|
|
509
|
+
borderRadius: 18,
|
|
510
|
+
fontSize: 13,
|
|
511
|
+
fontFamily: 'var(--font-mono)',
|
|
512
|
+
background: 'var(--bg-input)',
|
|
513
|
+
color: 'var(--text)',
|
|
514
|
+
}}
|
|
515
|
+
/>
|
|
516
|
+
</div>
|
|
517
|
+
<div>
|
|
518
|
+
<label style={{ fontSize: 11, fontWeight: 700, color: 'var(--text-muted)', display: 'block', marginBottom: 6, letterSpacing: 0.3 }}>
|
|
519
|
+
Clone destination
|
|
520
|
+
</label>
|
|
521
|
+
<input
|
|
522
|
+
value={cloneDir}
|
|
523
|
+
onChange={event => setCloneDir(event.target.value)}
|
|
524
|
+
placeholder="~/HelixEvo"
|
|
525
|
+
disabled={setupState === 'analyzing'}
|
|
526
|
+
style={{
|
|
527
|
+
width: '100%',
|
|
528
|
+
padding: '12px 16px',
|
|
529
|
+
border: '1px solid var(--border)',
|
|
530
|
+
borderRadius: 18,
|
|
531
|
+
fontSize: 13,
|
|
532
|
+
fontFamily: 'var(--font-mono)',
|
|
533
|
+
background: 'var(--bg-input)',
|
|
534
|
+
color: 'var(--text)',
|
|
535
|
+
}}
|
|
536
|
+
/>
|
|
537
|
+
<div style={{ marginTop: 8, fontSize: 11, color: 'var(--text-dim)' }}>
|
|
538
|
+
Target path: <code style={{ fontSize: 11, color: 'var(--text-secondary)' }}>{cloneTarget}</code>
|
|
539
|
+
</div>
|
|
540
|
+
</div>
|
|
298
541
|
</div>
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
542
|
+
)}
|
|
543
|
+
|
|
544
|
+
<div style={{
|
|
545
|
+
display: 'flex',
|
|
546
|
+
justifyContent: 'space-between',
|
|
547
|
+
alignItems: 'center',
|
|
548
|
+
gap: 16,
|
|
549
|
+
flexWrap: 'wrap',
|
|
550
|
+
paddingTop: 4,
|
|
551
|
+
}}>
|
|
552
|
+
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
|
|
553
|
+
<span className={`hero-chip hero-chip-${inputMode === 'local' ? 'purple' : 'blue'}`}>
|
|
554
|
+
{inputMode === 'local' ? 'Local analysis' : 'GitHub intake'}
|
|
555
|
+
</span>
|
|
556
|
+
<span className="hero-chip hero-chip-neutral">
|
|
557
|
+
{setupState === 'idle' ? 'Ready to run' : statusLabel(setupState)}
|
|
558
|
+
</span>
|
|
559
|
+
</div>
|
|
560
|
+
|
|
561
|
+
{setupState === 'analyzing' ? (
|
|
562
|
+
<button onClick={() => abortRef.current?.abort()} style={buttonStyle('red', true)}>
|
|
563
|
+
Stop analysis
|
|
308
564
|
</button>
|
|
309
565
|
) : (
|
|
310
|
-
<button onClick={
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
566
|
+
<button onClick={handleSetup} disabled={!canStart} style={{
|
|
567
|
+
...buttonStyle(inputMode === 'local' ? 'green' : 'blue', true),
|
|
568
|
+
opacity: canStart ? 1 : 0.55,
|
|
569
|
+
cursor: canStart ? 'pointer' : 'not-allowed',
|
|
314
570
|
}}>
|
|
315
|
-
|
|
571
|
+
{inputMode === 'local' ? 'Analyze project' : 'Clone & analyze'}
|
|
316
572
|
</button>
|
|
317
573
|
)}
|
|
318
574
|
</div>
|
|
319
|
-
<div style={{ fontSize: 10, color: 'var(--text-muted)', marginTop: 6 }}>
|
|
320
|
-
The repository will be cloned to <code style={{ fontSize: 9 }}>{cloneDir}/{githubUrl.match(/\/([\w.-]+?)(?:\.git)?$/)?.[1] ?? 'repo'}</code>, then analyzed
|
|
321
|
-
</div>
|
|
322
575
|
</div>
|
|
323
|
-
|
|
324
|
-
|
|
576
|
+
</SectionFrame>
|
|
577
|
+
|
|
578
|
+
<SectionFrame
|
|
579
|
+
eyebrow="Execution flow"
|
|
580
|
+
title="Capability mapping pipeline"
|
|
581
|
+
description="Every setup pass follows the same four-step sequence so analysis remains consistent and actionable."
|
|
582
|
+
tone="blue"
|
|
583
|
+
>
|
|
584
|
+
<div style={{ display: 'grid', gap: 12, marginBottom: 16 }}>
|
|
585
|
+
{PIPELINE.map((step, index) => (
|
|
586
|
+
<div key={step.step} style={{
|
|
587
|
+
position: 'relative',
|
|
588
|
+
padding: '16px 18px',
|
|
589
|
+
borderRadius: 18,
|
|
590
|
+
border: '1px solid var(--border)',
|
|
591
|
+
background: 'linear-gradient(180deg, rgba(255,255,255,0.86), rgba(245,241,235,0.92))',
|
|
592
|
+
boxShadow: 'var(--shadow-sm)',
|
|
593
|
+
}}>
|
|
594
|
+
<div style={{ display: 'flex', gap: 14, alignItems: 'flex-start' }}>
|
|
595
|
+
<div style={{
|
|
596
|
+
width: 34,
|
|
597
|
+
height: 34,
|
|
598
|
+
borderRadius: 12,
|
|
599
|
+
background: step.tone === 'blue'
|
|
600
|
+
? 'rgba(59,130,246,0.14)'
|
|
601
|
+
: step.tone === 'purple'
|
|
602
|
+
? 'rgba(107,73,223,0.14)'
|
|
603
|
+
: step.tone === 'yellow'
|
|
604
|
+
? 'rgba(245,158,11,0.14)'
|
|
605
|
+
: 'rgba(16,185,129,0.14)',
|
|
606
|
+
color: step.tone === 'blue'
|
|
607
|
+
? 'var(--blue)'
|
|
608
|
+
: step.tone === 'purple'
|
|
609
|
+
? 'var(--purple)'
|
|
610
|
+
: step.tone === 'yellow'
|
|
611
|
+
? 'var(--yellow)'
|
|
612
|
+
: 'var(--green)',
|
|
613
|
+
display: 'flex',
|
|
614
|
+
alignItems: 'center',
|
|
615
|
+
justifyContent: 'center',
|
|
616
|
+
fontSize: 13,
|
|
617
|
+
fontWeight: 800,
|
|
618
|
+
flexShrink: 0,
|
|
619
|
+
}}>
|
|
620
|
+
{step.step}
|
|
621
|
+
</div>
|
|
622
|
+
<div style={{ flex: 1 }}>
|
|
623
|
+
<div style={{ display: 'flex', justifyContent: 'space-between', gap: 10, marginBottom: 6 }}>
|
|
624
|
+
<div style={{ fontSize: 14, fontWeight: 700, color: 'var(--text)' }}>{step.title}</div>
|
|
625
|
+
<span className={`hero-chip hero-chip-${step.tone}`} style={{ whiteSpace: 'nowrap' }}>step {step.step}</span>
|
|
626
|
+
</div>
|
|
627
|
+
<div style={{ fontSize: 12, color: 'var(--text-dim)', lineHeight: 1.6, marginBottom: 8 }}>{step.desc}</div>
|
|
628
|
+
<code style={{ fontSize: 10.5, color: 'var(--text-secondary)', background: 'rgba(97,93,86,0.08)', padding: '4px 8px', borderRadius: 999 }}>
|
|
629
|
+
{step.command}
|
|
630
|
+
</code>
|
|
631
|
+
</div>
|
|
632
|
+
</div>
|
|
633
|
+
{index < PIPELINE.length - 1 ? (
|
|
634
|
+
<div style={{
|
|
635
|
+
position: 'absolute',
|
|
636
|
+
left: 33,
|
|
637
|
+
bottom: -14,
|
|
638
|
+
width: 2,
|
|
639
|
+
height: 18,
|
|
640
|
+
background: 'linear-gradient(180deg, rgba(107,73,223,0.45), rgba(16,185,129,0.12))',
|
|
641
|
+
}} />
|
|
642
|
+
) : null}
|
|
643
|
+
</div>
|
|
644
|
+
))}
|
|
645
|
+
</div>
|
|
325
646
|
|
|
326
|
-
{/* Output */}
|
|
327
|
-
{setupState !== 'idle' && (
|
|
328
|
-
<div className="card" style={{ marginBottom: 24, overflow: 'hidden' }}>
|
|
329
647
|
<div style={{
|
|
330
|
-
padding: '
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
color: setupState === 'done' ? 'var(--green)' : setupState === 'error' ? 'var(--red)' : 'var(--text-secondary)',
|
|
648
|
+
padding: '14px 16px',
|
|
649
|
+
borderRadius: 18,
|
|
650
|
+
background: 'rgba(107,73,223,0.08)',
|
|
651
|
+
border: '1px solid rgba(107,73,223,0.16)',
|
|
335
652
|
}}>
|
|
336
|
-
{
|
|
337
|
-
|
|
338
|
-
|
|
653
|
+
<div style={{ fontSize: 11, fontWeight: 700, color: 'var(--purple)', letterSpacing: 0.6, textTransform: 'uppercase', marginBottom: 6 }}>
|
|
654
|
+
Best follow-through
|
|
655
|
+
</div>
|
|
656
|
+
<div style={{ fontSize: 12.5, color: 'var(--text-secondary)', lineHeight: 1.6 }}>
|
|
657
|
+
After a profile finishes, inspect its strongest matched skills in the <strong>Skill Network</strong> and route uncovered gaps into <strong>Research</strong> for capability expansion.
|
|
658
|
+
</div>
|
|
339
659
|
</div>
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
padding: '7px 14px', background: 'var(--green)', color: '#fff', border: 'none',
|
|
354
|
-
borderRadius: 'var(--radius)', fontSize: 12, fontWeight: 600, cursor: 'pointer',
|
|
355
|
-
}}>
|
|
356
|
-
Refresh to see results
|
|
660
|
+
</SectionFrame>
|
|
661
|
+
</div>
|
|
662
|
+
|
|
663
|
+
{(setupState !== 'idle' || output) ? (
|
|
664
|
+
<SectionFrame
|
|
665
|
+
eyebrow="Live execution"
|
|
666
|
+
title="Project analysis console"
|
|
667
|
+
description="Streamed output from the active project-setup run."
|
|
668
|
+
tone={statusTone(setupState)}
|
|
669
|
+
actions={setupState === 'done' ? (
|
|
670
|
+
<div style={{ display: 'flex', gap: 10, flexWrap: 'wrap' }}>
|
|
671
|
+
<button onClick={() => window.location.reload()} style={buttonStyle('green', true)}>
|
|
672
|
+
Refresh results
|
|
357
673
|
</button>
|
|
358
|
-
<Link href="/network" style={
|
|
359
|
-
|
|
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
|
-
View Skill Network
|
|
364
|
-
</button>
|
|
674
|
+
<Link href="/network" style={buttonStyle('purple')}>
|
|
675
|
+
View skill network
|
|
365
676
|
</Link>
|
|
366
|
-
<Link href="/research" style={
|
|
367
|
-
|
|
368
|
-
padding: '7px 14px', background: 'var(--bg-section)', border: '1px solid var(--border)',
|
|
369
|
-
borderRadius: 'var(--radius)', fontSize: 12, fontWeight: 600, cursor: 'pointer', color: 'var(--text-secondary)',
|
|
370
|
-
}}>
|
|
371
|
-
Research Gaps
|
|
372
|
-
</button>
|
|
677
|
+
<Link href="/research" style={buttonStyle('blue')}>
|
|
678
|
+
Research gaps
|
|
373
679
|
</Link>
|
|
374
680
|
</div>
|
|
375
|
-
)}
|
|
376
|
-
|
|
377
|
-
|
|
681
|
+
) : null}
|
|
682
|
+
>
|
|
683
|
+
<ConsolePanel tone={statusTone(setupState)} title={statusLabel(setupState)}>
|
|
684
|
+
<pre ref={outputRef} style={{
|
|
685
|
+
margin: 0,
|
|
686
|
+
fontSize: 11.5,
|
|
687
|
+
lineHeight: 1.65,
|
|
688
|
+
fontFamily: 'var(--font-mono)',
|
|
689
|
+
color: 'var(--text-secondary)',
|
|
690
|
+
minHeight: 140,
|
|
691
|
+
maxHeight: 520,
|
|
692
|
+
overflow: 'auto',
|
|
693
|
+
whiteSpace: 'pre-wrap',
|
|
694
|
+
wordBreak: 'break-word',
|
|
695
|
+
}}>
|
|
696
|
+
{output || 'Console standing byβ¦'}
|
|
697
|
+
</pre>
|
|
698
|
+
</ConsolePanel>
|
|
699
|
+
</SectionFrame>
|
|
700
|
+
) : null}
|
|
378
701
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
702
|
+
<SectionFrame
|
|
703
|
+
eyebrow="Portfolio"
|
|
704
|
+
title={profiles.length > 0 ? `Analyzed projects (${profiles.length})` : 'No analyzed projects yet'}
|
|
705
|
+
description={profiles.length > 0
|
|
706
|
+
? 'Review project coverage, examine high-priority gaps, and jump directly into the next capability-building action.'
|
|
707
|
+
: `Run your first project profile above. HelixEvo will compare the project against your ${skillCount} skills and surface what needs to be filled next.`}
|
|
708
|
+
tone="yellow"
|
|
709
|
+
>
|
|
710
|
+
{profiles.length === 0 ? (
|
|
711
|
+
<div className="empty-state">
|
|
712
|
+
<div className="empty-state-icon">
|
|
713
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
|
|
714
|
+
<path d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
|
|
715
|
+
</svg>
|
|
716
|
+
</div>
|
|
717
|
+
<div className="empty-state-title">No analyzed projects yet</div>
|
|
718
|
+
<div className="empty-state-desc">
|
|
719
|
+
Start with a local folder or GitHub repository. The resulting profile will show matched skills, uncovered gaps, and recommended next steps.
|
|
720
|
+
</div>
|
|
384
721
|
</div>
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
const
|
|
722
|
+
) : (
|
|
723
|
+
<div style={{ display: 'grid', gap: 18 }}>
|
|
724
|
+
{profiles.map(profile => {
|
|
725
|
+
const highGaps = profile.gaps.filter(gap => gap.priority === 'high')
|
|
726
|
+
const matchedPct = skillCount > 0 ? Math.round((profile.matchedSkills.length / skillCount) * 100) : 0
|
|
389
727
|
|
|
390
728
|
return (
|
|
391
|
-
<
|
|
392
|
-
<div
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
729
|
+
<article key={profile.name} className="card" style={{ overflow: 'hidden' }}>
|
|
730
|
+
<div style={{
|
|
731
|
+
padding: '20px 24px 18px',
|
|
732
|
+
background: 'linear-gradient(135deg, rgba(255,251,235,0.95), rgba(248,244,238,0.88))',
|
|
733
|
+
borderBottom: '1px solid var(--border)',
|
|
734
|
+
}}>
|
|
735
|
+
<div style={{ display: 'flex', justifyContent: 'space-between', gap: 18, alignItems: 'flex-start', flexWrap: 'wrap' }}>
|
|
736
|
+
<div style={{ maxWidth: 760 }}>
|
|
737
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap', marginBottom: 8 }}>
|
|
738
|
+
<span className={`hero-chip hero-chip-${profile.status === 'active' ? 'green' : 'blue'}`}>{profile.status}</span>
|
|
739
|
+
<span className="hero-chip hero-chip-neutral">Analyzed {formatDate(profile.analyzedAt)}</span>
|
|
740
|
+
<span className={`hero-chip hero-chip-${highGaps.length > 0 ? 'red' : 'green'}`}>{highGaps.length} high-priority gap{highGaps.length === 1 ? '' : 's'}</span>
|
|
741
|
+
</div>
|
|
742
|
+
<div style={{ fontSize: 24, fontWeight: 800, color: 'var(--text)', marginBottom: 8 }}>{profile.name}</div>
|
|
743
|
+
<div style={{ fontSize: 13, color: 'var(--text-secondary)', lineHeight: 1.7, marginBottom: 12 }}>{profile.description}</div>
|
|
744
|
+
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
|
|
745
|
+
{profile.techStack.map(tech => <span key={tech} className="badge badge-gray">{tech}</span>)}
|
|
746
|
+
{profile.domains.map(domain => <span key={domain} className="badge badge-blue">{domain}</span>)}
|
|
401
747
|
</div>
|
|
402
748
|
</div>
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
<
|
|
749
|
+
|
|
750
|
+
<div style={{ minWidth: 240, display: 'grid', gap: 10 }}>
|
|
751
|
+
<div style={{
|
|
752
|
+
padding: '14px 16px',
|
|
753
|
+
borderRadius: 18,
|
|
754
|
+
background: 'rgba(255,255,255,0.7)',
|
|
755
|
+
border: '1px solid var(--border)',
|
|
756
|
+
}}>
|
|
757
|
+
<div style={{ fontSize: 11, color: 'var(--text-muted)', marginBottom: 4, letterSpacing: 0.4, textTransform: 'uppercase' }}>
|
|
758
|
+
Skill coverage
|
|
759
|
+
</div>
|
|
760
|
+
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', marginBottom: 8 }}>
|
|
761
|
+
<div style={{ fontSize: 30, fontWeight: 800, color: matchedPct >= 70 ? 'var(--green)' : matchedPct >= 40 ? 'var(--yellow)' : 'var(--red)' }}>
|
|
762
|
+
{matchedPct}%
|
|
763
|
+
</div>
|
|
764
|
+
<div style={{ fontSize: 11, color: 'var(--text-dim)' }}>{profile.matchedSkills.length}/{skillCount || 1} skills</div>
|
|
765
|
+
</div>
|
|
766
|
+
<div className="score-track" style={{ height: 6 }}>
|
|
767
|
+
<div className="score-fill" style={{
|
|
768
|
+
width: `${matchedPct}%`,
|
|
769
|
+
background: matchedPct >= 70 ? 'linear-gradient(90deg, #34d399, #059669)' : matchedPct >= 40 ? 'linear-gradient(90deg, #fbbf24, #f59e0b)' : 'linear-gradient(90deg, #fda4af, #ef4444)',
|
|
770
|
+
}} />
|
|
771
|
+
</div>
|
|
772
|
+
</div>
|
|
773
|
+
|
|
774
|
+
<div style={{
|
|
775
|
+
padding: '12px 14px',
|
|
776
|
+
borderRadius: 18,
|
|
777
|
+
background: 'rgba(97,93,86,0.07)',
|
|
778
|
+
border: '1px solid var(--border)',
|
|
779
|
+
}}>
|
|
780
|
+
<div style={{ fontSize: 11, color: 'var(--text-muted)', marginBottom: 4, letterSpacing: 0.4, textTransform: 'uppercase' }}>
|
|
781
|
+
Workspace path
|
|
782
|
+
</div>
|
|
783
|
+
<div style={{ fontSize: 11.5, fontFamily: 'var(--font-mono)', color: 'var(--text-secondary)', lineHeight: 1.6, wordBreak: 'break-all' }}>
|
|
784
|
+
{profile.path}
|
|
785
|
+
</div>
|
|
786
|
+
</div>
|
|
406
787
|
</div>
|
|
407
788
|
</div>
|
|
789
|
+
</div>
|
|
408
790
|
|
|
409
|
-
|
|
410
|
-
<div className="grid-3"
|
|
411
|
-
<div style={{ padding: '
|
|
412
|
-
<div style={{ fontSize:
|
|
413
|
-
<div style={{ fontSize:
|
|
791
|
+
<div className="card-body" style={{ display: 'grid', gap: 18 }}>
|
|
792
|
+
<div className="grid-3">
|
|
793
|
+
<div style={{ padding: '14px 16px', borderRadius: 18, background: 'rgba(16,185,129,0.08)', border: '1px solid rgba(16,185,129,0.16)' }}>
|
|
794
|
+
<div style={{ fontSize: 11, color: 'var(--green)', marginBottom: 6, textTransform: 'uppercase', letterSpacing: 0.4 }}>Matched skills</div>
|
|
795
|
+
<div style={{ fontSize: 26, fontWeight: 800, color: 'var(--green)', marginBottom: 4 }}>{profile.matchedSkills.length}</div>
|
|
796
|
+
<div style={{ fontSize: 12, color: 'var(--text-dim)' }}>Capability matches strong enough to activate for this project.</div>
|
|
414
797
|
</div>
|
|
415
|
-
<div style={{ padding: '
|
|
416
|
-
<div style={{ fontSize:
|
|
417
|
-
<div style={{ fontSize:
|
|
798
|
+
<div style={{ padding: '14px 16px', borderRadius: 18, background: highGaps.length > 0 ? 'var(--red-light)' : 'rgba(16,185,129,0.08)', border: `1px solid ${highGaps.length > 0 ? 'var(--red-border)' : 'rgba(16,185,129,0.16)'}` }}>
|
|
799
|
+
<div style={{ fontSize: 11, color: highGaps.length > 0 ? 'var(--red)' : 'var(--green)', marginBottom: 6, textTransform: 'uppercase', letterSpacing: 0.4 }}>High-priority gaps</div>
|
|
800
|
+
<div style={{ fontSize: 26, fontWeight: 800, color: highGaps.length > 0 ? 'var(--red)' : 'var(--green)', marginBottom: 4 }}>{highGaps.length}</div>
|
|
801
|
+
<div style={{ fontSize: 12, color: 'var(--text-dim)' }}>{profile.gaps.length} total uncovered capability area{profile.gaps.length === 1 ? '' : 's'}.</div>
|
|
418
802
|
</div>
|
|
419
|
-
<div style={{ padding: '
|
|
420
|
-
<div style={{ fontSize: 11, color: 'var(--
|
|
421
|
-
<div style={{ fontSize:
|
|
803
|
+
<div style={{ padding: '14px 16px', borderRadius: 18, background: 'rgba(107,73,223,0.08)', border: '1px solid rgba(107,73,223,0.16)' }}>
|
|
804
|
+
<div style={{ fontSize: 11, color: 'var(--purple)', marginBottom: 6, textTransform: 'uppercase', letterSpacing: 0.4 }}>Recommendations</div>
|
|
805
|
+
<div style={{ fontSize: 26, fontWeight: 800, color: 'var(--purple)', marginBottom: 4 }}>{profile.recommendations.length}</div>
|
|
806
|
+
<div style={{ fontSize: 12, color: 'var(--text-dim)' }}>Guided next actions distilled from the analysis pass.</div>
|
|
422
807
|
</div>
|
|
423
808
|
</div>
|
|
424
809
|
|
|
425
|
-
{
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
<div style={{
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
810
|
+
{profile.matchedSkills.length > 0 ? (
|
|
811
|
+
<div>
|
|
812
|
+
<div className="section-label">Matched skills</div>
|
|
813
|
+
<div style={{ display: 'grid', gap: 8 }}>
|
|
814
|
+
{profile.matchedSkills
|
|
815
|
+
.slice()
|
|
816
|
+
.sort((a, b) => b.relevance - a.relevance)
|
|
817
|
+
.slice(0, 8)
|
|
818
|
+
.map(match => (
|
|
819
|
+
<div key={match.slug} style={{
|
|
820
|
+
padding: '12px 14px',
|
|
821
|
+
borderRadius: 16,
|
|
822
|
+
border: '1px solid var(--border)',
|
|
823
|
+
background: 'rgba(255,255,255,0.66)',
|
|
824
|
+
}}>
|
|
825
|
+
<div style={{ display: 'flex', gap: 12, justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', marginBottom: 8 }}>
|
|
826
|
+
<Link href="/network" style={{ fontSize: 13, fontWeight: 700, color: 'var(--text)', textDecoration: 'none' }}>
|
|
827
|
+
{match.slug}
|
|
828
|
+
</Link>
|
|
829
|
+
<span className={`hero-chip hero-chip-${match.relevance >= 70 ? 'green' : match.relevance >= 40 ? 'yellow' : 'neutral'}`}>
|
|
830
|
+
{match.relevance}% relevance
|
|
831
|
+
</span>
|
|
832
|
+
</div>
|
|
833
|
+
<div className="score-track" style={{ height: 5, marginBottom: 8 }}>
|
|
438
834
|
<div className="score-fill" style={{
|
|
439
|
-
width: `${
|
|
440
|
-
background:
|
|
835
|
+
width: `${match.relevance}%`,
|
|
836
|
+
background: match.relevance >= 70 ? 'linear-gradient(90deg, #34d399, #059669)' : match.relevance >= 40 ? 'linear-gradient(90deg, #fbbf24, #f59e0b)' : 'linear-gradient(90deg, #d1d5db, #9ca3af)',
|
|
441
837
|
}} />
|
|
442
838
|
</div>
|
|
839
|
+
<div style={{ fontSize: 11.5, color: 'var(--text-dim)', lineHeight: 1.55 }}>{match.reason}</div>
|
|
443
840
|
</div>
|
|
444
|
-
|
|
445
|
-
<span style={{ fontSize: 10, color: 'var(--text-dim)', maxWidth: 200, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{m.reason}</span>
|
|
446
|
-
</div>
|
|
447
|
-
))}
|
|
448
|
-
{p.matchedSkills.length > 8 && (
|
|
449
|
-
<div style={{ fontSize: 10, color: 'var(--text-muted)', paddingLeft: 10 }}>+{p.matchedSkills.length - 8} more skills</div>
|
|
450
|
-
)}
|
|
841
|
+
))}
|
|
451
842
|
</div>
|
|
452
843
|
</div>
|
|
453
|
-
)}
|
|
454
|
-
|
|
455
|
-
{
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
<div style={{
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
844
|
+
) : null}
|
|
845
|
+
|
|
846
|
+
{profile.gaps.length > 0 ? (
|
|
847
|
+
<div>
|
|
848
|
+
<div className="section-label">Gap routing</div>
|
|
849
|
+
<div style={{ display: 'grid', gap: 10 }}>
|
|
850
|
+
{profile.gaps.map((gap, index) => (
|
|
851
|
+
<div key={`${gap.area}-${index}`} style={{
|
|
852
|
+
padding: '14px 16px',
|
|
853
|
+
borderRadius: 18,
|
|
854
|
+
border: `1px solid ${gap.priority === 'high' ? 'var(--red-border)' : gap.priority === 'medium' ? 'var(--yellow-border)' : 'var(--border)'}`,
|
|
855
|
+
background: gap.priority === 'high' ? 'var(--red-light)' : gap.priority === 'medium' ? 'var(--yellow-light)' : 'rgba(255,255,255,0.7)',
|
|
465
856
|
}}>
|
|
466
|
-
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom:
|
|
857
|
+
<div style={{ display: 'flex', justifyContent: 'space-between', gap: 12, alignItems: 'flex-start', flexWrap: 'wrap', marginBottom: 8 }}>
|
|
467
858
|
<div>
|
|
468
|
-
<
|
|
469
|
-
|
|
859
|
+
<div style={{ display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap', marginBottom: 6 }}>
|
|
860
|
+
<div style={{ fontSize: 14, fontWeight: 700, color: 'var(--text)' }}>{gap.area}</div>
|
|
861
|
+
<span className={`hero-chip hero-chip-${priorityTone(gap.priority)}`}>{gap.priority} priority</span>
|
|
862
|
+
<span className={`hero-chip hero-chip-${gap.suggestedAction === 'research' ? 'blue' : gap.suggestedAction === 'specialize' ? 'green' : 'purple'}`}>
|
|
863
|
+
{gap.suggestedAction}
|
|
864
|
+
</span>
|
|
865
|
+
</div>
|
|
866
|
+
<div style={{ fontSize: 12.5, color: 'var(--text-secondary)', lineHeight: 1.6 }}>{gap.description}</div>
|
|
470
867
|
</div>
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
868
|
+
|
|
869
|
+
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
|
|
870
|
+
{gap.suggestedAction === 'research' ? (
|
|
871
|
+
<Link href="/research" style={buttonStyle('blue')}>
|
|
872
|
+
Research gap
|
|
873
|
+
</Link>
|
|
874
|
+
) : null}
|
|
875
|
+
{gap.suggestedAction === 'specialize' ? (
|
|
876
|
+
<button
|
|
877
|
+
onClick={() => void triggerSpecialize(profile.name)}
|
|
878
|
+
disabled={specializingProject === profile.name}
|
|
879
|
+
style={{
|
|
880
|
+
...buttonStyle('green'),
|
|
881
|
+
opacity: specializingProject === profile.name ? 0.65 : 1,
|
|
882
|
+
}}
|
|
883
|
+
>
|
|
884
|
+
{specializingProject === profile.name ? 'Startingβ¦' : 'Specialize skill'}
|
|
479
885
|
</button>
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
<button style={{
|
|
484
|
-
padding: '4px 12px', fontSize: 10, fontWeight: 600,
|
|
485
|
-
background: 'var(--green)', color: '#fff', border: 'none',
|
|
486
|
-
borderRadius: 'var(--radius)', cursor: 'pointer',
|
|
487
|
-
}}>
|
|
488
|
-
Specialize a skill
|
|
489
|
-
</button>
|
|
490
|
-
)}
|
|
491
|
-
{g.suggestedAction === 'create' && (
|
|
492
|
-
<Link href="/network" style={{ textDecoration: 'none' }}>
|
|
493
|
-
<button style={{
|
|
494
|
-
padding: '4px 12px', fontSize: 10, fontWeight: 600,
|
|
495
|
-
background: 'var(--bg-section)', color: 'var(--text-secondary)',
|
|
496
|
-
border: '1px solid var(--border)',
|
|
497
|
-
borderRadius: 'var(--radius)', cursor: 'pointer',
|
|
498
|
-
}}>
|
|
886
|
+
) : null}
|
|
887
|
+
{gap.suggestedAction === 'create' ? (
|
|
888
|
+
<Link href="/network" style={buttonStyle('purple')}>
|
|
499
889
|
Create manually
|
|
500
|
-
</
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
</div>
|
|
504
|
-
<div style={{ fontSize: 11, color: 'var(--text-dim)', lineHeight: 1.5 }}>
|
|
505
|
-
{g.description}
|
|
890
|
+
</Link>
|
|
891
|
+
) : null}
|
|
892
|
+
</div>
|
|
506
893
|
</div>
|
|
507
894
|
</div>
|
|
508
895
|
))}
|
|
509
896
|
</div>
|
|
510
897
|
</div>
|
|
511
|
-
)}
|
|
512
|
-
|
|
513
|
-
{
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
))}
|
|
520
|
-
</div>
|
|
521
|
-
)}
|
|
522
|
-
|
|
523
|
-
{/* Re-analyze button */}
|
|
524
|
-
<div style={{ marginTop: 12, display: 'flex', gap: 8 }}>
|
|
525
|
-
<button onClick={() => { setProjectPath(p.path); setInputMode('local'); window.scrollTo(0, 0) }} 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)',
|
|
898
|
+
) : null}
|
|
899
|
+
|
|
900
|
+
{profile.recommendations.length > 0 ? (
|
|
901
|
+
<div style={{
|
|
902
|
+
padding: '16px 18px',
|
|
903
|
+
borderRadius: 18,
|
|
904
|
+
background: 'rgba(97,93,86,0.07)',
|
|
905
|
+
border: '1px solid var(--border)',
|
|
529
906
|
}}>
|
|
907
|
+
<div className="section-label" style={{ marginBottom: 10 }}>Recommended next steps</div>
|
|
908
|
+
<div style={{ display: 'grid', gap: 8 }}>
|
|
909
|
+
{profile.recommendations.map((recommendation, index) => (
|
|
910
|
+
<div key={`${profile.name}-recommendation-${index}`} style={{ display: 'flex', gap: 10, alignItems: 'flex-start' }}>
|
|
911
|
+
<span style={{ color: 'var(--purple)', fontWeight: 800, fontSize: 14 }}>β’</span>
|
|
912
|
+
<span style={{ fontSize: 12.5, color: 'var(--text-secondary)', lineHeight: 1.65 }}>{recommendation}</span>
|
|
913
|
+
</div>
|
|
914
|
+
))}
|
|
915
|
+
</div>
|
|
916
|
+
</div>
|
|
917
|
+
) : null}
|
|
918
|
+
|
|
919
|
+
<div style={{ display: 'flex', gap: 10, flexWrap: 'wrap' }}>
|
|
920
|
+
<button
|
|
921
|
+
onClick={() => {
|
|
922
|
+
setProjectPath(profile.path)
|
|
923
|
+
setInputMode('local')
|
|
924
|
+
window.scrollTo({ top: 0, behavior: 'smooth' })
|
|
925
|
+
}}
|
|
926
|
+
style={buttonStyle('neutral')}
|
|
927
|
+
>
|
|
530
928
|
Re-analyze
|
|
531
929
|
</button>
|
|
532
|
-
<
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
|
|
930
|
+
<button
|
|
931
|
+
onClick={() => void triggerSpecialize(profile.name)}
|
|
932
|
+
disabled={specializingProject === profile.name}
|
|
933
|
+
style={{ ...buttonStyle('green'), opacity: specializingProject === profile.name ? 0.65 : 1 }}
|
|
934
|
+
>
|
|
935
|
+
{specializingProject === profile.name ? 'Starting specializationβ¦' : 'Specialize for project'}
|
|
936
|
+
</button>
|
|
937
|
+
<Link href="/network" style={buttonStyle('purple')}>
|
|
938
|
+
View in skill network
|
|
939
|
+
</Link>
|
|
940
|
+
<Link href="/research" style={buttonStyle('blue')}>
|
|
941
|
+
Open research
|
|
540
942
|
</Link>
|
|
541
943
|
</div>
|
|
542
944
|
</div>
|
|
543
|
-
</
|
|
945
|
+
</article>
|
|
544
946
|
)
|
|
545
947
|
})}
|
|
546
948
|
</div>
|
|
547
|
-
|
|
548
|
-
|
|
949
|
+
)}
|
|
950
|
+
</SectionFrame>
|
|
549
951
|
|
|
550
|
-
{
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
952
|
+
{showBrowser ? (
|
|
953
|
+
<div
|
|
954
|
+
style={{
|
|
955
|
+
position: 'fixed',
|
|
956
|
+
inset: 0,
|
|
957
|
+
zIndex: 9999,
|
|
958
|
+
background: 'rgba(31, 27, 24, 0.34)',
|
|
959
|
+
display: 'flex',
|
|
960
|
+
alignItems: 'center',
|
|
961
|
+
justifyContent: 'center',
|
|
962
|
+
padding: 24,
|
|
963
|
+
backdropFilter: 'blur(8px)',
|
|
964
|
+
}}
|
|
965
|
+
onClick={() => setShowBrowser(false)}
|
|
966
|
+
>
|
|
967
|
+
<div
|
|
968
|
+
onClick={event => event.stopPropagation()}
|
|
969
|
+
style={{
|
|
970
|
+
width: 'min(880px, 100%)',
|
|
971
|
+
maxHeight: '78vh',
|
|
972
|
+
background: 'linear-gradient(180deg, rgba(255,255,255,0.96), rgba(247,243,238,0.98))',
|
|
973
|
+
borderRadius: 28,
|
|
974
|
+
boxShadow: 'var(--shadow-xl)',
|
|
975
|
+
border: '1px solid var(--border)',
|
|
976
|
+
overflow: 'hidden',
|
|
977
|
+
display: 'flex',
|
|
978
|
+
flexDirection: 'column',
|
|
979
|
+
}}
|
|
980
|
+
>
|
|
981
|
+
<div style={{
|
|
982
|
+
padding: '18px 22px',
|
|
983
|
+
borderBottom: '1px solid var(--border)',
|
|
984
|
+
display: 'flex',
|
|
985
|
+
justifyContent: 'space-between',
|
|
986
|
+
alignItems: 'flex-start',
|
|
987
|
+
gap: 18,
|
|
988
|
+
flexWrap: 'wrap',
|
|
989
|
+
}}>
|
|
990
|
+
<div>
|
|
991
|
+
<div style={{ fontSize: 11, fontWeight: 700, color: 'var(--text-muted)', letterSpacing: 0.5, textTransform: 'uppercase', marginBottom: 6 }}>
|
|
992
|
+
Folder browser
|
|
993
|
+
</div>
|
|
994
|
+
<div style={{ fontSize: 20, fontWeight: 800, color: 'var(--text)', marginBottom: 6 }}>Choose a project root</div>
|
|
995
|
+
<div style={{ fontSize: 12, color: 'var(--text-dim)', lineHeight: 1.6, marginBottom: 10 }}>
|
|
996
|
+
Browse into a workspace and select a directory. Project-like folders are highlighted to speed up intake.
|
|
997
|
+
</div>
|
|
998
|
+
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
|
|
999
|
+
<span className="hero-chip hero-chip-neutral">{browseData?.displayPath ?? '~'}</span>
|
|
1000
|
+
<span className="hero-chip hero-chip-green">{browseData?.items.filter(item => item.isProject).length ?? 0} project candidate{(browseData?.items.filter(item => item.isProject).length ?? 0) === 1 ? '' : 's'}</span>
|
|
1001
|
+
<span className="hero-chip hero-chip-blue">{browseData?.items.filter(item => item.isDirectory).length ?? 0} folder{(browseData?.items.filter(item => item.isDirectory).length ?? 0) === 1 ? '' : 's'}</span>
|
|
1002
|
+
</div>
|
|
1003
|
+
</div>
|
|
1004
|
+
|
|
1005
|
+
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
|
|
1006
|
+
{browseData?.parent ? (
|
|
1007
|
+
<button onClick={() => void browseTo(browseData.parent!)} style={buttonStyle('neutral')}>
|
|
1008
|
+
Go up
|
|
1009
|
+
</button>
|
|
1010
|
+
) : null}
|
|
1011
|
+
<button onClick={() => selectFolder(browseData?.path ?? '~')} style={buttonStyle('green', true)}>
|
|
1012
|
+
Select this folder
|
|
1013
|
+
</button>
|
|
1014
|
+
<button onClick={() => setShowBrowser(false)} style={buttonStyle('neutral')}>
|
|
1015
|
+
Close
|
|
1016
|
+
</button>
|
|
1017
|
+
</div>
|
|
558
1018
|
</div>
|
|
559
|
-
|
|
560
|
-
<div
|
|
561
|
-
|
|
562
|
-
|
|
1019
|
+
|
|
1020
|
+
<div style={{ overflow: 'auto', flex: 1, padding: '10px 12px 16px' }}>
|
|
1021
|
+
{browseLoading ? (
|
|
1022
|
+
<div style={{ padding: 32, textAlign: 'center', color: 'var(--text-dim)', fontSize: 13 }}>Loading folder contentsβ¦</div>
|
|
1023
|
+
) : browseData?.items.length === 0 ? (
|
|
1024
|
+
<div style={{ padding: 32, textAlign: 'center', color: 'var(--text-dim)', fontSize: 13 }}>This directory is empty.</div>
|
|
1025
|
+
) : (
|
|
1026
|
+
<div style={{ display: 'grid', gap: 8 }}>
|
|
1027
|
+
{browseData?.items.map(item => {
|
|
1028
|
+
const itemPath = `${browseData.path}/${item.name}`
|
|
1029
|
+
const interactive = item.isDirectory
|
|
1030
|
+
|
|
1031
|
+
return (
|
|
1032
|
+
<div
|
|
1033
|
+
key={item.name}
|
|
1034
|
+
onClick={() => {
|
|
1035
|
+
if (!interactive) return
|
|
1036
|
+
if (item.isProject) {
|
|
1037
|
+
selectFolder(itemPath)
|
|
1038
|
+
} else {
|
|
1039
|
+
void browseTo(itemPath)
|
|
1040
|
+
}
|
|
1041
|
+
}}
|
|
1042
|
+
style={{
|
|
1043
|
+
padding: '14px 16px',
|
|
1044
|
+
borderRadius: 18,
|
|
1045
|
+
border: `1px solid ${item.isProject ? 'rgba(16,185,129,0.24)' : 'var(--border)'}`,
|
|
1046
|
+
background: item.isProject ? 'var(--green-light)' : 'rgba(255,255,255,0.68)',
|
|
1047
|
+
display: 'flex',
|
|
1048
|
+
alignItems: 'center',
|
|
1049
|
+
gap: 14,
|
|
1050
|
+
cursor: interactive ? 'pointer' : 'default',
|
|
1051
|
+
transition: 'transform 0.15s ease, box-shadow 0.15s ease, border-color 0.15s ease',
|
|
1052
|
+
boxShadow: item.isProject ? 'var(--shadow-sm)' : 'none',
|
|
1053
|
+
}}
|
|
1054
|
+
>
|
|
1055
|
+
<div style={{
|
|
1056
|
+
width: 38,
|
|
1057
|
+
height: 38,
|
|
1058
|
+
borderRadius: 14,
|
|
1059
|
+
background: item.isProject ? 'rgba(16,185,129,0.16)' : item.isDirectory ? 'rgba(59,130,246,0.12)' : 'rgba(97,93,86,0.08)',
|
|
1060
|
+
color: item.isProject ? 'var(--green)' : item.isDirectory ? 'var(--blue)' : 'var(--text-muted)',
|
|
1061
|
+
display: 'flex',
|
|
1062
|
+
alignItems: 'center',
|
|
1063
|
+
justifyContent: 'center',
|
|
1064
|
+
fontSize: 16,
|
|
1065
|
+
fontWeight: 800,
|
|
1066
|
+
flexShrink: 0,
|
|
1067
|
+
}}>
|
|
1068
|
+
{item.isProject ? 'P' : item.isDirectory ? 'D' : 'F'}
|
|
1069
|
+
</div>
|
|
1070
|
+
<div style={{ flex: 1, minWidth: 0 }}>
|
|
1071
|
+
<div style={{ fontSize: 13, fontWeight: 700, color: 'var(--text)', marginBottom: 4 }}>{item.name}</div>
|
|
1072
|
+
<div style={{ fontSize: 11.5, color: 'var(--text-dim)' }}>
|
|
1073
|
+
{item.isProject ? 'Looks like a project root β click to select immediately.' : item.isDirectory ? 'Directory β click to continue browsing.' : 'File'}
|
|
1074
|
+
</div>
|
|
1075
|
+
</div>
|
|
1076
|
+
<div style={{ display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' }}>
|
|
1077
|
+
{item.isProject ? <span className="hero-chip hero-chip-green">project</span> : null}
|
|
1078
|
+
{item.isDirectory && !item.isProject ? <span className="hero-chip hero-chip-blue">folder</span> : null}
|
|
1079
|
+
{!item.isDirectory ? <span className="hero-chip hero-chip-neutral">file</span> : null}
|
|
1080
|
+
</div>
|
|
1081
|
+
</div>
|
|
1082
|
+
)
|
|
1083
|
+
})}
|
|
1084
|
+
</div>
|
|
1085
|
+
)}
|
|
563
1086
|
</div>
|
|
564
1087
|
</div>
|
|
565
1088
|
</div>
|
|
566
|
-
)}
|
|
1089
|
+
) : null}
|
|
567
1090
|
</div>
|
|
568
1091
|
)
|
|
569
1092
|
}
|