helixevo 0.2.39 β†’ 0.2.40

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,37 +1,123 @@
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
 
33
- interface BrowseItem { name: string; isDirectory: boolean; isProject: boolean }
34
- interface BrowseResult { path: string; displayPath: string; parent: string | null; items: BrowseItem[] }
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
+ }
35
121
 
36
122
  export default function ProjectsClient({ profiles, skillCount }: { profiles: ProjectProfile[]; skillCount: number }) {
37
123
  const [inputMode, setInputMode] = useState<InputMode>('local')
@@ -40,28 +126,53 @@ export default function ProjectsClient({ profiles, skillCount }: { profiles: Pro
40
126
  const [cloneDir, setCloneDir] = useState('~/HelixEvo')
41
127
  const [setupState, setSetupState] = useState<SetupState>('idle')
42
128
  const [output, setOutput] = useState('')
129
+ const [projectActionMessage, setProjectActionMessage] = useState<{ tone: StatusTone; text: string } | null>(null)
130
+ const [specializingProject, setSpecializingProject] = useState<string | null>(null)
43
131
  const outputRef = useRef<HTMLPreElement | null>(null)
44
132
  const abortRef = useRef<AbortController | null>(null)
45
133
 
46
- // Folder browser state
47
134
  const [showBrowser, setShowBrowser] = useState(false)
48
135
  const [browseData, setBrowseData] = useState<BrowseResult | null>(null)
49
136
  const [browseLoading, setBrowseLoading] = useState(false)
50
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}`
158
+ const canStart = inputMode === 'local' ? !!projectPath.trim() : !!githubUrl.trim()
159
+
51
160
  const browseTo = async (dir: string) => {
52
161
  setBrowseLoading(true)
53
162
  try {
54
163
  const res = await fetch(`/api/browse?dir=${encodeURIComponent(dir)}`)
55
- const data = await res.json()
56
- if (data.error) return
57
- setBrowseData(data)
58
- } catch {}
59
- setBrowseLoading(false)
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
+ }
60
171
  }
61
172
 
62
173
  const openBrowser = () => {
63
174
  setShowBrowser(true)
64
- browseTo('~')
175
+ void browseTo('~')
65
176
  }
66
177
 
67
178
  const selectFolder = (path: string) => {
@@ -69,38 +180,56 @@ export default function ProjectsClient({ profiles, skillCount }: { profiles: Pro
69
180
  setShowBrowser(false)
70
181
  }
71
182
 
72
- const effectivePath = inputMode === 'local' ? projectPath.trim() : ''
73
- const canStart = inputMode === 'local' ? !!projectPath.trim() : !!githubUrl.trim()
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
+ }
74
202
 
75
203
  const handleSetup = async () => {
76
204
  if (!canStart) return
77
205
 
206
+ setProjectActionMessage(null)
78
207
  let path = ''
79
- if (inputMode === 'github') {
80
- // Extract repo name from URL for clone path
81
- const repoMatch = githubUrl.match(/github\.com\/[\w-]+\/([\w.-]+)/)
82
- const repoName = repoMatch?.[1]?.replace(/\.git$/, '') ?? 'repo'
83
- path = `${cloneDir}/${repoName}`
84
208
 
85
- // Clone first
209
+ if (inputMode === 'github') {
210
+ path = cloneTarget
86
211
  setSetupState('analyzing')
87
- setOutput(`Cloning ${githubUrl} to ${path}...\n`)
212
+ setOutput(`Repository source: ${githubUrl}\nClone target: ${path}\n\n`)
213
+
88
214
  try {
89
215
  const cloneRes = await fetch('/api/run', {
90
216
  method: 'POST',
91
217
  headers: { 'Content-Type': 'application/json' },
92
218
  body: JSON.stringify({ command: 'clone', url: githubUrl, dir: path }),
93
219
  })
94
- // For now, use a simple approach
95
- setOutput(prev => prev + `Clone target: ${path}\n(Note: Git clone from dashboard requires the repo to already be cloned locally)\n\n`)
96
- } 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
+ }
97
227
  } else {
98
228
  path = projectPath.trim()
229
+ setSetupState('analyzing')
230
+ setOutput(`Project source: ${path}\n\nLaunching project analysis…\n\n`)
99
231
  }
100
232
 
101
- setSetupState('analyzing')
102
- setOutput('')
103
-
104
233
  const controller = new AbortController()
105
234
  abortRef.current = controller
106
235
 
@@ -112,47 +241,67 @@ export default function ProjectsClient({ profiles, skillCount }: { profiles: Pro
112
241
  signal: controller.signal,
113
242
  })
114
243
 
115
- 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
+ }
116
249
 
117
250
  const reader = res.body.getReader()
118
251
  const decoder = new TextDecoder()
119
252
  let sseBuffer = ''
253
+ let sawDoneEvent = false
120
254
 
121
255
  while (true) {
122
256
  const { done, value } = await reader.read()
123
257
  if (done) break
258
+
124
259
  sseBuffer += decoder.decode(value, { stream: true })
125
260
  const events = sseBuffer.split('\n\n')
126
261
  sseBuffer = events.pop() ?? ''
262
+
127
263
  for (const event of events) {
128
264
  const lines = event.split('\n')
129
- let eventType = '', eventData = ''
265
+ let eventType = ''
266
+ let eventData = ''
267
+
130
268
  for (const line of lines) {
131
269
  if (line.startsWith('event: ')) eventType = line.slice(7)
132
270
  if (line.startsWith('data: ')) eventData = line.slice(6)
133
271
  }
272
+
134
273
  if (eventType === 'output' && eventData) {
135
274
  try {
136
275
  setOutput(prev => prev + (JSON.parse(eventData) as string))
137
- setTimeout(() => { if (outputRef.current) outputRef.current.scrollTop = outputRef.current.scrollHeight }, 10)
138
- } catch {}
276
+ setTimeout(() => {
277
+ if (outputRef.current) outputRef.current.scrollTop = outputRef.current.scrollHeight
278
+ }, 10)
279
+ } catch {
280
+ // ignore malformed output events
281
+ }
139
282
  }
283
+
140
284
  if (eventType === 'done' && eventData) {
285
+ sawDoneEvent = true
141
286
  try {
142
- const r = JSON.parse(JSON.parse(eventData) as string) as { success: boolean }
143
- setSetupState(r.success ? 'done' : 'error')
144
- } 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
+ }
145
292
  }
146
293
  }
147
294
  }
148
- if (setupState === 'analyzing') setSetupState('done')
295
+
296
+ if (!sawDoneEvent) setSetupState('done')
149
297
  } catch (err: unknown) {
150
298
  if (err instanceof Error && err.name === 'AbortError') {
151
- setOutput(prev => prev + '\n\n[Cancelled]')
299
+ setOutput(prev => prev + '\n\n[Analysis cancelled by user]')
300
+ setSetupState('cancelled')
152
301
  } else {
153
302
  setOutput(prev => prev || 'Network error')
303
+ setSetupState('error')
154
304
  }
155
- setSetupState('error')
156
305
  } finally {
157
306
  abortRef.current = null
158
307
  }
@@ -160,551 +309,784 @@ export default function ProjectsClient({ profiles, skillCount }: { profiles: Pro
160
309
 
161
310
  return (
162
311
  <div>
163
- <div className="page-header">
164
- <h1 className="page-title">Project Setup</h1>
165
- <p className="page-desc">
166
- Analyze any project to match your {skillCount} skills, identify gaps, and prepare for work
167
- </p>
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>
328
+ </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
+ />
168
366
  </div>
169
367
 
170
- {/* Pipeline Visualization */}
171
- <div className="card" style={{ marginBottom: 24, padding: '22px 26px' }}>
172
- <div style={{ fontSize: 10, fontWeight: 700, color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: 1.5, marginBottom: 16 }}>
173
- How Project Setup Works
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}
174
392
  </div>
175
- <div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 0 }}>
176
- {PIPELINE.map((step, i) => (
177
- <div key={step.step} style={{ display: 'flex', alignItems: 'flex-start' }}>
178
- <div style={{ flex: 1 }}>
179
- <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}>
180
- <div style={{
181
- width: 32, height: 32, borderRadius: 9,
182
- background: `${step.color}15`,
183
- display: 'flex', alignItems: 'center', justifyContent: 'center',
184
- fontSize: 14, flexShrink: 0,
185
- }}>{step.icon}</div>
186
- <div>
187
- <div style={{ fontSize: 9, fontWeight: 600, color: step.color, letterSpacing: 0.5 }}>STEP {step.step}</div>
188
- <div style={{ fontSize: 13, fontWeight: 700, color: 'var(--text)' }}>{step.title}</div>
189
- </div>
190
- </div>
191
- <div style={{ fontSize: 11, color: 'var(--text-dim)', lineHeight: 1.5, paddingRight: 12, marginBottom: 6 }}>
192
- {step.desc}
193
- </div>
194
- <code style={{ fontSize: 9, padding: '2px 6px', background: 'var(--bg-section)', borderRadius: 3, color: 'var(--text-muted)' }}>
195
- {step.command}
196
- </code>
197
- </div>
198
- {i < 3 && (
199
- <span style={{ padding: '12px 6px 0', color: 'var(--text-muted)', fontSize: 14 }}>&rarr;</span>
200
- )}
393
+ ) : null}
394
+
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')}
406
+ style={{
407
+ ...buttonStyle(inputMode === 'local' ? 'purple' : 'neutral', inputMode === 'local'),
408
+ padding: '9px 16px',
409
+ minWidth: 130,
410
+ }}
411
+ >
412
+ Local folder
413
+ </button>
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
423
+ </button>
201
424
  </div>
202
- ))}
203
- </div>
204
- </div>
205
425
 
206
- {/* Input Section */}
207
- <div className="card" style={{ marginBottom: 24, padding: '20px 24px' }}>
208
- {/* Mode Toggle */}
209
- <div style={{ display: 'flex', gap: 0, marginBottom: 16 }}>
210
- <button
211
- onClick={() => setInputMode('local')}
212
- style={{
213
- padding: '7px 16px', fontSize: 12, fontWeight: 600, cursor: 'pointer',
214
- background: inputMode === 'local' ? 'var(--bg-card)' : 'var(--bg-section)',
215
- border: `1px solid ${inputMode === 'local' ? 'var(--border-focus)' : 'var(--border)'}`,
216
- borderRadius: '8px 0 0 8px', color: inputMode === 'local' ? 'var(--text)' : 'var(--text-dim)',
217
- }}
218
- >
219
- πŸ“ Local Folder
220
- </button>
221
- <button
222
- onClick={() => setInputMode('github')}
223
- style={{
224
- padding: '7px 16px', fontSize: 12, fontWeight: 600, cursor: 'pointer',
225
- background: inputMode === 'github' ? 'var(--bg-card)' : 'var(--bg-section)',
226
- borderTop: `1px solid ${inputMode === 'github' ? 'var(--border-focus)' : 'var(--border)'}`,
227
- borderRight: `1px solid ${inputMode === 'github' ? 'var(--border-focus)' : 'var(--border)'}`,
228
- borderBottom: `1px solid ${inputMode === 'github' ? 'var(--border-focus)' : 'var(--border)'}`,
229
- borderLeft: 'none',
230
- borderRadius: '0 8px 8px 0',
231
- color: inputMode === 'github' ? 'var(--text)' : 'var(--text-dim)',
232
- }}
233
- >
234
- GitHub URL
235
- </button>
236
- </div>
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>
237
462
 
238
- {/* Local Folder Input */}
239
- {inputMode === 'local' && (
240
- <div style={{ display: 'flex', gap: 10, alignItems: 'flex-end' }}>
241
- <div style={{ flex: 1 }}>
242
- <label style={{ fontSize: 11, fontWeight: 600, color: 'var(--text-dim)', display: 'block', marginBottom: 5 }}>
243
- Project folder path
244
- </label>
245
- <div style={{ display: 'flex', gap: 6 }}>
246
- <input
247
- value={projectPath}
248
- onChange={e => setProjectPath(e.target.value)}
249
- placeholder="/path/to/your/project"
250
- disabled={setupState === 'analyzing'}
251
- style={{
252
- flex: 1, padding: '9px 14px',
253
- border: '1px solid var(--border)', borderRadius: 'var(--radius)',
254
- fontSize: 13, fontFamily: 'var(--font-mono)',
255
- background: 'var(--bg-input)', color: 'var(--text)',
256
- }}
257
- />
258
- <button onClick={openBrowser} disabled={setupState === 'analyzing'} style={{
259
- padding: '9px 14px', border: '1px solid var(--border)', borderRadius: 'var(--radius)',
260
- background: 'var(--bg-card)', cursor: 'pointer', fontSize: 12, fontWeight: 600,
261
- color: 'var(--text-secondary)', display: 'flex', alignItems: 'center', gap: 5,
262
- whiteSpace: 'nowrap',
263
- }}>
264
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
265
- <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" />
266
- </svg>
267
- Browse
268
- </button>
269
- </div>
270
- <div style={{ fontSize: 10, color: 'var(--text-muted)', marginTop: 6, display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'wrap' }}>
271
- <span>Quick select:</span>
272
- {['.', '~/Documents/GitHub', '~/Desktop', '~/HelixEvo', ...profiles.map(p => p.path).filter(p => !['.', '~/Documents/GitHub', '~/Desktop', '~/HelixEvo'].includes(p))].map(p => (
273
- <button key={p} onClick={() => setProjectPath(p)} style={{
274
- padding: '1px 8px', fontSize: 10, background: projectPath === p ? 'var(--green-light)' : 'var(--bg-section)',
275
- border: `1px solid ${projectPath === p ? 'var(--green-border)' : 'var(--border-subtle)'}`,
276
- borderRadius: 4, cursor: 'pointer', color: projectPath === p ? 'var(--green)' : 'var(--text-dim)',
277
- fontFamily: 'var(--font-mono)',
278
- }}>{p}</button>
279
- ))}
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>
280
493
  </div>
281
- </div>
282
- {setupState !== 'analyzing' ? (
283
- <button onClick={handleSetup} disabled={!canStart} style={{
284
- padding: '9px 20px', border: 'none', borderRadius: 'var(--radius)',
285
- background: canStart ? 'var(--green)' : 'var(--bg-section)',
286
- color: canStart ? '#fff' : 'var(--text-muted)',
287
- fontSize: 13, fontWeight: 600, cursor: canStart ? 'pointer' : 'default',
288
- whiteSpace: 'nowrap',
289
- }}>
290
- Analyze Project
291
- </button>
292
494
  ) : (
293
- <button onClick={() => abortRef.current?.abort()} style={{
294
- padding: '9px 20px', border: 'none', borderRadius: 'var(--radius)',
295
- background: 'var(--red)', color: '#fff',
296
- fontSize: 13, fontWeight: 600, cursor: 'pointer', whiteSpace: 'nowrap',
297
- }}>
298
- Stop
299
- </button>
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>
541
+ </div>
300
542
  )}
301
- </div>
302
- )}
303
543
 
304
- {/* GitHub URL Input */}
305
- {inputMode === 'github' && (
306
- <div>
307
- <div style={{ display: 'flex', gap: 10, alignItems: 'flex-end', marginBottom: 10 }}>
308
- <div style={{ flex: 1 }}>
309
- <label style={{ fontSize: 11, fontWeight: 600, color: 'var(--text-dim)', display: 'block', marginBottom: 5 }}>
310
- GitHub repository URL
311
- </label>
312
- <input
313
- value={githubUrl}
314
- onChange={e => setGithubUrl(e.target.value)}
315
- placeholder="https://github.com/user/repo"
316
- disabled={setupState === 'analyzing'}
317
- style={{
318
- width: '100%', padding: '9px 14px',
319
- border: '1px solid var(--border)', borderRadius: 'var(--radius)',
320
- fontSize: 13, fontFamily: 'var(--font-mono)',
321
- background: 'var(--bg-input)', color: 'var(--text)',
322
- }}
323
- />
324
- </div>
325
- </div>
326
- <div style={{ display: 'flex', gap: 10, alignItems: 'flex-end' }}>
327
- <div style={{ flex: 1 }}>
328
- <label style={{ fontSize: 11, fontWeight: 600, color: 'var(--text-dim)', display: 'block', marginBottom: 5 }}>
329
- Clone to directory
330
- </label>
331
- <input
332
- value={cloneDir}
333
- onChange={e => setCloneDir(e.target.value)}
334
- placeholder="~/projects"
335
- disabled={setupState === 'analyzing'}
336
- style={{
337
- width: '100%', padding: '9px 14px',
338
- border: '1px solid var(--border)', borderRadius: 'var(--radius)',
339
- fontSize: 13, fontFamily: 'var(--font-mono)',
340
- background: 'var(--bg-input)', color: 'var(--text)',
341
- }}
342
- />
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>
343
559
  </div>
344
- {setupState !== 'analyzing' ? (
345
- <button onClick={handleSetup} disabled={!canStart} style={{
346
- padding: '9px 20px', border: 'none', borderRadius: 'var(--radius)',
347
- background: canStart ? 'var(--green)' : 'var(--bg-section)',
348
- color: canStart ? '#fff' : 'var(--text-muted)',
349
- fontSize: 13, fontWeight: 600, cursor: canStart ? 'pointer' : 'default',
350
- whiteSpace: 'nowrap',
351
- }}>
352
- Clone &amp; Analyze
560
+
561
+ {setupState === 'analyzing' ? (
562
+ <button onClick={() => abortRef.current?.abort()} style={buttonStyle('red', true)}>
563
+ Stop analysis
353
564
  </button>
354
565
  ) : (
355
- <button onClick={() => abortRef.current?.abort()} style={{
356
- padding: '9px 20px', border: 'none', borderRadius: 'var(--radius)',
357
- background: 'var(--red)', color: '#fff',
358
- 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',
359
570
  }}>
360
- Stop
571
+ {inputMode === 'local' ? 'Analyze project' : 'Clone & analyze'}
361
572
  </button>
362
573
  )}
363
574
  </div>
364
- <div style={{ fontSize: 10, color: 'var(--text-muted)', marginTop: 6 }}>
365
- The repository will be cloned to <code style={{ fontSize: 9 }}>{cloneDir}/{githubUrl.match(/\/([\w.-]+?)(?:\.git)?$/)?.[1] ?? 'repo'}</code>, then analyzed
366
- </div>
367
575
  </div>
368
- )}
369
- </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>
370
646
 
371
- {/* Output */}
372
- {setupState !== 'idle' && (
373
- <div className="card" style={{ marginBottom: 24, overflow: 'hidden' }}>
374
647
  <div style={{
375
- padding: '10px 16px',
376
- background: setupState === 'done' ? 'var(--green-light)' : setupState === 'error' ? 'var(--red-light)' : 'var(--bg-section)',
377
- borderBottom: '1px solid var(--border)',
378
- fontSize: 12, fontWeight: 600,
379
- 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)',
380
652
  }}>
381
- {setupState === 'analyzing' && '● Running project analysis...'}
382
- {setupState === 'done' && 'βœ“ Analysis complete β€” project profile saved'}
383
- {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>
384
659
  </div>
385
- <pre ref={outputRef} style={{
386
- padding: '14px 16px', margin: 0,
387
- fontSize: 11, lineHeight: 1.55,
388
- fontFamily: 'var(--font-mono)',
389
- color: 'var(--text-secondary)',
390
- maxHeight: 500, overflow: 'auto',
391
- whiteSpace: 'pre-wrap', wordBreak: 'break-word',
392
- }}>
393
- {output || 'Starting project analysis...'}
394
- </pre>
395
- {setupState === 'done' && (
396
- <div style={{ padding: '12px 16px', borderTop: '1px solid var(--border)', display: 'flex', gap: 8 }}>
397
- <button onClick={() => window.location.reload()} style={{
398
- padding: '7px 14px', background: 'var(--green)', color: '#fff', border: 'none',
399
- borderRadius: 'var(--radius)', fontSize: 12, fontWeight: 600, cursor: 'pointer',
400
- }}>
401
- 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
402
673
  </button>
403
- <Link href="/network" style={{ textDecoration: 'none' }}>
404
- <button style={{
405
- padding: '7px 14px', background: 'var(--bg-section)', border: '1px solid var(--border)',
406
- borderRadius: 'var(--radius)', fontSize: 12, fontWeight: 600, cursor: 'pointer', color: 'var(--text-secondary)',
407
- }}>
408
- View Skill Network
409
- </button>
674
+ <Link href="/network" style={buttonStyle('purple')}>
675
+ View skill network
410
676
  </Link>
411
- <Link href="/research" style={{ textDecoration: 'none' }}>
412
- <button style={{
413
- padding: '7px 14px', background: 'var(--bg-section)', border: '1px solid var(--border)',
414
- borderRadius: 'var(--radius)', fontSize: 12, fontWeight: 600, cursor: 'pointer', color: 'var(--text-secondary)',
415
- }}>
416
- Research Gaps
417
- </button>
677
+ <Link href="/research" style={buttonStyle('blue')}>
678
+ Research gaps
418
679
  </Link>
419
680
  </div>
420
- )}
421
- </div>
422
- )}
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}
423
701
 
424
- {/* Existing Profiles */}
425
- {profiles.length > 0 && (
426
- <div>
427
- <div style={{ fontSize: 10, fontWeight: 700, color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: 1.5, marginBottom: 12 }}>
428
- 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>
429
721
  </div>
430
- <div style={{ display: 'grid', gap: 14 }}>
431
- {profiles.map(p => {
432
- const highGaps = p.gaps.filter(g => g.priority === 'high')
433
- 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
434
727
 
435
728
  return (
436
- <div key={p.name} className="card">
437
- <div className="card-body">
438
- {/* Header */}
439
- <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 12 }}>
440
- <div>
441
- <div style={{ fontSize: 18, fontWeight: 700, marginBottom: 4 }}>{p.name}</div>
442
- <div style={{ fontSize: 12, color: 'var(--text-dim)', marginBottom: 6 }}>{p.description}</div>
443
- <div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
444
- {p.techStack.map(t => <span key={t} className="badge badge-gray">{t}</span>)}
445
- {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>)}
446
747
  </div>
447
748
  </div>
448
- <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-end', gap: 4 }}>
449
- <span className={`badge ${p.status === 'active' ? 'badge-green' : 'badge-blue'}`}>{p.status}</span>
450
- <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>
451
787
  </div>
452
788
  </div>
789
+ </div>
453
790
 
454
- {/* Stats */}
455
- <div className="grid-3" style={{ gap: 10, marginBottom: 16 }}>
456
- <div style={{ padding: '12px 14px', background: 'var(--bg-section)', borderRadius: 'var(--radius)' }}>
457
- <div style={{ fontSize: 24, fontWeight: 800, color: 'var(--green)' }}>{p.matchedSkills.length}</div>
458
- <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>
459
797
  </div>
460
- <div style={{ padding: '12px 14px', background: 'var(--bg-section)', borderRadius: 'var(--radius)' }}>
461
- <div style={{ fontSize: 24, fontWeight: 800, color: highGaps.length > 0 ? 'var(--red)' : 'var(--green)' }}>{p.gaps.length}</div>
462
- <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>
463
802
  </div>
464
- <div style={{ padding: '12px 14px', background: 'var(--bg-section)', borderRadius: 'var(--radius)' }}>
465
- <div style={{ fontSize: 11, color: 'var(--text-dim)', marginBottom: 4 }}>Path</div>
466
- <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>
467
807
  </div>
468
808
  </div>
469
809
 
470
- {/* Matched Skills */}
471
- {p.matchedSkills.length > 0 && (
472
- <div style={{ marginBottom: 14 }}>
473
- <div style={{ fontSize: 10, fontWeight: 600, color: 'var(--text-muted)', marginBottom: 6 }}>MATCHED SKILLS</div>
474
- <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
475
- {p.matchedSkills.sort((a, b) => b.relevance - a.relevance).slice(0, 8).map(m => (
476
- <div key={m.slug} style={{
477
- display: 'flex', alignItems: 'center', gap: 10,
478
- padding: '6px 10px', background: 'var(--bg-section)', borderRadius: 'var(--radius)',
479
- }}>
480
- <Link href="/network" style={{ textDecoration: 'none', fontWeight: 600, fontSize: 12, color: 'var(--text)' }}>{m.slug}</Link>
481
- <div style={{ flex: 1 }}>
482
- <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 }}>
483
834
  <div className="score-fill" style={{
484
- width: `${m.relevance}%`,
485
- 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)',
486
837
  }} />
487
838
  </div>
839
+ <div style={{ fontSize: 11.5, color: 'var(--text-dim)', lineHeight: 1.55 }}>{match.reason}</div>
488
840
  </div>
489
- <span style={{ fontSize: 12, fontWeight: 700, color: m.relevance >= 70 ? 'var(--green)' : 'var(--yellow)', minWidth: 32 }}>{m.relevance}%</span>
490
- <span style={{ fontSize: 10, color: 'var(--text-dim)', maxWidth: 200, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{m.reason}</span>
491
- </div>
492
- ))}
493
- {p.matchedSkills.length > 8 && (
494
- <div style={{ fontSize: 10, color: 'var(--text-muted)', paddingLeft: 10 }}>+{p.matchedSkills.length - 8} more skills</div>
495
- )}
841
+ ))}
496
842
  </div>
497
843
  </div>
498
- )}
499
-
500
- {/* Gaps with Action Buttons */}
501
- {p.gaps.length > 0 && (
502
- <div style={{ marginBottom: 14 }}>
503
- <div style={{ fontSize: 10, fontWeight: 600, color: 'var(--text-muted)', marginBottom: 6 }}>GAPS β€” CLICK TO FILL</div>
504
- <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
505
- {p.gaps.map((g, i) => (
506
- <div key={i} style={{
507
- padding: '10px 14px', borderRadius: 'var(--radius)',
508
- background: g.priority === 'high' ? 'var(--red-light)' : g.priority === 'medium' ? 'var(--yellow-light)' : 'var(--bg-section)',
509
- 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)',
510
856
  }}>
511
- <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 }}>
512
858
  <div>
513
- <span style={{ fontSize: 13, fontWeight: 700, color: 'var(--text)' }}>{g.area}</span>
514
- <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>
515
867
  </div>
516
- {g.suggestedAction === 'research' && (
517
- <Link href="/research" style={{ textDecoration: 'none' }}>
518
- <button style={{
519
- padding: '4px 12px', fontSize: 10, fontWeight: 600,
520
- background: 'var(--blue)', color: '#fff', border: 'none',
521
- borderRadius: 'var(--radius)', cursor: 'pointer',
522
- }}>
523
- 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'}
524
885
  </button>
525
- </Link>
526
- )}
527
- {g.suggestedAction === 'specialize' && (
528
- <button style={{
529
- padding: '4px 12px', fontSize: 10, fontWeight: 600,
530
- background: 'var(--green)', color: '#fff', border: 'none',
531
- borderRadius: 'var(--radius)', cursor: 'pointer',
532
- }}>
533
- Specialize a skill
534
- </button>
535
- )}
536
- {g.suggestedAction === 'create' && (
537
- <Link href="/network" style={{ textDecoration: 'none' }}>
538
- <button style={{
539
- padding: '4px 12px', fontSize: 10, fontWeight: 600,
540
- background: 'var(--bg-section)', color: 'var(--text-secondary)',
541
- border: '1px solid var(--border)',
542
- borderRadius: 'var(--radius)', cursor: 'pointer',
543
- }}>
886
+ ) : null}
887
+ {gap.suggestedAction === 'create' ? (
888
+ <Link href="/network" style={buttonStyle('purple')}>
544
889
  Create manually
545
- </button>
546
- </Link>
547
- )}
548
- </div>
549
- <div style={{ fontSize: 11, color: 'var(--text-dim)', lineHeight: 1.5 }}>
550
- {g.description}
890
+ </Link>
891
+ ) : null}
892
+ </div>
551
893
  </div>
552
894
  </div>
553
895
  ))}
554
896
  </div>
555
897
  </div>
556
- )}
557
-
558
- {/* Recommendations */}
559
- {p.recommendations.length > 0 && (
560
- <div style={{ padding: '10px 14px', background: 'var(--bg-section)', borderRadius: 'var(--radius)' }}>
561
- <div style={{ fontSize: 10, fontWeight: 600, color: 'var(--text-muted)', marginBottom: 4 }}>RECOMMENDED NEXT STEPS</div>
562
- {p.recommendations.map((r, i) => (
563
- <div key={i} style={{ fontSize: 11, color: 'var(--text-dim)', lineHeight: 1.6 }}>β†’ {r}</div>
564
- ))}
565
- </div>
566
- )}
567
-
568
- {/* Re-analyze button */}
569
- <div style={{ marginTop: 12, display: 'flex', gap: 8 }}>
570
- <button onClick={() => { setProjectPath(p.path); setInputMode('local'); window.scrollTo(0, 0) }} style={{
571
- padding: '6px 12px', fontSize: 11, fontWeight: 600,
572
- background: 'var(--bg-section)', border: '1px solid var(--border)',
573
- 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)',
574
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
+ >
575
928
  Re-analyze
576
929
  </button>
577
- <Link href="/network" style={{ textDecoration: 'none' }}>
578
- <button style={{
579
- padding: '6px 12px', fontSize: 11, fontWeight: 600,
580
- background: 'var(--bg-section)', border: '1px solid var(--border)',
581
- borderRadius: 'var(--radius)', cursor: 'pointer', color: 'var(--text-secondary)',
582
- }}>
583
- View in Skill Network
584
- </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
585
942
  </Link>
586
943
  </div>
587
944
  </div>
588
- </div>
945
+ </article>
589
946
  )
590
947
  })}
591
948
  </div>
592
- </div>
593
- )}
949
+ )}
950
+ </SectionFrame>
594
951
 
595
- {/* Empty state */}
596
- {profiles.length === 0 && setupState === 'idle' && (
597
- <div className="card">
598
- <div className="empty-state">
599
- <div className="empty-state-icon">
600
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
601
- <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" />
602
- </svg>
603
- </div>
604
- <div className="empty-state-title">No projects analyzed yet</div>
605
- <div className="empty-state-desc">
606
- Enter a project folder path or GitHub URL above. HelixEvo will analyze it against your {skillCount} skills,
607
- identify which ones match, and tell you what gaps need filling.
608
- </div>
609
- </div>
610
- </div>
611
- )}
612
- {/* Folder Browser Modal */}
613
- {showBrowser && (
614
- <div style={{
615
- position: 'fixed', inset: 0, zIndex: 9999,
616
- background: 'rgba(0,0,0,0.3)', display: 'flex',
617
- alignItems: 'center', justifyContent: 'center',
618
- }} onClick={() => setShowBrowser(false)}>
619
- <div onClick={e => e.stopPropagation()} style={{
620
- width: 520, maxHeight: '70vh', background: 'var(--bg-card)',
621
- borderRadius: 'var(--radius-xl)', boxShadow: 'var(--shadow-xl)',
622
- border: '1px solid var(--border)', overflow: 'hidden',
623
- display: 'flex', flexDirection: 'column',
624
- }}>
625
- {/* Header */}
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
+ >
626
981
  <div style={{
627
- padding: '14px 18px', borderBottom: '1px solid var(--border)',
628
- display: 'flex', justifyContent: 'space-between', alignItems: 'center',
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',
629
989
  }}>
630
990
  <div>
631
- <div style={{ fontSize: 14, fontWeight: 700 }}>Browse Folders</div>
632
- <div style={{ fontSize: 11, color: 'var(--text-dim)', fontFamily: 'var(--font-mono)', marginTop: 2 }}>
633
- {browseData?.displayPath ?? '~'}
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>
634
1002
  </div>
635
1003
  </div>
636
- <div style={{ display: 'flex', gap: 6 }}>
637
- {browseData?.parent && (
638
- <button onClick={() => browseTo(browseData.parent!)} style={{
639
- padding: '5px 10px', fontSize: 11, fontWeight: 600,
640
- background: 'var(--bg-section)', border: '1px solid var(--border)',
641
- borderRadius: 'var(--radius)', cursor: 'pointer', color: 'var(--text-secondary)',
642
- }}>
643
- ↑ Up
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
644
1009
  </button>
645
- )}
646
- <button onClick={() => selectFolder(browseData?.path ?? '~')} style={{
647
- padding: '5px 12px', fontSize: 11, fontWeight: 600,
648
- background: 'var(--green)', color: '#fff', border: 'none',
649
- borderRadius: 'var(--radius)', cursor: 'pointer',
650
- }}>
651
- Select This Folder
1010
+ ) : null}
1011
+ <button onClick={() => selectFolder(browseData?.path ?? '~')} style={buttonStyle('green', true)}>
1012
+ Select this folder
652
1013
  </button>
653
- <button onClick={() => setShowBrowser(false)} style={{
654
- padding: '5px 10px', fontSize: 14, background: 'none',
655
- border: 'none', cursor: 'pointer', color: 'var(--text-dim)',
656
- }}>
657
- &times;
1014
+ <button onClick={() => setShowBrowser(false)} style={buttonStyle('neutral')}>
1015
+ Close
658
1016
  </button>
659
1017
  </div>
660
1018
  </div>
661
1019
 
662
- {/* Content */}
663
- <div style={{ overflow: 'auto', flex: 1, padding: '8px 0' }}>
1020
+ <div style={{ overflow: 'auto', flex: 1, padding: '10px 12px 16px' }}>
664
1021
  {browseLoading ? (
665
- <div style={{ padding: 24, textAlign: 'center', color: 'var(--text-dim)', fontSize: 12 }}>Loading...</div>
1022
+ <div style={{ padding: 32, textAlign: 'center', color: 'var(--text-dim)', fontSize: 13 }}>Loading folder contents…</div>
666
1023
  ) : browseData?.items.length === 0 ? (
667
- <div style={{ padding: 24, textAlign: 'center', color: 'var(--text-dim)', fontSize: 12 }}>Empty directory</div>
1024
+ <div style={{ padding: 32, textAlign: 'center', color: 'var(--text-dim)', fontSize: 13 }}>This directory is empty.</div>
668
1025
  ) : (
669
- browseData?.items.map(item => (
670
- <div
671
- key={item.name}
672
- onClick={() => {
673
- if (item.isDirectory) {
674
- if (item.isProject) {
675
- selectFolder(browseData.path + '/' + item.name)
676
- } else {
677
- browseTo(browseData.path + '/' + item.name)
678
- }
679
- }
680
- }}
681
- style={{
682
- padding: '7px 18px', display: 'flex', alignItems: 'center', gap: 10,
683
- cursor: item.isDirectory ? 'pointer' : 'default',
684
- fontSize: 13, color: item.isDirectory ? 'var(--text)' : 'var(--text-dim)',
685
- background: item.isProject ? 'var(--green-light)' : 'transparent',
686
- borderLeft: item.isProject ? '3px solid var(--green)' : '3px solid transparent',
687
- }}
688
- onMouseOver={e => { if (item.isDirectory) (e.currentTarget.style.background = item.isProject ? 'var(--green-light)' : 'var(--bg-hover)') }}
689
- onMouseOut={e => { e.currentTarget.style.background = item.isProject ? 'var(--green-light)' : 'transparent' }}
690
- >
691
- <span style={{ fontSize: 14, width: 20, textAlign: 'center' }}>
692
- {item.isProject ? 'πŸ“¦' : item.isDirectory ? 'πŸ“' : 'πŸ“„'}
693
- </span>
694
- <span style={{ flex: 1, fontWeight: item.isProject ? 600 : 400 }}>{item.name}</span>
695
- {item.isProject && (
696
- <span style={{ fontSize: 10, color: 'var(--green)', fontWeight: 600 }}>project</span>
697
- )}
698
- {item.isDirectory && !item.isProject && (
699
- <span style={{ fontSize: 10, color: 'var(--text-muted)' }}>&rarr;</span>
700
- )}
701
- </div>
702
- ))
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>
703
1085
  )}
704
1086
  </div>
705
1087
  </div>
706
1088
  </div>
707
- )}
1089
+ ) : null}
708
1090
  </div>
709
1091
  )
710
1092
  }