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.
@@ -1,35 +1,124 @@
1
1
  'use client'
2
2
 
3
- import { useState, useRef } from 'react'
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, 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)',
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, 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)',
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, 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)',
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, 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)',
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 effectivePath = inputMode === 'local' ? projectPath.trim() : ''
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
- // Clone first
209
+ if (inputMode === 'github') {
210
+ path = cloneTarget
57
211
  setSetupState('analyzing')
58
- setOutput(`Cloning ${githubUrl} to ${path}...\n`)
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
- // 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 {}
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) { setOutput('No response'); setSetupState('error'); return }
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 = '', eventData = ''
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(() => { if (outputRef.current) outputRef.current.scrollTop = outputRef.current.scrollHeight }, 10)
109
- } catch {}
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 r = JSON.parse(JSON.parse(eventData) as string) as { success: boolean }
114
- setSetupState(r.success ? 'done' : 'error')
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
- if (setupState === 'analyzing') setSetupState('done')
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[Cancelled]')
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
- <div className="page-header">
135
- <h1 className="page-title">Project Setup</h1>
136
- <p className="page-desc">
137
- Analyze any project to match your {skillCount} skills, identify gaps, and prepare for work
138
- </p>
139
- </div>
140
-
141
- {/* Pipeline Visualization */}
142
- <div className="card" style={{ marginBottom: 24, padding: '22px 26px' }}>
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' }}>
149
- <div style={{ flex: 1 }}>
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 }}>&rarr;</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
- </div>
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
- {/* 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>
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
- {/* 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'}
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
- 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)',
407
+ ...buttonStyle(inputMode === 'local' ? 'purple' : 'neutral', inputMode === 'local'),
408
+ padding: '9px 16px',
409
+ minWidth: 130,
223
410
  }}
224
- />
225
- <div style={{ fontSize: 10, color: 'var(--text-muted)', marginTop: 6, display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'wrap' }}>
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
- <button onClick={() => abortRef.current?.abort()} style={{
249
- padding: '9px 20px', border: 'none', borderRadius: 'var(--radius)',
250
- background: 'var(--red)', color: '#fff',
251
- fontSize: 13, fontWeight: 600, cursor: 'pointer', whiteSpace: 'nowrap',
252
- }}>
253
- Stop
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
- {/* GitHub URL Input */}
260
- {inputMode === 'github' && (
261
- <div>
262
- <div style={{ display: 'flex', gap: 10, alignItems: 'flex-end', marginBottom: 10 }}>
263
- <div style={{ flex: 1 }}>
264
- <label style={{ fontSize: 11, fontWeight: 600, color: 'var(--text-dim)', display: 'block', marginBottom: 5 }}>
265
- GitHub repository URL
266
- </label>
267
- <input
268
- value={githubUrl}
269
- onChange={e => setGithubUrl(e.target.value)}
270
- placeholder="https://github.com/user/repo"
271
- disabled={setupState === 'analyzing'}
272
- style={{
273
- width: '100%', padding: '9px 14px',
274
- border: '1px solid var(--border)', borderRadius: 'var(--radius)',
275
- fontSize: 13, fontFamily: 'var(--font-mono)',
276
- background: 'var(--bg-input)', color: 'var(--text)',
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
- </div>
281
- <div style={{ display: 'flex', gap: 10, alignItems: 'flex-end' }}>
282
- <div style={{ flex: 1 }}>
283
- <label style={{ fontSize: 11, fontWeight: 600, color: 'var(--text-dim)', display: 'block', marginBottom: 5 }}>
284
- Clone to directory
285
- </label>
286
- <input
287
- value={cloneDir}
288
- onChange={e => setCloneDir(e.target.value)}
289
- placeholder="~/projects"
290
- disabled={setupState === 'analyzing'}
291
- style={{
292
- width: '100%', padding: '9px 14px',
293
- border: '1px solid var(--border)', borderRadius: 'var(--radius)',
294
- fontSize: 13, fontFamily: 'var(--font-mono)',
295
- background: 'var(--bg-input)', color: 'var(--text)',
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
- {setupState !== 'analyzing' ? (
300
- <button onClick={handleSetup} disabled={!canStart} style={{
301
- padding: '9px 20px', border: 'none', borderRadius: 'var(--radius)',
302
- background: canStart ? 'var(--green)' : 'var(--bg-section)',
303
- color: canStart ? '#fff' : 'var(--text-muted)',
304
- fontSize: 13, fontWeight: 600, cursor: canStart ? 'pointer' : 'default',
305
- whiteSpace: 'nowrap',
306
- }}>
307
- Clone &amp; Analyze
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={() => abortRef.current?.abort()} style={{
311
- padding: '9px 20px', border: 'none', borderRadius: 'var(--radius)',
312
- background: 'var(--red)', color: '#fff',
313
- fontSize: 13, fontWeight: 600, cursor: 'pointer', whiteSpace: 'nowrap',
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
- Stop
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
- </div>
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: '10px 16px',
331
- background: setupState === 'done' ? 'var(--green-light)' : setupState === 'error' ? 'var(--red-light)' : 'var(--bg-section)',
332
- borderBottom: '1px solid var(--border)',
333
- fontSize: 12, fontWeight: 600,
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
- {setupState === 'analyzing' && '● Running project analysis...'}
337
- {setupState === 'done' && 'βœ“ Analysis complete β€” project profile saved'}
338
- {setupState === 'error' && 'βœ— Analysis failed'}
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
- <pre ref={outputRef} style={{
341
- padding: '14px 16px', margin: 0,
342
- fontSize: 11, lineHeight: 1.55,
343
- fontFamily: 'var(--font-mono)',
344
- color: 'var(--text-secondary)',
345
- maxHeight: 500, overflow: 'auto',
346
- whiteSpace: 'pre-wrap', wordBreak: 'break-word',
347
- }}>
348
- {output || 'Starting project analysis...'}
349
- </pre>
350
- {setupState === 'done' && (
351
- <div style={{ padding: '12px 16px', borderTop: '1px solid var(--border)', display: 'flex', gap: 8 }}>
352
- <button onClick={() => window.location.reload()} style={{
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={{ 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
- View Skill Network
364
- </button>
674
+ <Link href="/network" style={buttonStyle('purple')}>
675
+ View skill network
365
676
  </Link>
366
- <Link href="/research" style={{ textDecoration: 'none' }}>
367
- <button style={{
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
- </div>
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
- {/* Existing Profiles */}
380
- {profiles.length > 0 && (
381
- <div>
382
- <div style={{ fontSize: 10, fontWeight: 700, color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: 1.5, marginBottom: 12 }}>
383
- Analyzed Projects ({profiles.length})
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
- <div style={{ display: 'grid', gap: 14 }}>
386
- {profiles.map(p => {
387
- const highGaps = p.gaps.filter(g => g.priority === 'high')
388
- const matchedPct = skillCount > 0 ? Math.round((p.matchedSkills.length / skillCount) * 100) : 0
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
- <div key={p.name} className="card">
392
- <div className="card-body">
393
- {/* Header */}
394
- <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 12 }}>
395
- <div>
396
- <div style={{ fontSize: 18, fontWeight: 700, marginBottom: 4 }}>{p.name}</div>
397
- <div style={{ fontSize: 12, color: 'var(--text-dim)', marginBottom: 6 }}>{p.description}</div>
398
- <div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
399
- {p.techStack.map(t => <span key={t} className="badge badge-gray">{t}</span>)}
400
- {p.domains.map(d => <span key={d} className="badge badge-blue">{d}</span>)}
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
- <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-end', gap: 4 }}>
404
- <span className={`badge ${p.status === 'active' ? 'badge-green' : 'badge-blue'}`}>{p.status}</span>
405
- <span style={{ fontSize: 10, color: 'var(--text-muted)' }}>{new Date(p.analyzedAt).toLocaleDateString()}</span>
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
- {/* Stats */}
410
- <div className="grid-3" style={{ gap: 10, marginBottom: 16 }}>
411
- <div style={{ padding: '12px 14px', background: 'var(--bg-section)', borderRadius: 'var(--radius)' }}>
412
- <div style={{ fontSize: 24, fontWeight: 800, color: 'var(--green)' }}>{p.matchedSkills.length}</div>
413
- <div style={{ fontSize: 11, color: 'var(--text-dim)' }}>Skills matched ({matchedPct}%)</div>
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: '12px 14px', background: 'var(--bg-section)', borderRadius: 'var(--radius)' }}>
416
- <div style={{ fontSize: 24, fontWeight: 800, color: highGaps.length > 0 ? 'var(--red)' : 'var(--green)' }}>{p.gaps.length}</div>
417
- <div style={{ fontSize: 11, color: 'var(--text-dim)' }}>Gaps ({highGaps.length} high priority)</div>
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: '12px 14px', background: 'var(--bg-section)', borderRadius: 'var(--radius)' }}>
420
- <div style={{ fontSize: 11, color: 'var(--text-dim)', marginBottom: 4 }}>Path</div>
421
- <div style={{ fontSize: 11, fontFamily: 'var(--font-mono)', color: 'var(--text-secondary)', wordBreak: 'break-all' }}>{p.path}</div>
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
- {/* Matched Skills */}
426
- {p.matchedSkills.length > 0 && (
427
- <div style={{ marginBottom: 14 }}>
428
- <div style={{ fontSize: 10, fontWeight: 600, color: 'var(--text-muted)', marginBottom: 6 }}>MATCHED SKILLS</div>
429
- <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
430
- {p.matchedSkills.sort((a, b) => b.relevance - a.relevance).slice(0, 8).map(m => (
431
- <div key={m.slug} style={{
432
- display: 'flex', alignItems: 'center', gap: 10,
433
- padding: '6px 10px', background: 'var(--bg-section)', borderRadius: 'var(--radius)',
434
- }}>
435
- <Link href="/network" style={{ textDecoration: 'none', fontWeight: 600, fontSize: 12, color: 'var(--text)' }}>{m.slug}</Link>
436
- <div style={{ flex: 1 }}>
437
- <div className="score-track" style={{ height: 4 }}>
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: `${m.relevance}%`,
440
- background: m.relevance >= 70 ? 'var(--green)' : m.relevance >= 40 ? 'var(--yellow)' : 'var(--text-muted)',
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
- <span style={{ fontSize: 12, fontWeight: 700, color: m.relevance >= 70 ? 'var(--green)' : 'var(--yellow)', minWidth: 32 }}>{m.relevance}%</span>
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
- {/* Gaps with Action Buttons */}
456
- {p.gaps.length > 0 && (
457
- <div style={{ marginBottom: 14 }}>
458
- <div style={{ fontSize: 10, fontWeight: 600, color: 'var(--text-muted)', marginBottom: 6 }}>GAPS β€” CLICK TO FILL</div>
459
- <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
460
- {p.gaps.map((g, i) => (
461
- <div key={i} style={{
462
- padding: '10px 14px', borderRadius: 'var(--radius)',
463
- background: g.priority === 'high' ? 'var(--red-light)' : g.priority === 'medium' ? 'var(--yellow-light)' : 'var(--bg-section)',
464
- borderLeft: `3px solid ${g.priority === 'high' ? 'var(--red)' : g.priority === 'medium' ? 'var(--yellow)' : 'var(--text-muted)'}`,
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: 4 }}>
857
+ <div style={{ display: 'flex', justifyContent: 'space-between', gap: 12, alignItems: 'flex-start', flexWrap: 'wrap', marginBottom: 8 }}>
467
858
  <div>
468
- <span style={{ fontSize: 13, fontWeight: 700, color: 'var(--text)' }}>{g.area}</span>
469
- <span className={`badge ${g.priority === 'high' ? 'badge-red' : g.priority === 'medium' ? 'badge-yellow' : 'badge-gray'}`} style={{ marginLeft: 6 }}>{g.priority}</span>
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
- {g.suggestedAction === 'research' && (
472
- <Link href="/research" style={{ textDecoration: 'none' }}>
473
- <button style={{
474
- padding: '4px 12px', fontSize: 10, fontWeight: 600,
475
- background: 'var(--blue)', color: '#fff', border: 'none',
476
- borderRadius: 'var(--radius)', cursor: 'pointer',
477
- }}>
478
- Research this gap
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
- </Link>
481
- )}
482
- {g.suggestedAction === 'specialize' && (
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
- </button>
501
- </Link>
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
- {/* Recommendations */}
514
- {p.recommendations.length > 0 && (
515
- <div style={{ padding: '10px 14px', background: 'var(--bg-section)', borderRadius: 'var(--radius)' }}>
516
- <div style={{ fontSize: 10, fontWeight: 600, color: 'var(--text-muted)', marginBottom: 4 }}>RECOMMENDED NEXT STEPS</div>
517
- {p.recommendations.map((r, i) => (
518
- <div key={i} style={{ fontSize: 11, color: 'var(--text-dim)', lineHeight: 1.6 }}>β†’ {r}</div>
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
- <Link href="/network" style={{ textDecoration: 'none' }}>
533
- <button style={{
534
- padding: '6px 12px', fontSize: 11, fontWeight: 600,
535
- background: 'var(--bg-section)', border: '1px solid var(--border)',
536
- borderRadius: 'var(--radius)', cursor: 'pointer', color: 'var(--text-secondary)',
537
- }}>
538
- View in Skill Network
539
- </button>
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
- </div>
945
+ </article>
544
946
  )
545
947
  })}
546
948
  </div>
547
- </div>
548
- )}
949
+ )}
950
+ </SectionFrame>
549
951
 
550
- {/* Empty state */}
551
- {profiles.length === 0 && setupState === 'idle' && (
552
- <div className="card">
553
- <div className="empty-state">
554
- <div className="empty-state-icon">
555
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
556
- <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" />
557
- </svg>
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
- <div className="empty-state-title">No projects analyzed yet</div>
560
- <div className="empty-state-desc">
561
- Enter a project folder path or GitHub URL above. HelixEvo will analyze it against your {skillCount} skills,
562
- identify which ones match, and tell you what gaps need filling.
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
  }