total-recall-brain 3.12.2 → 3.13.0
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/frontend/package.json +2 -1
- package/frontend/src/App.tsx +44 -0
- package/frontend/src/api.ts +101 -1
- package/frontend/src/components/UsageChart.tsx +78 -0
- package/frontend/src/pages/DesignDocsPage.tsx +614 -0
- package/frontend/src/pages/InstructionsPage.tsx +583 -0
- package/frontend/src/pages/ModelsPage.tsx +280 -23
- package/frontend/src/pages/OkfPage.tsx +535 -0
- package/frontend/src/pages/OpenWikiPage.tsx +472 -0
- package/frontend/src/pages/SettingsPage.tsx +183 -0
- package/frontend/src/pages/SkillsPage.tsx +1054 -37
- package/frontend/src/pages/UsagePage.tsx +254 -1
- package/frontend/src/types.ts +160 -0
- package/package.json +1 -1
- package/src/server/api.mjs +2 -0
- package/src/server/auth.mjs +2 -2
- package/src/server/rest.mjs +207 -43
- package/src/server/route-manifest.json +12 -0
- package/src/server/routes/_shared.mjs +1 -1
|
@@ -0,0 +1,583 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react'
|
|
2
|
+
import { fetchInstructions, fetchInstructionContent, triggerRecompile } from '../api'
|
|
3
|
+
|
|
4
|
+
// ─── Types ──────────────────────────────────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
interface Surface {
|
|
7
|
+
name: string
|
|
8
|
+
filename: string
|
|
9
|
+
size: number
|
|
10
|
+
lastCompiled: string
|
|
11
|
+
active: boolean
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface SurfacesData {
|
|
15
|
+
surfaces: Surface[]
|
|
16
|
+
lastCompileTimestamp: string
|
|
17
|
+
totalNodes: number
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// ─── Known fallback surfaces when API is unavailable ────────────────────────────
|
|
21
|
+
|
|
22
|
+
const FALLBACK_SURFACES: Surface[] = [
|
|
23
|
+
{ name: 'AGENTS.md', filename: 'AGENTS.md', size: 0, lastCompiled: '', active: true },
|
|
24
|
+
{ name: 'GEMINI.md', filename: 'GEMINI.md', size: 0, lastCompiled: '', active: true },
|
|
25
|
+
{ name: 'CLAUDE.md', filename: 'CLAUDE.md', size: 0, lastCompiled: '', active: true },
|
|
26
|
+
{ name: 'INSTRUCTIONS.md', filename: 'INSTRUCTIONS.md', size: 0, lastCompiled: '', active: false },
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
// ─── Helpers ────────────────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
function formatBytes(bytes: number): string {
|
|
32
|
+
if (bytes === 0) return '—'
|
|
33
|
+
if (bytes < 1024) return `${bytes} B`
|
|
34
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
|
|
35
|
+
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function relativeTime(iso: string): string {
|
|
39
|
+
if (!iso) return 'Never'
|
|
40
|
+
const diff = Date.now() - new Date(iso).getTime()
|
|
41
|
+
const secs = Math.floor(diff / 1000)
|
|
42
|
+
if (secs < 60) return 'Just now'
|
|
43
|
+
const mins = Math.floor(secs / 60)
|
|
44
|
+
if (mins < 60) return `${mins}m ago`
|
|
45
|
+
const hrs = Math.floor(mins / 60)
|
|
46
|
+
if (hrs < 24) return `${hrs}h ago`
|
|
47
|
+
const days = Math.floor(hrs / 24)
|
|
48
|
+
return `${days}d ago`
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ─── Simple Markdown Renderer ───────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
function renderMarkdown(raw: string): React.ReactNode[] {
|
|
54
|
+
const lines = raw.split('\n')
|
|
55
|
+
const nodes: React.ReactNode[] = []
|
|
56
|
+
let i = 0
|
|
57
|
+
|
|
58
|
+
while (i < lines.length) {
|
|
59
|
+
const line = lines[i]
|
|
60
|
+
|
|
61
|
+
// Fenced code block
|
|
62
|
+
if (line.trimStart().startsWith('```')) {
|
|
63
|
+
const codeLines: string[] = []
|
|
64
|
+
i++
|
|
65
|
+
while (i < lines.length && !lines[i].trimStart().startsWith('```')) {
|
|
66
|
+
codeLines.push(lines[i])
|
|
67
|
+
i++
|
|
68
|
+
}
|
|
69
|
+
i++ // skip closing ```
|
|
70
|
+
nodes.push(
|
|
71
|
+
<pre key={`code-${i}`} style={{
|
|
72
|
+
background: 'rgba(0,0,0,0.3)',
|
|
73
|
+
padding: 12,
|
|
74
|
+
borderRadius: 6,
|
|
75
|
+
fontFamily: 'var(--font-mono, "JetBrains Mono", "Fira Code", monospace)',
|
|
76
|
+
fontSize: 12,
|
|
77
|
+
lineHeight: 1.6,
|
|
78
|
+
overflowX: 'auto',
|
|
79
|
+
margin: '8px 0',
|
|
80
|
+
border: '1px solid rgba(255,255,255,0.04)',
|
|
81
|
+
color: 'var(--text-primary)',
|
|
82
|
+
}}>
|
|
83
|
+
<code>{codeLines.join('\n')}</code>
|
|
84
|
+
</pre>
|
|
85
|
+
)
|
|
86
|
+
continue
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Headings
|
|
90
|
+
if (line.startsWith('### ')) {
|
|
91
|
+
nodes.push(<h3 key={`h3-${i}`} style={{ fontSize: 14, fontWeight: 700, color: 'var(--text-primary)', margin: '16px 0 6px' }}>{inlineFormat(line.slice(4))}</h3>)
|
|
92
|
+
i++; continue
|
|
93
|
+
}
|
|
94
|
+
if (line.startsWith('## ')) {
|
|
95
|
+
nodes.push(<h2 key={`h2-${i}`} style={{ fontSize: 16, fontWeight: 700, color: 'var(--text-primary)', margin: '20px 0 8px', borderBottom: '1px solid var(--border)', paddingBottom: 6 }}>{inlineFormat(line.slice(3))}</h2>)
|
|
96
|
+
i++; continue
|
|
97
|
+
}
|
|
98
|
+
if (line.startsWith('# ')) {
|
|
99
|
+
nodes.push(<h1 key={`h1-${i}`} style={{ fontSize: 20, fontWeight: 800, color: 'var(--text-primary)', margin: '24px 0 10px' }}>{inlineFormat(line.slice(2))}</h1>)
|
|
100
|
+
i++; continue
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Blockquote
|
|
104
|
+
if (line.startsWith('> ')) {
|
|
105
|
+
nodes.push(
|
|
106
|
+
<blockquote key={`bq-${i}`} style={{
|
|
107
|
+
borderLeft: '3px solid var(--accent)',
|
|
108
|
+
paddingLeft: 12,
|
|
109
|
+
margin: '8px 0',
|
|
110
|
+
color: 'var(--text-secondary)',
|
|
111
|
+
fontStyle: 'italic',
|
|
112
|
+
fontSize: 13,
|
|
113
|
+
}}>
|
|
114
|
+
{inlineFormat(line.slice(2))}
|
|
115
|
+
</blockquote>
|
|
116
|
+
)
|
|
117
|
+
i++; continue
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// List item
|
|
121
|
+
if (line.startsWith('- ') || line.startsWith('* ')) {
|
|
122
|
+
nodes.push(
|
|
123
|
+
<div key={`li-${i}`} style={{ display: 'flex', gap: 8, margin: '3px 0', fontSize: 13, color: 'var(--text-primary)' }}>
|
|
124
|
+
<span style={{ color: 'var(--accent)', flexShrink: 0 }}>•</span>
|
|
125
|
+
<span>{inlineFormat(line.slice(2))}</span>
|
|
126
|
+
</div>
|
|
127
|
+
)
|
|
128
|
+
i++; continue
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Empty line
|
|
132
|
+
if (line.trim() === '') {
|
|
133
|
+
nodes.push(<div key={`br-${i}`} style={{ height: 8 }} />)
|
|
134
|
+
i++; continue
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Paragraph
|
|
138
|
+
nodes.push(<p key={`p-${i}`} style={{ margin: '4px 0', fontSize: 13, lineHeight: 1.6, color: 'var(--text-primary)' }}>{inlineFormat(line)}</p>)
|
|
139
|
+
i++
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return nodes
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function inlineFormat(text: string): React.ReactNode {
|
|
146
|
+
// Process inline: **bold**, *italic*, `code`
|
|
147
|
+
const parts: React.ReactNode[] = []
|
|
148
|
+
const regex = /(\*\*(.+?)\*\*|\*(.+?)\*|`([^`]+)`)/g
|
|
149
|
+
let lastIndex = 0
|
|
150
|
+
let match: RegExpExecArray | null
|
|
151
|
+
|
|
152
|
+
while ((match = regex.exec(text)) !== null) {
|
|
153
|
+
if (match.index > lastIndex) {
|
|
154
|
+
parts.push(text.slice(lastIndex, match.index))
|
|
155
|
+
}
|
|
156
|
+
if (match[2]) {
|
|
157
|
+
parts.push(<strong key={match.index} style={{ fontWeight: 700, color: 'var(--text-primary)' }}>{match[2]}</strong>)
|
|
158
|
+
} else if (match[3]) {
|
|
159
|
+
parts.push(<em key={match.index} style={{ fontStyle: 'italic', color: 'var(--text-secondary)' }}>{match[3]}</em>)
|
|
160
|
+
} else if (match[4]) {
|
|
161
|
+
parts.push(
|
|
162
|
+
<code key={match.index} style={{
|
|
163
|
+
background: 'rgba(108,92,231,0.15)',
|
|
164
|
+
padding: '1px 5px',
|
|
165
|
+
borderRadius: 3,
|
|
166
|
+
fontFamily: 'var(--font-mono, "JetBrains Mono", "Fira Code", monospace)',
|
|
167
|
+
fontSize: '0.9em',
|
|
168
|
+
color: 'var(--accent)',
|
|
169
|
+
}}>{match[4]}</code>
|
|
170
|
+
)
|
|
171
|
+
}
|
|
172
|
+
lastIndex = match.index + match[0].length
|
|
173
|
+
}
|
|
174
|
+
if (lastIndex < text.length) {
|
|
175
|
+
parts.push(text.slice(lastIndex))
|
|
176
|
+
}
|
|
177
|
+
return parts.length === 1 ? parts[0] : <>{parts}</>
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ─── Component ──────────────────────────────────────────────────────────────────
|
|
181
|
+
|
|
182
|
+
export default function InstructionsPage() {
|
|
183
|
+
const [surfaces, setSurfaces] = useState<Surface[]>([])
|
|
184
|
+
const [selected, setSelected] = useState<Surface | null>(null)
|
|
185
|
+
const [content, setContent] = useState('')
|
|
186
|
+
const [loading, setLoading] = useState(true)
|
|
187
|
+
const [contentLoading, setContentLoading] = useState(false)
|
|
188
|
+
const [compiling, setCompiling] = useState(false)
|
|
189
|
+
const [error, setError] = useState<string | null>(null)
|
|
190
|
+
const [success, setSuccess] = useState<string | null>(null)
|
|
191
|
+
const [lastCompiled, setLastCompiled] = useState('')
|
|
192
|
+
const [totalNodes, setTotalNodes] = useState(0)
|
|
193
|
+
const [isFallback, setIsFallback] = useState(false)
|
|
194
|
+
|
|
195
|
+
// ─── Data Loading ─────────────────────────────────────────────────────────────
|
|
196
|
+
|
|
197
|
+
const loadSurfaces = async () => {
|
|
198
|
+
setLoading(true)
|
|
199
|
+
setError(null)
|
|
200
|
+
try {
|
|
201
|
+
const data: SurfacesData = await fetchInstructions()
|
|
202
|
+
|
|
203
|
+
// Handle case where API returns surfaces array
|
|
204
|
+
if (data.surfaces && Array.isArray(data.surfaces)) {
|
|
205
|
+
setSurfaces(data.surfaces)
|
|
206
|
+
setLastCompiled(data.lastCompileTimestamp || '')
|
|
207
|
+
setTotalNodes(data.totalNodes || 0)
|
|
208
|
+
setIsFallback(false)
|
|
209
|
+
if (data.surfaces.length > 0) {
|
|
210
|
+
const first = data.surfaces[0]
|
|
211
|
+
setSelected(first)
|
|
212
|
+
void loadContent(first.name)
|
|
213
|
+
}
|
|
214
|
+
} else {
|
|
215
|
+
// API returned unexpected format — use fallback
|
|
216
|
+
applyFallback()
|
|
217
|
+
}
|
|
218
|
+
} catch {
|
|
219
|
+
applyFallback()
|
|
220
|
+
} finally {
|
|
221
|
+
setLoading(false)
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const applyFallback = () => {
|
|
226
|
+
setSurfaces(FALLBACK_SURFACES)
|
|
227
|
+
setIsFallback(true)
|
|
228
|
+
setLastCompiled('')
|
|
229
|
+
setTotalNodes(0)
|
|
230
|
+
const first = FALLBACK_SURFACES[0]
|
|
231
|
+
setSelected(first)
|
|
232
|
+
setContent('Content unavailable — recompile to regenerate')
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const loadContent = async (name: string) => {
|
|
236
|
+
setContentLoading(true)
|
|
237
|
+
try {
|
|
238
|
+
const data = await fetchInstructionContent(name)
|
|
239
|
+
setContent(data.content || 'Empty surface — no compiled content yet.')
|
|
240
|
+
} catch {
|
|
241
|
+
setContent('Content unavailable — recompile to regenerate')
|
|
242
|
+
} finally {
|
|
243
|
+
setContentLoading(false)
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const handleSelect = (surface: Surface) => {
|
|
248
|
+
setSelected(surface)
|
|
249
|
+
void loadContent(surface.name)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const handleRecompile = async () => {
|
|
253
|
+
setCompiling(true)
|
|
254
|
+
setError(null)
|
|
255
|
+
setSuccess(null)
|
|
256
|
+
try {
|
|
257
|
+
const result = await triggerRecompile()
|
|
258
|
+
setSuccess(result.message || 'Surfaces recompiled successfully!')
|
|
259
|
+
setTimeout(() => setSuccess(null), 5000)
|
|
260
|
+
// Refresh surfaces after recompile
|
|
261
|
+
void loadSurfaces()
|
|
262
|
+
} catch (err: unknown) {
|
|
263
|
+
setError((err as Error).message || 'Recompilation failed.')
|
|
264
|
+
setTimeout(() => setError(null), 8000)
|
|
265
|
+
} finally {
|
|
266
|
+
setCompiling(false)
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
useEffect(() => {
|
|
271
|
+
// eslint-disable-next-line react-hooks/set-state-in-effect -- legitimate data fetch on mount
|
|
272
|
+
void loadSurfaces()
|
|
273
|
+
}, [])
|
|
274
|
+
|
|
275
|
+
// ─── Derived Stats ───────────────────────────────────────────────────────────
|
|
276
|
+
|
|
277
|
+
const activeSurfaces = surfaces.filter(s => s.active).length
|
|
278
|
+
const totalSize = surfaces.reduce((sum, s) => sum + (s.size || 0), 0)
|
|
279
|
+
|
|
280
|
+
const stats = [
|
|
281
|
+
{ id: 'stat-total', label: 'Total Surfaces', value: surfaces.length, icon: '📄', color: 'var(--accent)' },
|
|
282
|
+
{ id: 'stat-active', label: 'Active Surfaces', value: activeSurfaces, icon: '✅', color: 'var(--success)' },
|
|
283
|
+
{ id: 'stat-nodes', label: 'Total Memory Nodes', value: totalNodes || '—', icon: '🧠', color: 'var(--warning)' },
|
|
284
|
+
{ id: 'stat-compiled', label: 'Last Compiled', value: lastCompiled ? relativeTime(lastCompiled) : 'Never', icon: '🕐', color: 'var(--text-secondary)' },
|
|
285
|
+
]
|
|
286
|
+
|
|
287
|
+
// ─── Render ───────────────────────────────────────────────────────────────────
|
|
288
|
+
|
|
289
|
+
return (
|
|
290
|
+
<div className="page" id="instructions-page">
|
|
291
|
+
|
|
292
|
+
{/* Page Header */}
|
|
293
|
+
<div className="page-header" id="instructions-header" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: 16 }}>
|
|
294
|
+
<div>
|
|
295
|
+
<h1>Instruction Surfaces</h1>
|
|
296
|
+
<p style={{ color: 'var(--text-secondary)', fontSize: 13, marginTop: 4 }}>
|
|
297
|
+
Compiled rule shims injected into agent context windows
|
|
298
|
+
{isFallback && <span style={{ color: 'var(--warning)', marginLeft: 8, fontSize: 11 }}>⚠ Fallback mode</span>}
|
|
299
|
+
</p>
|
|
300
|
+
</div>
|
|
301
|
+
<button
|
|
302
|
+
id="btn-recompile-all"
|
|
303
|
+
className="btn btn-primary"
|
|
304
|
+
onClick={() => void handleRecompile()}
|
|
305
|
+
disabled={compiling}
|
|
306
|
+
style={{
|
|
307
|
+
background: compiling ? 'var(--bg-secondary)' : 'linear-gradient(135deg, var(--accent), #8b7cf7)',
|
|
308
|
+
color: '#fff',
|
|
309
|
+
border: 'none',
|
|
310
|
+
padding: '10px 20px',
|
|
311
|
+
borderRadius: 8,
|
|
312
|
+
fontWeight: 600,
|
|
313
|
+
cursor: compiling ? 'not-allowed' : 'pointer',
|
|
314
|
+
display: 'flex',
|
|
315
|
+
alignItems: 'center',
|
|
316
|
+
gap: 8,
|
|
317
|
+
fontSize: 13,
|
|
318
|
+
transition: 'all 0.2s ease',
|
|
319
|
+
}}
|
|
320
|
+
>
|
|
321
|
+
{compiling ? (
|
|
322
|
+
<>
|
|
323
|
+
<span style={{
|
|
324
|
+
width: 14, height: 14, border: '2px solid rgba(255,255,255,0.3)',
|
|
325
|
+
borderTopColor: '#fff', borderRadius: '50%',
|
|
326
|
+
animation: 'spin 0.8s linear infinite', display: 'inline-block',
|
|
327
|
+
}} />
|
|
328
|
+
Recompiling…
|
|
329
|
+
</>
|
|
330
|
+
) : (
|
|
331
|
+
<>🔄 Recompile All</>
|
|
332
|
+
)}
|
|
333
|
+
</button>
|
|
334
|
+
</div>
|
|
335
|
+
|
|
336
|
+
{/* Status Banners */}
|
|
337
|
+
{error && (
|
|
338
|
+
<div id="instructions-error" style={{
|
|
339
|
+
marginBottom: 16, padding: '10px 16px', borderRadius: 8,
|
|
340
|
+
background: 'rgba(255,107,107,0.1)', border: '1px solid rgba(255,107,107,0.3)',
|
|
341
|
+
color: 'var(--error)', fontSize: 13, display: 'flex', alignItems: 'center', gap: 8,
|
|
342
|
+
}}>
|
|
343
|
+
⚠️ {error}
|
|
344
|
+
</div>
|
|
345
|
+
)}
|
|
346
|
+
{success && (
|
|
347
|
+
<div id="instructions-success" style={{
|
|
348
|
+
marginBottom: 16, padding: '10px 16px', borderRadius: 8,
|
|
349
|
+
background: 'rgba(0,206,201,0.1)', border: '1px solid rgba(0,206,201,0.3)',
|
|
350
|
+
color: 'var(--success)', fontSize: 13, display: 'flex', alignItems: 'center', gap: 8,
|
|
351
|
+
}}>
|
|
352
|
+
✓ {success}
|
|
353
|
+
</div>
|
|
354
|
+
)}
|
|
355
|
+
|
|
356
|
+
{/* Stats Cards Row */}
|
|
357
|
+
<div id="instructions-stats" style={{
|
|
358
|
+
display: 'flex', gap: 16, marginBottom: 24, flexWrap: 'wrap',
|
|
359
|
+
}}>
|
|
360
|
+
{stats.map((stat) => (
|
|
361
|
+
<div key={stat.id} id={stat.id} className="card" style={{
|
|
362
|
+
flex: '1 1 180px',
|
|
363
|
+
padding: '18px 20px',
|
|
364
|
+
background: 'rgba(255,255,255,0.03)',
|
|
365
|
+
border: '1px solid var(--border)',
|
|
366
|
+
backdropFilter: 'blur(12px)',
|
|
367
|
+
borderRadius: 10,
|
|
368
|
+
display: 'flex',
|
|
369
|
+
flexDirection: 'column',
|
|
370
|
+
gap: 6,
|
|
371
|
+
transition: 'all 0.2s ease',
|
|
372
|
+
}}>
|
|
373
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
|
374
|
+
<span style={{ fontSize: 18 }}>{stat.icon}</span>
|
|
375
|
+
<span style={{ fontSize: 11, fontWeight: 600, color: 'var(--text-tertiary)', textTransform: 'uppercase', letterSpacing: 0.5 }}>{stat.label}</span>
|
|
376
|
+
</div>
|
|
377
|
+
<div style={{ fontSize: 24, fontWeight: 800, color: stat.color, letterSpacing: -0.5 }}>
|
|
378
|
+
{stat.value}
|
|
379
|
+
</div>
|
|
380
|
+
</div>
|
|
381
|
+
))}
|
|
382
|
+
</div>
|
|
383
|
+
|
|
384
|
+
{/* Main Split Layout */}
|
|
385
|
+
<div id="instructions-layout" style={{
|
|
386
|
+
display: 'flex', gap: 20, flex: 1, minHeight: 0, overflow: 'hidden',
|
|
387
|
+
}}>
|
|
388
|
+
|
|
389
|
+
{/* Left Panel — Surface List */}
|
|
390
|
+
<div id="surface-list-panel" style={{
|
|
391
|
+
width: 260, flexShrink: 0, display: 'flex', flexDirection: 'column',
|
|
392
|
+
overflowY: 'auto',
|
|
393
|
+
}}>
|
|
394
|
+
<div className="card" style={{
|
|
395
|
+
padding: 16,
|
|
396
|
+
background: 'rgba(255,255,255,0.03)',
|
|
397
|
+
border: '1px solid var(--border)',
|
|
398
|
+
backdropFilter: 'blur(12px)',
|
|
399
|
+
borderRadius: 10,
|
|
400
|
+
flex: 1,
|
|
401
|
+
display: 'flex',
|
|
402
|
+
flexDirection: 'column',
|
|
403
|
+
}}>
|
|
404
|
+
<h3 style={{
|
|
405
|
+
fontSize: 11, fontWeight: 700, color: 'var(--text-tertiary)',
|
|
406
|
+
textTransform: 'uppercase', letterSpacing: 0.5,
|
|
407
|
+
borderBottom: '1px solid var(--border)', paddingBottom: 8, marginBottom: 12,
|
|
408
|
+
}}>
|
|
409
|
+
Surfaces ({surfaces.length})
|
|
410
|
+
</h3>
|
|
411
|
+
|
|
412
|
+
{loading ? (
|
|
413
|
+
<div style={{ padding: 20, textAlign: 'center', color: 'var(--text-tertiary)', fontSize: 13 }}>
|
|
414
|
+
Loading surfaces…
|
|
415
|
+
</div>
|
|
416
|
+
) : (
|
|
417
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 6, flex: 1, overflowY: 'auto' }}>
|
|
418
|
+
{surfaces.map((surface) => {
|
|
419
|
+
const isActive = selected?.name === surface.name
|
|
420
|
+
return (
|
|
421
|
+
<div
|
|
422
|
+
key={surface.name}
|
|
423
|
+
id={`surface-item-${surface.name.replace(/[^a-zA-Z0-9]/g, '-')}`}
|
|
424
|
+
onClick={() => handleSelect(surface)}
|
|
425
|
+
role="button"
|
|
426
|
+
tabIndex={0}
|
|
427
|
+
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') handleSelect(surface) }}
|
|
428
|
+
style={{
|
|
429
|
+
padding: '12px 14px',
|
|
430
|
+
borderRadius: 8,
|
|
431
|
+
cursor: 'pointer',
|
|
432
|
+
transition: 'all 0.15s ease',
|
|
433
|
+
background: isActive ? 'rgba(108,92,231,0.12)' : 'transparent',
|
|
434
|
+
borderLeft: isActive ? '3px solid var(--accent)' : '3px solid transparent',
|
|
435
|
+
position: 'relative',
|
|
436
|
+
}}
|
|
437
|
+
onMouseEnter={(e) => {
|
|
438
|
+
if (!isActive) (e.currentTarget as HTMLElement).style.background = 'rgba(108,92,231,0.08)'
|
|
439
|
+
}}
|
|
440
|
+
onMouseLeave={(e) => {
|
|
441
|
+
if (!isActive) (e.currentTarget as HTMLElement).style.background = 'transparent'
|
|
442
|
+
}}
|
|
443
|
+
>
|
|
444
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 4 }}>
|
|
445
|
+
<span style={{
|
|
446
|
+
fontSize: 13, fontWeight: 600,
|
|
447
|
+
color: isActive ? 'var(--accent)' : 'var(--text-primary)',
|
|
448
|
+
}}>
|
|
449
|
+
{surface.name}
|
|
450
|
+
</span>
|
|
451
|
+
{surface.active && (
|
|
452
|
+
<span style={{
|
|
453
|
+
width: 7, height: 7, borderRadius: '50%',
|
|
454
|
+
background: 'var(--success)',
|
|
455
|
+
boxShadow: '0 0 6px rgba(0,206,201,0.4)',
|
|
456
|
+
display: 'inline-block', flexShrink: 0,
|
|
457
|
+
}} />
|
|
458
|
+
)}
|
|
459
|
+
</div>
|
|
460
|
+
<div style={{
|
|
461
|
+
display: 'flex', gap: 10, fontSize: 11, color: 'var(--text-tertiary)',
|
|
462
|
+
}}>
|
|
463
|
+
<span>{formatBytes(surface.size)}</span>
|
|
464
|
+
{surface.lastCompiled && <span>{relativeTime(surface.lastCompiled)}</span>}
|
|
465
|
+
</div>
|
|
466
|
+
</div>
|
|
467
|
+
)
|
|
468
|
+
})}
|
|
469
|
+
</div>
|
|
470
|
+
)}
|
|
471
|
+
|
|
472
|
+
{/* Total size footer */}
|
|
473
|
+
{!loading && surfaces.length > 0 && (
|
|
474
|
+
<div style={{
|
|
475
|
+
borderTop: '1px solid var(--border)', marginTop: 12, paddingTop: 10,
|
|
476
|
+
fontSize: 11, color: 'var(--text-tertiary)', display: 'flex', justifyContent: 'space-between',
|
|
477
|
+
}}>
|
|
478
|
+
<span>Total</span>
|
|
479
|
+
<span>{formatBytes(totalSize)}</span>
|
|
480
|
+
</div>
|
|
481
|
+
)}
|
|
482
|
+
</div>
|
|
483
|
+
</div>
|
|
484
|
+
|
|
485
|
+
{/* Right Panel — Content Viewer */}
|
|
486
|
+
<div id="surface-content-panel" style={{
|
|
487
|
+
flex: 1, display: 'flex', flexDirection: 'column', minWidth: 0,
|
|
488
|
+
}}>
|
|
489
|
+
<div className="card" style={{
|
|
490
|
+
padding: 24,
|
|
491
|
+
background: 'rgba(255,255,255,0.03)',
|
|
492
|
+
border: '1px solid var(--border)',
|
|
493
|
+
backdropFilter: 'blur(12px)',
|
|
494
|
+
borderRadius: 10,
|
|
495
|
+
flex: 1,
|
|
496
|
+
display: 'flex',
|
|
497
|
+
flexDirection: 'column',
|
|
498
|
+
overflow: 'hidden',
|
|
499
|
+
}}>
|
|
500
|
+
{/* Content Header */}
|
|
501
|
+
<div style={{
|
|
502
|
+
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
|
|
503
|
+
borderBottom: '1px solid var(--border)', paddingBottom: 12, marginBottom: 16,
|
|
504
|
+
}}>
|
|
505
|
+
<div>
|
|
506
|
+
<h2 style={{ fontSize: 15, fontWeight: 700, color: 'var(--text-primary)', margin: 0 }}>
|
|
507
|
+
{selected ? selected.name : 'Select a surface'}
|
|
508
|
+
</h2>
|
|
509
|
+
{selected && (
|
|
510
|
+
<div style={{ fontSize: 11, color: 'var(--text-tertiary)', marginTop: 4, display: 'flex', gap: 12 }}>
|
|
511
|
+
<span>{formatBytes(selected.size)}</span>
|
|
512
|
+
{selected.lastCompiled && <span>Compiled {relativeTime(selected.lastCompiled)}</span>}
|
|
513
|
+
{selected.active && (
|
|
514
|
+
<span className="badge badge-success" style={{
|
|
515
|
+
fontSize: 10, padding: '1px 8px', borderRadius: 4,
|
|
516
|
+
background: 'rgba(0,206,201,0.12)', color: 'var(--success)',
|
|
517
|
+
border: '1px solid rgba(0,206,201,0.2)',
|
|
518
|
+
}}>
|
|
519
|
+
Active
|
|
520
|
+
</span>
|
|
521
|
+
)}
|
|
522
|
+
</div>
|
|
523
|
+
)}
|
|
524
|
+
</div>
|
|
525
|
+
</div>
|
|
526
|
+
|
|
527
|
+
{/* Content Body */}
|
|
528
|
+
<div id="surface-content-body" style={{
|
|
529
|
+
flex: 1, overflowY: 'auto', overflowX: 'hidden',
|
|
530
|
+
paddingRight: 8,
|
|
531
|
+
}}>
|
|
532
|
+
{contentLoading ? (
|
|
533
|
+
<div style={{ padding: 40, textAlign: 'center', color: 'var(--text-tertiary)' }}>
|
|
534
|
+
<div style={{
|
|
535
|
+
width: 28, height: 28, border: '3px solid var(--border)',
|
|
536
|
+
borderTopColor: 'var(--accent)', borderRadius: '50%',
|
|
537
|
+
animation: 'spin 0.8s linear infinite',
|
|
538
|
+
margin: '0 auto 12px',
|
|
539
|
+
}} />
|
|
540
|
+
Loading surface content…
|
|
541
|
+
</div>
|
|
542
|
+
) : !selected ? (
|
|
543
|
+
<div style={{
|
|
544
|
+
padding: 60, textAlign: 'center', color: 'var(--text-tertiary)', fontSize: 14,
|
|
545
|
+
}}>
|
|
546
|
+
<div style={{ fontSize: 40, marginBottom: 12 }}>📋</div>
|
|
547
|
+
Select a surface from the left to view its compiled content
|
|
548
|
+
</div>
|
|
549
|
+
) : (
|
|
550
|
+
<div style={{ lineHeight: 1.6 }}>
|
|
551
|
+
{renderMarkdown(content)}
|
|
552
|
+
</div>
|
|
553
|
+
)}
|
|
554
|
+
</div>
|
|
555
|
+
</div>
|
|
556
|
+
</div>
|
|
557
|
+
</div>
|
|
558
|
+
|
|
559
|
+
{/* Inline keyframe for spinner */}
|
|
560
|
+
<style>{`
|
|
561
|
+
@keyframes spin {
|
|
562
|
+
to { transform: rotate(360deg); }
|
|
563
|
+
}
|
|
564
|
+
/* Responsive: stack panels vertically at 768px */
|
|
565
|
+
@media (max-width: 768px) {
|
|
566
|
+
#instructions-layout {
|
|
567
|
+
flex-direction: column !important;
|
|
568
|
+
}
|
|
569
|
+
#surface-list-panel {
|
|
570
|
+
width: 100% !important;
|
|
571
|
+
max-height: 260px;
|
|
572
|
+
}
|
|
573
|
+
#instructions-stats {
|
|
574
|
+
flex-direction: column !important;
|
|
575
|
+
}
|
|
576
|
+
#instructions-stats .card {
|
|
577
|
+
flex: 1 1 100% !important;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
`}</style>
|
|
581
|
+
</div>
|
|
582
|
+
)
|
|
583
|
+
}
|