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.
- package/CHANGELOG.md +16 -0
- package/dashboard/app/changelog/page.tsx +179 -54
- package/dashboard/app/commands/page.tsx +232 -155
- package/dashboard/app/evolution/page.tsx +105 -102
- package/dashboard/app/frontier/page.tsx +103 -100
- package/dashboard/app/globals.css +1105 -403
- package/dashboard/app/guide/page.tsx +46 -2
- package/dashboard/app/layout.tsx +28 -57
- package/dashboard/app/network/client.tsx +453 -269
- package/dashboard/app/network/page.tsx +12 -2
- package/dashboard/app/page.tsx +166 -185
- package/dashboard/app/projects/client.tsx +891 -509
- package/dashboard/app/research/client.tsx +180 -248
- package/dashboard/components/SkillFlowNode.tsx +86 -128
- package/dashboard/components/console-panel.tsx +25 -0
- package/dashboard/components/metric-card.tsx +45 -0
- package/dashboard/components/overview-actions.tsx +29 -40
- package/dashboard/components/page-hero.tsx +44 -0
- package/dashboard/components/quick-actions.tsx +93 -167
- package/dashboard/components/section-frame.tsx +35 -0
- package/dashboard/components/sidebar-nav.tsx +75 -0
- package/dashboard/components/update-banner.tsx +101 -145
- package/dashboard/lib/data.ts +2 -2
- package/package.json +1 -1
|
@@ -1,37 +1,123 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { useMemo, useRef, useState, type CSSProperties } from 'react'
|
|
4
4
|
import Link from 'next/link'
|
|
5
|
+
import { ConsolePanel } from '@/components/console-panel'
|
|
6
|
+
import { MetricCard } from '@/components/metric-card'
|
|
7
|
+
import { PageHero } from '@/components/page-hero'
|
|
8
|
+
import { SectionFrame } from '@/components/section-frame'
|
|
5
9
|
import type { ProjectProfile } from '@/lib/data'
|
|
6
10
|
|
|
7
|
-
type SetupState = 'idle' | 'analyzing' | 'done' | 'error'
|
|
11
|
+
type SetupState = 'idle' | 'analyzing' | 'done' | 'error' | 'cancelled'
|
|
8
12
|
type InputMode = 'local' | 'github'
|
|
13
|
+
type StatusTone = 'neutral' | 'green' | 'red' | 'yellow' | 'blue' | 'purple'
|
|
9
14
|
|
|
10
15
|
const PIPELINE = [
|
|
11
16
|
{
|
|
12
|
-
step: 1,
|
|
13
|
-
|
|
14
|
-
|
|
17
|
+
step: 1,
|
|
18
|
+
title: 'Analyze structure',
|
|
19
|
+
command: 'helixevo project-setup <path>',
|
|
20
|
+
desc: 'Read README, package manifests, config files, and the source tree to understand what the project is and how it is built.',
|
|
21
|
+
tone: 'blue' as const,
|
|
15
22
|
},
|
|
16
23
|
{
|
|
17
|
-
step: 2,
|
|
18
|
-
|
|
19
|
-
|
|
24
|
+
step: 2,
|
|
25
|
+
title: 'Match capabilities',
|
|
26
|
+
command: 'match existing skills',
|
|
27
|
+
desc: 'Score your current skill network against the project so only relevant skills remain in the working set.',
|
|
28
|
+
tone: 'purple' as const,
|
|
20
29
|
},
|
|
21
30
|
{
|
|
22
|
-
step: 3,
|
|
23
|
-
|
|
24
|
-
|
|
31
|
+
step: 3,
|
|
32
|
+
title: 'Expose gaps',
|
|
33
|
+
command: 'detect uncovered needs',
|
|
34
|
+
desc: 'Surface capabilities the project requires but your current skills do not yet cover, with clear priority levels.',
|
|
35
|
+
tone: 'yellow' as const,
|
|
25
36
|
},
|
|
26
37
|
{
|
|
27
|
-
step: 4,
|
|
28
|
-
|
|
29
|
-
|
|
38
|
+
step: 4,
|
|
39
|
+
title: 'Route next action',
|
|
40
|
+
command: 'research β’ specialize β’ create',
|
|
41
|
+
desc: 'Turn each gap into the next best move so you can research, specialize a skill, or create a new one with intent.',
|
|
42
|
+
tone: 'green' as const,
|
|
30
43
|
},
|
|
31
44
|
]
|
|
32
45
|
|
|
33
|
-
|
|
34
|
-
|
|
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)
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
|
73
|
-
|
|
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
|
-
|
|
209
|
+
if (inputMode === 'github') {
|
|
210
|
+
path = cloneTarget
|
|
86
211
|
setSetupState('analyzing')
|
|
87
|
-
setOutput(`
|
|
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
|
-
|
|
95
|
-
setOutput(prev => prev +
|
|
96
|
-
|
|
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) {
|
|
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 = ''
|
|
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(() => {
|
|
138
|
-
|
|
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
|
|
143
|
-
setSetupState(
|
|
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
|
-
|
|
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[
|
|
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
|
-
<
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
-
{
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
</
|
|
267
|
-
|
|
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
|
-
<
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
color: canStart ? '#fff' : 'var(--text-muted)',
|
|
349
|
-
fontSize: 13, fontWeight: 600, cursor: canStart ? 'pointer' : 'default',
|
|
350
|
-
whiteSpace: 'nowrap',
|
|
351
|
-
}}>
|
|
352
|
-
Clone & 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={
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: '
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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
|
-
{
|
|
382
|
-
|
|
383
|
-
|
|
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
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
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={
|
|
404
|
-
|
|
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={
|
|
412
|
-
|
|
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
|
-
|
|
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
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
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
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
const
|
|
722
|
+
) : (
|
|
723
|
+
<div style={{ display: 'grid', gap: 18 }}>
|
|
724
|
+
{profiles.map(profile => {
|
|
725
|
+
const highGaps = profile.gaps.filter(gap => gap.priority === 'high')
|
|
726
|
+
const matchedPct = skillCount > 0 ? Math.round((profile.matchedSkills.length / skillCount) * 100) : 0
|
|
434
727
|
|
|
435
728
|
return (
|
|
436
|
-
<
|
|
437
|
-
<div
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
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
|
-
|
|
449
|
-
|
|
450
|
-
<
|
|
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
|
-
|
|
455
|
-
<div className="grid-3"
|
|
456
|
-
<div style={{ padding: '
|
|
457
|
-
<div style={{ fontSize:
|
|
458
|
-
<div style={{ fontSize:
|
|
791
|
+
<div className="card-body" style={{ display: 'grid', gap: 18 }}>
|
|
792
|
+
<div className="grid-3">
|
|
793
|
+
<div style={{ padding: '14px 16px', borderRadius: 18, background: 'rgba(16,185,129,0.08)', border: '1px solid rgba(16,185,129,0.16)' }}>
|
|
794
|
+
<div style={{ fontSize: 11, color: 'var(--green)', marginBottom: 6, textTransform: 'uppercase', letterSpacing: 0.4 }}>Matched skills</div>
|
|
795
|
+
<div style={{ fontSize: 26, fontWeight: 800, color: 'var(--green)', marginBottom: 4 }}>{profile.matchedSkills.length}</div>
|
|
796
|
+
<div style={{ fontSize: 12, color: 'var(--text-dim)' }}>Capability matches strong enough to activate for this project.</div>
|
|
459
797
|
</div>
|
|
460
|
-
<div style={{ padding: '
|
|
461
|
-
<div style={{ fontSize:
|
|
462
|
-
<div style={{ fontSize:
|
|
798
|
+
<div style={{ padding: '14px 16px', borderRadius: 18, background: highGaps.length > 0 ? 'var(--red-light)' : 'rgba(16,185,129,0.08)', border: `1px solid ${highGaps.length > 0 ? 'var(--red-border)' : 'rgba(16,185,129,0.16)'}` }}>
|
|
799
|
+
<div style={{ fontSize: 11, color: highGaps.length > 0 ? 'var(--red)' : 'var(--green)', marginBottom: 6, textTransform: 'uppercase', letterSpacing: 0.4 }}>High-priority gaps</div>
|
|
800
|
+
<div style={{ fontSize: 26, fontWeight: 800, color: highGaps.length > 0 ? 'var(--red)' : 'var(--green)', marginBottom: 4 }}>{highGaps.length}</div>
|
|
801
|
+
<div style={{ fontSize: 12, color: 'var(--text-dim)' }}>{profile.gaps.length} total uncovered capability area{profile.gaps.length === 1 ? '' : 's'}.</div>
|
|
463
802
|
</div>
|
|
464
|
-
<div style={{ padding: '
|
|
465
|
-
<div style={{ fontSize: 11, color: 'var(--
|
|
466
|
-
<div style={{ fontSize:
|
|
803
|
+
<div style={{ padding: '14px 16px', borderRadius: 18, background: 'rgba(107,73,223,0.08)', border: '1px solid rgba(107,73,223,0.16)' }}>
|
|
804
|
+
<div style={{ fontSize: 11, color: 'var(--purple)', marginBottom: 6, textTransform: 'uppercase', letterSpacing: 0.4 }}>Recommendations</div>
|
|
805
|
+
<div style={{ fontSize: 26, fontWeight: 800, color: 'var(--purple)', marginBottom: 4 }}>{profile.recommendations.length}</div>
|
|
806
|
+
<div style={{ fontSize: 12, color: 'var(--text-dim)' }}>Guided next actions distilled from the analysis pass.</div>
|
|
467
807
|
</div>
|
|
468
808
|
</div>
|
|
469
809
|
|
|
470
|
-
{
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
<div style={{
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
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: `${
|
|
485
|
-
background:
|
|
835
|
+
width: `${match.relevance}%`,
|
|
836
|
+
background: match.relevance >= 70 ? 'linear-gradient(90deg, #34d399, #059669)' : match.relevance >= 40 ? 'linear-gradient(90deg, #fbbf24, #f59e0b)' : 'linear-gradient(90deg, #d1d5db, #9ca3af)',
|
|
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
|
-
|
|
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
|
-
{
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
<div style={{
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
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:
|
|
857
|
+
<div style={{ display: 'flex', justifyContent: 'space-between', gap: 12, alignItems: 'flex-start', flexWrap: 'wrap', marginBottom: 8 }}>
|
|
512
858
|
<div>
|
|
513
|
-
<
|
|
514
|
-
|
|
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
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
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
|
-
|
|
526
|
-
|
|
527
|
-
|
|
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
|
-
</
|
|
546
|
-
|
|
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
|
-
{
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
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
|
-
<
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
|
|
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
|
-
</
|
|
945
|
+
</article>
|
|
589
946
|
)
|
|
590
947
|
})}
|
|
591
948
|
</div>
|
|
592
|
-
|
|
593
|
-
|
|
949
|
+
)}
|
|
950
|
+
</SectionFrame>
|
|
594
951
|
|
|
595
|
-
{
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
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: '
|
|
628
|
-
|
|
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:
|
|
632
|
-
|
|
633
|
-
|
|
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
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
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
|
-
|
|
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
|
-
|
|
655
|
-
border: 'none', cursor: 'pointer', color: 'var(--text-dim)',
|
|
656
|
-
}}>
|
|
657
|
-
×
|
|
1014
|
+
<button onClick={() => setShowBrowser(false)} style={buttonStyle('neutral')}>
|
|
1015
|
+
Close
|
|
658
1016
|
</button>
|
|
659
1017
|
</div>
|
|
660
1018
|
</div>
|
|
661
1019
|
|
|
662
|
-
{
|
|
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:
|
|
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:
|
|
1024
|
+
<div style={{ padding: 32, textAlign: 'center', color: 'var(--text-dim)', fontSize: 13 }}>This directory is empty.</div>
|
|
668
1025
|
) : (
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
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
|
}
|