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.
@@ -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
+ }