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,6 +1,7 @@
1
1
  'use client'
2
2
 
3
- import { useState, useRef, type ReactNode, type ComponentType } from 'react'
3
+ import { useRef, useState, type ComponentType } from 'react'
4
+ import { ConsolePanel } from './console-panel'
4
5
 
5
6
  interface Action {
6
7
  id: string
@@ -22,10 +23,17 @@ interface QuickActionsProps {
22
23
 
23
24
  type RunState = 'idle' | 'running' | 'success' | 'error' | 'stopped'
24
25
 
26
+ function toneForState(state: RunState): 'neutral' | 'green' | 'red' | 'yellow' {
27
+ if (state === 'success') return 'green'
28
+ if (state === 'error') return 'red'
29
+ if (state === 'stopped') return 'yellow'
30
+ return 'neutral'
31
+ }
32
+
25
33
  export function QuickActions({ actions, flowDiagram: FlowDiagram }: QuickActionsProps) {
26
34
  const [running, setRunning] = useState<string | null>(null)
27
35
  const [state, setState] = useState<RunState>('idle')
28
- const [output, setOutput] = useState<string>('')
36
+ const [output, setOutput] = useState('')
29
37
  const abortRef = useRef<AbortController | null>(null)
30
38
  const outputRef = useRef<HTMLPreElement | null>(null)
31
39
 
@@ -61,10 +69,8 @@ export function QuickActions({ actions, flowDiagram: FlowDiagram }: QuickActions
61
69
  if (done) break
62
70
 
63
71
  buffer += decoder.decode(value, { stream: true })
64
-
65
- // Parse SSE events from buffer
66
72
  const events = buffer.split('\n\n')
67
- buffer = events.pop() ?? '' // keep incomplete event
73
+ buffer = events.pop() ?? ''
68
74
 
69
75
  for (const event of events) {
70
76
  const lines = event.split('\n')
@@ -80,11 +86,8 @@ export function QuickActions({ actions, flowDiagram: FlowDiagram }: QuickActions
80
86
  try {
81
87
  const text = JSON.parse(eventData) as string
82
88
  setOutput(prev => prev + text)
83
- // Auto-scroll to bottom
84
89
  setTimeout(() => {
85
- if (outputRef.current) {
86
- outputRef.current.scrollTop = outputRef.current.scrollHeight
87
- }
90
+ if (outputRef.current) outputRef.current.scrollTop = outputRef.current.scrollHeight
88
91
  }, 10)
89
92
  } catch {}
90
93
  }
@@ -106,8 +109,7 @@ export function QuickActions({ actions, flowDiagram: FlowDiagram }: QuickActions
106
109
  }
107
110
  }
108
111
 
109
- // If we reach here without a 'done' event, mark as complete
110
- if (state === 'running') setState('success')
112
+ setState(prev => prev === 'running' ? 'success' : prev)
111
113
  } catch (err: unknown) {
112
114
  if (err instanceof Error && err.name === 'AbortError') {
113
115
  try { await fetch('/api/run', { method: 'DELETE' }) } catch {}
@@ -130,176 +132,100 @@ export function QuickActions({ actions, flowDiagram: FlowDiagram }: QuickActions
130
132
  const handleClose = () => {
131
133
  setRunning(null)
132
134
  setState('idle')
133
- setOutput(null)
135
+ setOutput('')
134
136
  }
135
137
 
136
- const stateColor = state === 'success' ? 'var(--green)'
137
- : state === 'error' ? 'var(--red)'
138
- : state === 'stopped' ? 'var(--yellow)'
139
- : 'var(--text-secondary)'
140
-
141
- const stateBorderColor = state === 'success' ? 'var(--green-border)'
142
- : state === 'error' ? 'var(--red-border)'
143
- : state === 'stopped' ? 'var(--yellow-border)'
144
- : 'var(--border)'
145
-
146
- const stateBgColor = state === 'success' ? 'var(--green-light)'
147
- : state === 'error' ? 'var(--red-light)'
148
- : state === 'stopped' ? 'var(--yellow-light)'
149
- : 'var(--bg-section)'
138
+ const currentAction = actions.find(action => action.id === running)
150
139
 
151
140
  return (
152
141
  <>
153
- <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(240px, 1fr))', gap: 10 }}>
154
- {actions.map(action => (
155
- <button
156
- key={action.id}
157
- onClick={() => handleRun(action)}
158
- disabled={action.disabled || (running !== null && running !== action.id)}
159
- title={action.disabled ? action.disabledReason : action.description}
160
- style={{
161
- display: 'flex', flexDirection: 'column', alignItems: 'flex-start', gap: 4,
162
- padding: '14px 16px',
163
- background: action.disabled ? 'var(--bg-section)' : 'var(--bg-card)',
164
- border: `1px solid ${running === action.id && state === 'running' ? action.color : 'var(--border)'}`,
165
- borderRadius: 'var(--radius-lg)',
166
- fontSize: 12,
167
- fontWeight: 600,
168
- color: action.disabled ? 'var(--text-muted)' : 'var(--text-secondary)',
169
- cursor: action.disabled ? 'not-allowed' : 'pointer',
170
- opacity: (running !== null && running !== action.id) ? 0.4 : 1,
171
- transition: 'all 0.2s',
172
- textAlign: 'left',
173
- }}
174
- >
175
- {/* Header: icon + label */}
176
- <div style={{ display: 'flex', alignItems: 'center', gap: 7, width: '100%' }}>
177
- {running === action.id && state === 'running' ? (
178
- <span style={{
179
- width: 16, height: 16, border: '2px solid var(--border)',
180
- borderTopColor: action.color, borderRadius: '50%',
181
- animation: 'actionSpin 0.8s linear infinite',
182
- flexShrink: 0,
183
- }} />
184
- ) : (
185
- <div style={{
186
- width: 26, height: 26, borderRadius: 7, flexShrink: 0,
187
- background: action.disabled ? 'var(--bg-hover)' : `${action.color}12`,
188
- display: 'flex', alignItems: 'center', justifyContent: 'center',
189
- }}>
190
- <svg width="13" height="13" viewBox="0 0 24 24" fill="none"
191
- stroke={action.disabled ? 'var(--text-muted)' : action.color}
192
- strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
193
- <path d={action.icon} />
194
- </svg>
142
+ <div className="quick-actions-grid">
143
+ {actions.map((action) => {
144
+ const cardColor = action.disabled ? 'var(--text-muted)' : action.color
145
+ return (
146
+ <button
147
+ key={action.id}
148
+ onClick={() => handleRun(action)}
149
+ disabled={action.disabled || (running !== null && running !== action.id)}
150
+ title={action.disabled ? action.disabledReason : action.description}
151
+ className="action-card"
152
+ style={{
153
+ color: cardColor,
154
+ opacity: running !== null && running !== action.id ? 0.42 : 1,
155
+ background: action.disabled ? 'linear-gradient(180deg, rgba(239,235,228,0.88), rgba(239,235,228,0.8))' : undefined,
156
+ borderColor: running === action.id && state === 'running' ? action.color : undefined,
157
+ }}
158
+ >
159
+ <div className="action-card-header">
160
+ <div className="action-card-icon" style={{ color: cardColor, background: action.disabled ? 'rgba(255,255,255,0.55)' : `${action.color}18` }}>
161
+ {running === action.id && state === 'running' ? (
162
+ <span style={{ width: 16, height: 16, border: '2px solid rgba(97,93,86,0.28)', borderTopColor: action.color, borderRadius: '50%', animation: 'actionSpin 0.8s linear infinite' }} />
163
+ ) : (
164
+ <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
165
+ <path d={action.icon} />
166
+ </svg>
167
+ )}
195
168
  </div>
196
- )}
197
- <span style={{ fontSize: 13 }}>{action.label}</span>
198
- </div>
199
-
200
- {/* Subtitle */}
201
- {action.subtitle && (
202
- <div style={{
203
- fontSize: 11, fontWeight: 400, lineHeight: 1.45,
204
- color: action.disabled ? 'var(--text-muted)' : 'var(--text-dim)',
205
- marginTop: 2,
206
- }}>
207
- {action.disabled ? action.disabledReason : action.subtitle}
169
+ <div className="action-card-title">{action.label}</div>
208
170
  </div>
209
- )}
210
171
 
211
- {/* Flow diagram */}
212
- {FlowDiagram && action.flowSteps && !action.disabled && (
213
- <FlowDiagram steps={action.flowSteps} color={action.color} />
214
- )}
215
- </button>
216
- ))}
172
+ {action.subtitle ? (
173
+ <div className="action-card-subtitle">
174
+ {action.disabled ? action.disabledReason : action.subtitle}
175
+ </div>
176
+ ) : null}
177
+
178
+ {FlowDiagram && action.flowSteps && !action.disabled ? (
179
+ <FlowDiagram steps={action.flowSteps} color={action.color} />
180
+ ) : null}
181
+ </button>
182
+ )
183
+ })}
217
184
  </div>
218
185
 
219
- {/* Output panel */}
220
- {running && state !== 'idle' && (
221
- <div style={{
222
- marginTop: 14,
223
- background: 'var(--bg-card)',
224
- border: `1px solid ${stateBorderColor}`,
225
- borderRadius: 'var(--radius-lg)',
226
- overflow: 'hidden',
227
- }}>
228
- <div style={{
229
- display: 'flex', justifyContent: 'space-between', alignItems: 'center',
230
- padding: '10px 14px',
231
- background: stateBgColor,
232
- borderBottom: '1px solid var(--border)',
233
- }}>
234
- <div style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 12, fontWeight: 600 }}>
235
- {state === 'running' && (
236
- <span style={{
237
- width: 12, height: 12, border: '2px solid var(--border)',
238
- borderTopColor: 'var(--purple)', borderRadius: '50%',
239
- animation: 'actionSpin 0.8s linear infinite',
240
- }} />
241
- )}
242
- {state === 'success' && <span style={{ color: 'var(--green)' }}>&#10003;</span>}
243
- {state === 'error' && <span style={{ color: 'var(--red)' }}>&#10007;</span>}
244
- {state === 'stopped' && <span style={{ color: 'var(--yellow)' }}>&#9632;</span>}
245
- <span style={{ color: stateColor }}>
246
- {state === 'running' ? `Running ${actions.find(a => a.id === running)?.label}...`
247
- : state === 'success' ? 'Completed'
248
- : state === 'stopped' ? 'Stopped'
249
- : 'Failed'}
186
+ {running && state !== 'idle' ? (
187
+ <div style={{ marginTop: 16 }}>
188
+ <ConsolePanel
189
+ tone={toneForState(state)}
190
+ title={
191
+ <span>
192
+ {state === 'running' ? `Running ${currentAction?.label ?? 'Action'}…` :
193
+ state === 'success' ? `${currentAction?.label ?? 'Action'} completed` :
194
+ state === 'stopped' ? `${currentAction?.label ?? 'Action'} stopped` :
195
+ `${currentAction?.label ?? 'Action'} failed`}
250
196
  </span>
251
- </div>
252
- <div style={{ display: 'flex', gap: 6 }}>
253
- {state === 'running' && (
254
- <button onClick={handleStop} style={{
255
- background: 'var(--red)', color: '#fff', border: 'none', borderRadius: 'var(--radius)',
256
- padding: '3px 12px', fontSize: 11, fontWeight: 600, cursor: 'pointer',
257
- display: 'flex', alignItems: 'center', gap: 4,
258
- }}>
259
- <svg width="10" height="10" viewBox="0 0 24 24" fill="currentColor" stroke="none">
260
- <rect x="4" y="4" width="16" height="16" rx="2" />
261
- </svg>
262
- Stop
263
- </button>
264
- )}
265
- {state !== 'running' && (
266
- <>
267
- <button onClick={() => { window.location.reload() }} style={{
268
- background: 'none', border: '1px solid var(--border)', borderRadius: 'var(--radius)',
269
- padding: '3px 10px', fontSize: 11, fontWeight: 600, cursor: 'pointer',
270
- color: 'var(--text-secondary)',
271
- }}>
272
- Refresh
273
- </button>
274
- <button onClick={handleClose} style={{
275
- background: 'none', border: '1px solid var(--border)', borderRadius: 'var(--radius)',
276
- padding: '3px 10px', fontSize: 11, fontWeight: 600, cursor: 'pointer',
277
- color: 'var(--text-secondary)',
278
- }}>
279
- Close
280
- </button>
281
- </>
282
- )}
283
- </div>
284
- </div>
285
-
286
- <pre ref={outputRef} style={{
287
- padding: '12px 14px', margin: 0,
288
- fontSize: 11, lineHeight: 1.5,
289
- fontFamily: 'var(--font-mono)',
290
- color: 'var(--text-secondary)',
291
- maxHeight: 400, overflow: 'auto',
292
- whiteSpace: 'pre-wrap', wordBreak: 'break-word',
293
- }}>
294
- {output || (state === 'running' ? 'Starting...' : 'No output')}
295
- </pre>
197
+ }
198
+ actions={
199
+ state === 'running' ? (
200
+ <button onClick={handleStop} className="badge badge-red" style={{ border: 'none', cursor: 'pointer' }}>Stop</button>
201
+ ) : (
202
+ <div style={{ display: 'flex', gap: 8 }}>
203
+ <button onClick={() => window.location.reload()} className="badge badge-gray" style={{ border: 'none', cursor: 'pointer' }}>Refresh</button>
204
+ <button onClick={handleClose} className="badge badge-gray" style={{ border: 'none', cursor: 'pointer' }}>Close</button>
205
+ </div>
206
+ )
207
+ }
208
+ >
209
+ <pre
210
+ ref={outputRef}
211
+ style={{
212
+ margin: 0,
213
+ padding: '16px 18px',
214
+ fontSize: 11.5,
215
+ lineHeight: 1.62,
216
+ color: 'var(--text-secondary)',
217
+ whiteSpace: 'pre-wrap',
218
+ wordBreak: 'break-word',
219
+ }}
220
+ >
221
+ {output || (state === 'running' ? 'Starting…' : 'No output')}
222
+ </pre>
223
+ </ConsolePanel>
296
224
  </div>
297
- )}
225
+ ) : null}
298
226
 
299
227
  <style>{`
300
- @keyframes actionSpin {
301
- to { transform: rotate(360deg); }
302
- }
228
+ @keyframes actionSpin { to { transform: rotate(360deg); } }
303
229
  `}</style>
304
230
  </>
305
231
  )
@@ -0,0 +1,35 @@
1
+ 'use client'
2
+
3
+ import type { ReactNode } from 'react'
4
+
5
+ export function SectionFrame({
6
+ eyebrow,
7
+ title,
8
+ description,
9
+ actions,
10
+ children,
11
+ tone = 'neutral',
12
+ className = '',
13
+ }: {
14
+ eyebrow?: string
15
+ title: string
16
+ description?: ReactNode
17
+ actions?: ReactNode
18
+ children: ReactNode
19
+ tone?: 'neutral' | 'purple' | 'green' | 'blue' | 'yellow' | 'red'
20
+ className?: string
21
+ }) {
22
+ return (
23
+ <section className={`section-frame section-frame-${tone} ${className}`.trim()}>
24
+ <div className="section-frame-header">
25
+ <div>
26
+ {eyebrow ? <div className="section-frame-eyebrow">{eyebrow}</div> : null}
27
+ <div className="section-frame-title">{title}</div>
28
+ {description ? <div className="section-frame-description">{description}</div> : null}
29
+ </div>
30
+ {actions ? <div className="section-frame-actions">{actions}</div> : null}
31
+ </div>
32
+ <div className="section-frame-body">{children}</div>
33
+ </section>
34
+ )
35
+ }
@@ -0,0 +1,75 @@
1
+ 'use client'
2
+
3
+ import Link from 'next/link'
4
+ import { usePathname } from 'next/navigation'
5
+
6
+ interface NavItem {
7
+ href: string
8
+ label: string
9
+ icon: string
10
+ }
11
+
12
+ const WORKSPACE_NAV: NavItem[] = [
13
+ { href: '/', label: 'Overview', icon: 'M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-4 0a1 1 0 01-1-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 01-1 1' },
14
+ { href: '/network', label: 'Skill Network', icon: 'M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1' },
15
+ { href: '/evolution', label: 'Evolution', icon: 'M13 7h8m0 0v8m0-8l-8 8-4-4-6 6' },
16
+ { href: '/research', label: 'Research', icon: 'M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z' },
17
+ { href: '/frontier', label: 'Frontier', icon: 'M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z' },
18
+ { href: '/projects', label: 'Projects', icon: 'M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z' },
19
+ ]
20
+
21
+ const REFERENCE_NAV: NavItem[] = [
22
+ { href: '/commands', label: 'Commands', icon: 'M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z' },
23
+ { href: '/guide', label: 'Guide', icon: 'M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253' },
24
+ { href: '/changelog', label: 'Changelog', icon: 'M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z M14 2v6h6 M16 13H8 M16 17H8 M10 9H8' },
25
+ ]
26
+
27
+ function NavIcon({ d }: { d: string }) {
28
+ return (
29
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
30
+ {d.split(' M').map((segment, idx) => (
31
+ <path key={idx} d={idx === 0 ? segment : `M${segment}`} />
32
+ ))}
33
+ </svg>
34
+ )
35
+ }
36
+
37
+ function isActive(pathname: string, href: string) {
38
+ if (href === '/') return pathname === '/'
39
+ return pathname === href || pathname.startsWith(`${href}/`)
40
+ }
41
+
42
+ function NavGroup({ title, items, pathname }: { title: string; items: NavItem[]; pathname: string }) {
43
+ return (
44
+ <div className="sidebar-group">
45
+ <div className="sidebar-group-label">{title}</div>
46
+ <div className="sidebar-group-items">
47
+ {items.map((item) => {
48
+ const active = isActive(pathname, item.href)
49
+ return (
50
+ <Link
51
+ key={item.href}
52
+ href={item.href}
53
+ className={`nav-item ${active ? 'active' : ''}`}
54
+ aria-current={active ? 'page' : undefined}
55
+ >
56
+ <span className="nav-icon"><NavIcon d={item.icon} /></span>
57
+ <span>{item.label}</span>
58
+ </Link>
59
+ )
60
+ })}
61
+ </div>
62
+ </div>
63
+ )
64
+ }
65
+
66
+ export function SidebarNav() {
67
+ const pathname = usePathname() || '/'
68
+
69
+ return (
70
+ <>
71
+ <NavGroup title="Workspace" items={WORKSPACE_NAV} pathname={pathname} />
72
+ <NavGroup title="Reference" items={REFERENCE_NAV} pathname={pathname} />
73
+ </>
74
+ )
75
+ }