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,535 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { runSandbox } from '../api';
|
|
3
|
+
|
|
4
|
+
type OkfTab = 'import' | 'export' | 'lint';
|
|
5
|
+
type ConflictMode = 'skip' | 'warn' | 'overwrite';
|
|
6
|
+
type CategoryOverride = 'auto' | 'invariants' | 'preferences' | 'facts' | 'concepts' | 'patterns' | 'decisions' | 'lore';
|
|
7
|
+
type ExportFormat = 'directory' | 'tar.gz';
|
|
8
|
+
type ExportScope = 'all' | 'global' | 'project';
|
|
9
|
+
|
|
10
|
+
interface LintRow {
|
|
11
|
+
slug: string;
|
|
12
|
+
field: string;
|
|
13
|
+
severity: 'error' | 'warning' | 'info';
|
|
14
|
+
message: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// ─── Styles ────────────────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
const tabBarStyle: React.CSSProperties = {
|
|
20
|
+
display: 'inline-flex',
|
|
21
|
+
gap: 0,
|
|
22
|
+
borderBottom: '1px solid var(--border)',
|
|
23
|
+
marginBottom: 24,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const tabStyle = (active: boolean): React.CSSProperties => ({
|
|
27
|
+
padding: '10px 20px',
|
|
28
|
+
background: 'transparent',
|
|
29
|
+
border: 'none',
|
|
30
|
+
borderBottom: active ? '2px solid var(--accent)' : '2px solid transparent',
|
|
31
|
+
color: active ? 'var(--text-primary)' : 'var(--text-secondary)',
|
|
32
|
+
fontWeight: active ? 600 : 400,
|
|
33
|
+
fontSize: 14,
|
|
34
|
+
cursor: 'pointer',
|
|
35
|
+
transition: 'color 0.2s, border-color 0.2s',
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const inputStyle: React.CSSProperties = {
|
|
39
|
+
background: 'var(--bg-secondary)',
|
|
40
|
+
color: 'var(--text-primary)',
|
|
41
|
+
border: '1px solid var(--border)',
|
|
42
|
+
borderRadius: 6,
|
|
43
|
+
padding: '10px 14px',
|
|
44
|
+
fontSize: 14,
|
|
45
|
+
outline: 'none',
|
|
46
|
+
width: '100%',
|
|
47
|
+
boxSizing: 'border-box',
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const selectStyle: React.CSSProperties = {
|
|
51
|
+
...inputStyle,
|
|
52
|
+
appearance: 'none',
|
|
53
|
+
backgroundImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='%239e9e9e' viewBox='0 0 16 16'%3E%3Cpath d='M8 11L3 6h10z'/%3E%3C/svg%3E")`,
|
|
54
|
+
backgroundRepeat: 'no-repeat',
|
|
55
|
+
backgroundPosition: 'right 12px center',
|
|
56
|
+
paddingRight: 32,
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const fieldGroup: React.CSSProperties = {
|
|
60
|
+
display: 'flex',
|
|
61
|
+
flexDirection: 'column',
|
|
62
|
+
gap: 6,
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const labelStyle: React.CSSProperties = {
|
|
66
|
+
fontSize: 13,
|
|
67
|
+
fontWeight: 500,
|
|
68
|
+
color: 'var(--text-secondary)',
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const cardStyle: React.CSSProperties = {
|
|
72
|
+
background: 'rgba(255,255,255,0.03)',
|
|
73
|
+
border: '1px solid var(--border)',
|
|
74
|
+
backdropFilter: 'blur(12px)',
|
|
75
|
+
borderRadius: 10,
|
|
76
|
+
padding: 24,
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const outputStyle: React.CSSProperties = {
|
|
80
|
+
background: 'var(--bg-secondary)',
|
|
81
|
+
border: '1px solid var(--border)',
|
|
82
|
+
borderRadius: 8,
|
|
83
|
+
padding: 16,
|
|
84
|
+
fontFamily: 'var(--font-mono, monospace)',
|
|
85
|
+
fontSize: 13,
|
|
86
|
+
lineHeight: 1.6,
|
|
87
|
+
color: 'var(--text-secondary)',
|
|
88
|
+
whiteSpace: 'pre-wrap',
|
|
89
|
+
wordBreak: 'break-word',
|
|
90
|
+
maxHeight: 320,
|
|
91
|
+
overflowY: 'auto',
|
|
92
|
+
marginTop: 16,
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const formGrid: React.CSSProperties = {
|
|
96
|
+
display: 'grid',
|
|
97
|
+
gridTemplateColumns: '1fr 1fr',
|
|
98
|
+
gap: 16,
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const severityColor: Record<string, string> = {
|
|
102
|
+
error: 'var(--error)',
|
|
103
|
+
warning: 'var(--warning)',
|
|
104
|
+
info: 'var(--accent)',
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
// ─── Component ─────────────────────────────────────────────────────────────────
|
|
108
|
+
|
|
109
|
+
export default function OkfPage() {
|
|
110
|
+
const [activeTab, setActiveTab] = useState<OkfTab>('import');
|
|
111
|
+
|
|
112
|
+
// ── Import state ──
|
|
113
|
+
const [importPath, setImportPath] = useState('');
|
|
114
|
+
const [conflictMode, setConflictMode] = useState<ConflictMode>('warn');
|
|
115
|
+
const [categoryOverride, setCategoryOverride] = useState<CategoryOverride>('auto');
|
|
116
|
+
const [importanceOverride, setImportanceOverride] = useState('');
|
|
117
|
+
const [importOutput, setImportOutput] = useState('');
|
|
118
|
+
const [importRunning, setImportRunning] = useState(false);
|
|
119
|
+
const [importError, setImportError] = useState(false);
|
|
120
|
+
|
|
121
|
+
// ── Export state ──
|
|
122
|
+
const [exportPath, setExportPath] = useState('');
|
|
123
|
+
const [stripMeta, setStripMeta] = useState(false);
|
|
124
|
+
const [exportFormat, setExportFormat] = useState<ExportFormat>('directory');
|
|
125
|
+
const [exportScope, setExportScope] = useState<ExportScope>('all');
|
|
126
|
+
const [exportOutput, setExportOutput] = useState('');
|
|
127
|
+
const [exportRunning, setExportRunning] = useState(false);
|
|
128
|
+
const [exportError, setExportError] = useState(false);
|
|
129
|
+
|
|
130
|
+
// ── Lint state ──
|
|
131
|
+
const [lintRows, setLintRows] = useState<LintRow[]>([]);
|
|
132
|
+
const [lintRan, setLintRan] = useState(false);
|
|
133
|
+
const [lintRunning, setLintRunning] = useState(false);
|
|
134
|
+
const [lintRawOutput, setLintRawOutput] = useState('');
|
|
135
|
+
|
|
136
|
+
// ── Handlers ──
|
|
137
|
+
|
|
138
|
+
const handleImport = async () => {
|
|
139
|
+
if (!importPath.trim()) return;
|
|
140
|
+
setImportRunning(true);
|
|
141
|
+
setImportOutput('');
|
|
142
|
+
setImportError(false);
|
|
143
|
+
try {
|
|
144
|
+
let cmd = `npx total-recall ingest okf ${JSON.stringify(importPath.trim())} --on-conflict ${conflictMode}`;
|
|
145
|
+
if (categoryOverride !== 'auto') cmd += ` --category ${categoryOverride}`;
|
|
146
|
+
const imp = parseInt(importanceOverride, 10);
|
|
147
|
+
if (imp >= 1 && imp <= 5) cmd += ` --importance ${imp}`;
|
|
148
|
+
|
|
149
|
+
const code = `
|
|
150
|
+
const { execSync } = require('child_process');
|
|
151
|
+
try {
|
|
152
|
+
const out = execSync(${JSON.stringify(cmd)}, { encoding: 'utf8', timeout: 30000 });
|
|
153
|
+
console.log(out);
|
|
154
|
+
} catch (e) {
|
|
155
|
+
console.error(e.stderr || e.message);
|
|
156
|
+
process.exit(1);
|
|
157
|
+
}`;
|
|
158
|
+
const result = await runSandbox(code, 30000);
|
|
159
|
+
setImportOutput(result.output);
|
|
160
|
+
setImportError(!result.success);
|
|
161
|
+
} catch (e) {
|
|
162
|
+
setImportOutput((e as Error).message);
|
|
163
|
+
setImportError(true);
|
|
164
|
+
} finally {
|
|
165
|
+
setImportRunning(false);
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const handleExport = async () => {
|
|
170
|
+
if (!exportPath.trim()) return;
|
|
171
|
+
setExportRunning(true);
|
|
172
|
+
setExportOutput('');
|
|
173
|
+
setExportError(false);
|
|
174
|
+
try {
|
|
175
|
+
let cmd = `npx total-recall export okf ${JSON.stringify(exportPath.trim())} --format ${exportFormat} --scope ${exportScope}`;
|
|
176
|
+
if (stripMeta) cmd += ' --strip-ssss';
|
|
177
|
+
|
|
178
|
+
const code = `
|
|
179
|
+
const { execSync } = require('child_process');
|
|
180
|
+
try {
|
|
181
|
+
const out = execSync(${JSON.stringify(cmd)}, { encoding: 'utf8', timeout: 30000 });
|
|
182
|
+
console.log(out);
|
|
183
|
+
} catch (e) {
|
|
184
|
+
console.error(e.stderr || e.message);
|
|
185
|
+
process.exit(1);
|
|
186
|
+
}`;
|
|
187
|
+
const result = await runSandbox(code, 30000);
|
|
188
|
+
setExportOutput(result.output);
|
|
189
|
+
setExportError(!result.success);
|
|
190
|
+
} catch (e) {
|
|
191
|
+
setExportOutput((e as Error).message);
|
|
192
|
+
setExportError(true);
|
|
193
|
+
} finally {
|
|
194
|
+
setExportRunning(false);
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const handleLint = async () => {
|
|
199
|
+
setLintRunning(true);
|
|
200
|
+
setLintRows([]);
|
|
201
|
+
setLintRawOutput('');
|
|
202
|
+
setLintRan(false);
|
|
203
|
+
try {
|
|
204
|
+
const code = `
|
|
205
|
+
const { execSync } = require('child_process');
|
|
206
|
+
try {
|
|
207
|
+
const out = execSync('npx total-recall lint okf --json', { encoding: 'utf8', timeout: 30000 });
|
|
208
|
+
console.log(out);
|
|
209
|
+
} catch (e) {
|
|
210
|
+
// lint exits non-zero if there are errors — output is still valid
|
|
211
|
+
if (e.stdout) console.log(e.stdout);
|
|
212
|
+
else console.error(e.stderr || e.message);
|
|
213
|
+
}`;
|
|
214
|
+
const result = await runSandbox(code, 30000);
|
|
215
|
+
setLintRawOutput(result.output);
|
|
216
|
+
|
|
217
|
+
// Try to parse JSON lint output
|
|
218
|
+
try {
|
|
219
|
+
const parsed = JSON.parse(result.output);
|
|
220
|
+
const rows: LintRow[] = (Array.isArray(parsed) ? parsed : parsed.issues || []).map(
|
|
221
|
+
(item: { slug?: string; field?: string; severity?: string; message?: string }) => ({
|
|
222
|
+
slug: item.slug || '—',
|
|
223
|
+
field: item.field || '—',
|
|
224
|
+
severity: (['error', 'warning', 'info'].includes(item.severity || '') ? item.severity : 'info') as LintRow['severity'],
|
|
225
|
+
message: item.message || '',
|
|
226
|
+
}),
|
|
227
|
+
);
|
|
228
|
+
setLintRows(rows);
|
|
229
|
+
} catch {
|
|
230
|
+
// Non-JSON output — show raw
|
|
231
|
+
}
|
|
232
|
+
setLintRan(true);
|
|
233
|
+
} catch (e) {
|
|
234
|
+
setLintRawOutput((e as Error).message);
|
|
235
|
+
setLintRan(true);
|
|
236
|
+
} finally {
|
|
237
|
+
setLintRunning(false);
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
const lintErrors = lintRows.filter(r => r.severity === 'error').length;
|
|
242
|
+
const lintWarnings = lintRows.filter(r => r.severity === 'warning').length;
|
|
243
|
+
|
|
244
|
+
// ── Render ──
|
|
245
|
+
|
|
246
|
+
return (
|
|
247
|
+
<div className="page">
|
|
248
|
+
<div className="page-header">
|
|
249
|
+
<h1>📦 Open Knowledge Format</h1>
|
|
250
|
+
<p style={{ color: 'var(--text-secondary)', margin: '4px 0 0' }}>
|
|
251
|
+
Import, export, and lint OKF bundles for interoperable memory exchange.
|
|
252
|
+
</p>
|
|
253
|
+
</div>
|
|
254
|
+
|
|
255
|
+
{/* Tab Navigation */}
|
|
256
|
+
<div style={tabBarStyle}>
|
|
257
|
+
{(['import', 'export', 'lint'] as OkfTab[]).map(tab => (
|
|
258
|
+
<button
|
|
259
|
+
key={tab}
|
|
260
|
+
id={`okf-tab-${tab}`}
|
|
261
|
+
style={tabStyle(activeTab === tab)}
|
|
262
|
+
onClick={() => setActiveTab(tab)}
|
|
263
|
+
>
|
|
264
|
+
{tab === 'import' && '⬇ Import'}
|
|
265
|
+
{tab === 'export' && '⬆ Export'}
|
|
266
|
+
{tab === 'lint' && '🔍 Lint'}
|
|
267
|
+
</button>
|
|
268
|
+
))}
|
|
269
|
+
</div>
|
|
270
|
+
|
|
271
|
+
{/* ── Import Tab ── */}
|
|
272
|
+
{activeTab === 'import' && (
|
|
273
|
+
<div style={cardStyle}>
|
|
274
|
+
<p style={{ color: 'var(--text-secondary)', fontSize: 14, marginBottom: 20 }}>
|
|
275
|
+
Import Open Knowledge Format bundles into your memory vault.
|
|
276
|
+
</p>
|
|
277
|
+
|
|
278
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
|
279
|
+
<div style={fieldGroup}>
|
|
280
|
+
<label htmlFor="okf-import-path" style={labelStyle}>OKF Bundle Path</label>
|
|
281
|
+
<input
|
|
282
|
+
id="okf-import-path"
|
|
283
|
+
type="text"
|
|
284
|
+
placeholder="/path/to/okf-bundle"
|
|
285
|
+
value={importPath}
|
|
286
|
+
onChange={e => setImportPath(e.target.value)}
|
|
287
|
+
style={inputStyle}
|
|
288
|
+
/>
|
|
289
|
+
</div>
|
|
290
|
+
|
|
291
|
+
<div style={formGrid}>
|
|
292
|
+
<div style={fieldGroup}>
|
|
293
|
+
<label htmlFor="okf-conflict-mode" style={labelStyle}>Conflict Resolution</label>
|
|
294
|
+
<select
|
|
295
|
+
id="okf-conflict-mode"
|
|
296
|
+
value={conflictMode}
|
|
297
|
+
onChange={e => setConflictMode(e.target.value as ConflictMode)}
|
|
298
|
+
style={selectStyle}
|
|
299
|
+
>
|
|
300
|
+
<option value="skip">Skip</option>
|
|
301
|
+
<option value="warn">Warn (default)</option>
|
|
302
|
+
<option value="overwrite">Overwrite</option>
|
|
303
|
+
</select>
|
|
304
|
+
</div>
|
|
305
|
+
|
|
306
|
+
<div style={fieldGroup}>
|
|
307
|
+
<label htmlFor="okf-category-override" style={labelStyle}>Category Override</label>
|
|
308
|
+
<select
|
|
309
|
+
id="okf-category-override"
|
|
310
|
+
value={categoryOverride}
|
|
311
|
+
onChange={e => setCategoryOverride(e.target.value as CategoryOverride)}
|
|
312
|
+
style={selectStyle}
|
|
313
|
+
>
|
|
314
|
+
<option value="auto">Auto (default)</option>
|
|
315
|
+
<option value="invariants">Invariants</option>
|
|
316
|
+
<option value="preferences">Preferences</option>
|
|
317
|
+
<option value="facts">Facts</option>
|
|
318
|
+
<option value="concepts">Concepts</option>
|
|
319
|
+
<option value="patterns">Patterns</option>
|
|
320
|
+
<option value="decisions">Decisions</option>
|
|
321
|
+
<option value="lore">Lore</option>
|
|
322
|
+
</select>
|
|
323
|
+
</div>
|
|
324
|
+
</div>
|
|
325
|
+
|
|
326
|
+
<div style={{ ...fieldGroup, maxWidth: 200 }}>
|
|
327
|
+
<label htmlFor="okf-importance-override" style={labelStyle}>Importance Override (1–5)</label>
|
|
328
|
+
<input
|
|
329
|
+
id="okf-importance-override"
|
|
330
|
+
type="number"
|
|
331
|
+
min={1}
|
|
332
|
+
max={5}
|
|
333
|
+
placeholder="Optional"
|
|
334
|
+
value={importanceOverride}
|
|
335
|
+
onChange={e => setImportanceOverride(e.target.value)}
|
|
336
|
+
style={inputStyle}
|
|
337
|
+
/>
|
|
338
|
+
</div>
|
|
339
|
+
|
|
340
|
+
<button
|
|
341
|
+
id="okf-import-run"
|
|
342
|
+
className="btn btn-primary"
|
|
343
|
+
onClick={handleImport}
|
|
344
|
+
disabled={importRunning || !importPath.trim()}
|
|
345
|
+
style={{ alignSelf: 'flex-start', marginTop: 4 }}
|
|
346
|
+
>
|
|
347
|
+
{importRunning ? '⏳ Importing…' : '⬇ Import Bundle'}
|
|
348
|
+
</button>
|
|
349
|
+
</div>
|
|
350
|
+
|
|
351
|
+
{importOutput && (
|
|
352
|
+
<div id="okf-import-output" style={{ ...outputStyle, borderColor: importError ? 'var(--error)' : 'var(--border)' }}>
|
|
353
|
+
{importOutput}
|
|
354
|
+
</div>
|
|
355
|
+
)}
|
|
356
|
+
</div>
|
|
357
|
+
)}
|
|
358
|
+
|
|
359
|
+
{/* ── Export Tab ── */}
|
|
360
|
+
{activeTab === 'export' && (
|
|
361
|
+
<div style={cardStyle}>
|
|
362
|
+
<p style={{ color: 'var(--text-secondary)', fontSize: 14, marginBottom: 20 }}>
|
|
363
|
+
Export your memory vault as an Open Knowledge Format bundle.
|
|
364
|
+
</p>
|
|
365
|
+
|
|
366
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
|
367
|
+
<div style={fieldGroup}>
|
|
368
|
+
<label htmlFor="okf-export-path" style={labelStyle}>Output Path</label>
|
|
369
|
+
<input
|
|
370
|
+
id="okf-export-path"
|
|
371
|
+
type="text"
|
|
372
|
+
placeholder="/path/to/export"
|
|
373
|
+
value={exportPath}
|
|
374
|
+
onChange={e => setExportPath(e.target.value)}
|
|
375
|
+
style={inputStyle}
|
|
376
|
+
/>
|
|
377
|
+
</div>
|
|
378
|
+
|
|
379
|
+
<div style={formGrid}>
|
|
380
|
+
<div style={fieldGroup}>
|
|
381
|
+
<label htmlFor="okf-export-format" style={labelStyle}>Format</label>
|
|
382
|
+
<select
|
|
383
|
+
id="okf-export-format"
|
|
384
|
+
value={exportFormat}
|
|
385
|
+
onChange={e => setExportFormat(e.target.value as ExportFormat)}
|
|
386
|
+
style={selectStyle}
|
|
387
|
+
>
|
|
388
|
+
<option value="directory">Directory</option>
|
|
389
|
+
<option value="tar.gz">tar.gz</option>
|
|
390
|
+
</select>
|
|
391
|
+
</div>
|
|
392
|
+
|
|
393
|
+
<div style={fieldGroup}>
|
|
394
|
+
<label htmlFor="okf-export-scope" style={labelStyle}>Scope</label>
|
|
395
|
+
<select
|
|
396
|
+
id="okf-export-scope"
|
|
397
|
+
value={exportScope}
|
|
398
|
+
onChange={e => setExportScope(e.target.value as ExportScope)}
|
|
399
|
+
style={selectStyle}
|
|
400
|
+
>
|
|
401
|
+
<option value="all">All</option>
|
|
402
|
+
<option value="global">Global</option>
|
|
403
|
+
<option value="project">Project</option>
|
|
404
|
+
</select>
|
|
405
|
+
</div>
|
|
406
|
+
</div>
|
|
407
|
+
|
|
408
|
+
<label
|
|
409
|
+
htmlFor="okf-strip-meta"
|
|
410
|
+
style={{ display: 'flex', alignItems: 'center', gap: 8, cursor: 'pointer', fontSize: 14, color: 'var(--text-secondary)' }}
|
|
411
|
+
>
|
|
412
|
+
<input
|
|
413
|
+
id="okf-strip-meta"
|
|
414
|
+
type="checkbox"
|
|
415
|
+
checked={stripMeta}
|
|
416
|
+
onChange={e => setStripMeta(e.target.checked)}
|
|
417
|
+
style={{ accentColor: 'var(--accent)' }}
|
|
418
|
+
/>
|
|
419
|
+
Strip SSSS metadata
|
|
420
|
+
</label>
|
|
421
|
+
|
|
422
|
+
<button
|
|
423
|
+
id="okf-export-run"
|
|
424
|
+
className="btn btn-primary"
|
|
425
|
+
onClick={handleExport}
|
|
426
|
+
disabled={exportRunning || !exportPath.trim()}
|
|
427
|
+
style={{ alignSelf: 'flex-start', marginTop: 4 }}
|
|
428
|
+
>
|
|
429
|
+
{exportRunning ? '⏳ Exporting…' : '⬆ Export Vault'}
|
|
430
|
+
</button>
|
|
431
|
+
</div>
|
|
432
|
+
|
|
433
|
+
{exportOutput && (
|
|
434
|
+
<div id="okf-export-output" style={{ ...outputStyle, borderColor: exportError ? 'var(--error)' : 'var(--border)' }}>
|
|
435
|
+
{exportOutput}
|
|
436
|
+
</div>
|
|
437
|
+
)}
|
|
438
|
+
</div>
|
|
439
|
+
)}
|
|
440
|
+
|
|
441
|
+
{/* ── Lint Tab ── */}
|
|
442
|
+
{activeTab === 'lint' && (
|
|
443
|
+
<div style={cardStyle}>
|
|
444
|
+
<p style={{ color: 'var(--text-secondary)', fontSize: 14, marginBottom: 20 }}>
|
|
445
|
+
Run OKF compliance checks across all memory nodes.
|
|
446
|
+
</p>
|
|
447
|
+
|
|
448
|
+
<button
|
|
449
|
+
id="okf-lint-run"
|
|
450
|
+
className="btn btn-primary"
|
|
451
|
+
onClick={handleLint}
|
|
452
|
+
disabled={lintRunning}
|
|
453
|
+
style={{ marginBottom: 20 }}
|
|
454
|
+
>
|
|
455
|
+
{lintRunning ? '⏳ Checking…' : '🔍 Run Compliance Check'}
|
|
456
|
+
</button>
|
|
457
|
+
|
|
458
|
+
{lintRan && lintRows.length > 0 && (
|
|
459
|
+
<>
|
|
460
|
+
<div style={{ overflowX: 'auto' }}>
|
|
461
|
+
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 13 }}>
|
|
462
|
+
<thead>
|
|
463
|
+
<tr style={{ borderBottom: '1px solid var(--border)' }}>
|
|
464
|
+
<th style={{ textAlign: 'left', padding: '10px 12px', color: 'var(--text-secondary)', fontWeight: 500 }}>Slug</th>
|
|
465
|
+
<th style={{ textAlign: 'left', padding: '10px 12px', color: 'var(--text-secondary)', fontWeight: 500 }}>Field</th>
|
|
466
|
+
<th style={{ textAlign: 'left', padding: '10px 12px', color: 'var(--text-secondary)', fontWeight: 500 }}>Severity</th>
|
|
467
|
+
<th style={{ textAlign: 'left', padding: '10px 12px', color: 'var(--text-secondary)', fontWeight: 500 }}>Message</th>
|
|
468
|
+
</tr>
|
|
469
|
+
</thead>
|
|
470
|
+
<tbody>
|
|
471
|
+
{lintRows.map((row, i) => (
|
|
472
|
+
<tr key={i} style={{ borderBottom: '1px solid var(--border)' }}>
|
|
473
|
+
<td style={{ padding: '10px 12px', color: 'var(--text-primary)', fontFamily: 'var(--font-mono, monospace)' }}>{row.slug}</td>
|
|
474
|
+
<td style={{ padding: '10px 12px', color: 'var(--text-secondary)' }}>{row.field}</td>
|
|
475
|
+
<td style={{ padding: '10px 12px' }}>
|
|
476
|
+
<span
|
|
477
|
+
className="badge"
|
|
478
|
+
style={{
|
|
479
|
+
background: `${severityColor[row.severity]}22`,
|
|
480
|
+
color: severityColor[row.severity],
|
|
481
|
+
padding: '3px 10px',
|
|
482
|
+
borderRadius: 4,
|
|
483
|
+
fontSize: 12,
|
|
484
|
+
fontWeight: 600,
|
|
485
|
+
textTransform: 'uppercase',
|
|
486
|
+
}}
|
|
487
|
+
>
|
|
488
|
+
{row.severity}
|
|
489
|
+
</span>
|
|
490
|
+
</td>
|
|
491
|
+
<td style={{ padding: '10px 12px', color: 'var(--text-secondary)' }}>{row.message}</td>
|
|
492
|
+
</tr>
|
|
493
|
+
))}
|
|
494
|
+
</tbody>
|
|
495
|
+
<tfoot>
|
|
496
|
+
<tr style={{ borderTop: '2px solid var(--border)' }}>
|
|
497
|
+
<td colSpan={4} style={{ padding: '12px', color: 'var(--text-secondary)', fontSize: 13 }}>
|
|
498
|
+
<strong style={{ color: 'var(--text-primary)' }}>Total: {lintRows.length}</strong>
|
|
499
|
+
{' · '}
|
|
500
|
+
<span style={{ color: 'var(--error)' }}>Errors: {lintErrors}</span>
|
|
501
|
+
{' · '}
|
|
502
|
+
<span style={{ color: 'var(--warning)' }}>Warnings: {lintWarnings}</span>
|
|
503
|
+
</td>
|
|
504
|
+
</tr>
|
|
505
|
+
</tfoot>
|
|
506
|
+
</table>
|
|
507
|
+
</div>
|
|
508
|
+
</>
|
|
509
|
+
)}
|
|
510
|
+
|
|
511
|
+
{lintRan && lintRows.length === 0 && !lintRawOutput && (
|
|
512
|
+
<div
|
|
513
|
+
id="okf-lint-pass"
|
|
514
|
+
style={{
|
|
515
|
+
textAlign: 'center',
|
|
516
|
+
padding: '40px 20px',
|
|
517
|
+
color: 'var(--success)',
|
|
518
|
+
fontSize: 15,
|
|
519
|
+
fontWeight: 500,
|
|
520
|
+
}}
|
|
521
|
+
>
|
|
522
|
+
✅ All nodes pass OKF compliance
|
|
523
|
+
</div>
|
|
524
|
+
)}
|
|
525
|
+
|
|
526
|
+
{lintRan && lintRows.length === 0 && lintRawOutput && (
|
|
527
|
+
<div id="okf-lint-output" style={outputStyle}>
|
|
528
|
+
{lintRawOutput}
|
|
529
|
+
</div>
|
|
530
|
+
)}
|
|
531
|
+
</div>
|
|
532
|
+
)}
|
|
533
|
+
</div>
|
|
534
|
+
);
|
|
535
|
+
}
|