@polderlabs/bizar 5.0.0 → 5.0.2
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/bizar-dash/dist/assets/{icons-CFqu2M-c.js → icons-Bo0iH9EC.js} +166 -121
- package/bizar-dash/dist/assets/icons-Bo0iH9EC.js.map +1 -0
- package/bizar-dash/dist/assets/main-C1cpttnv.js +19 -0
- package/bizar-dash/dist/assets/main-C1cpttnv.js.map +1 -0
- package/bizar-dash/dist/assets/main-DTkNlLrw.css +1 -0
- package/bizar-dash/dist/assets/mobile-BtxQJftK.js +2 -0
- package/bizar-dash/dist/assets/mobile-BtxQJftK.js.map +1 -0
- package/bizar-dash/dist/assets/{mobile-DHXXbn1A.js → mobile-DD-FZrTC.js} +1 -1
- package/bizar-dash/dist/assets/{mobile-DHXXbn1A.js.map → mobile-DD-FZrTC.js.map} +1 -1
- package/bizar-dash/dist/index.html +4 -4
- package/bizar-dash/dist/mobile.html +3 -3
- package/bizar-dash/node_modules/.package-lock.json +6 -0
- package/bizar-dash/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
- package/bizar-dash/package-lock.json +6 -0
- package/bizar-dash/src/server/api.mjs +7 -0
- package/bizar-dash/src/server/diagnostics-store.mjs +452 -2
- package/bizar-dash/src/server/memory-store.mjs +46 -0
- package/bizar-dash/src/server/providers-store.mjs +11 -4
- package/bizar-dash/src/server/routes/_shared.mjs +2 -2
- package/bizar-dash/src/server/routes/doctor.mjs +71 -0
- package/bizar-dash/src/server/routes/env-vars.mjs +67 -1
- package/bizar-dash/src/server/routes/memory.mjs +3 -0
- package/bizar-dash/src/server/routes/opencode-session-detail.mjs +61 -42
- package/bizar-dash/src/server/routes/schedules.mjs +55 -0
- package/bizar-dash/src/server/serve-info.mjs +172 -0
- package/bizar-dash/src/web/App.tsx +48 -11
- package/bizar-dash/src/web/components/AutosaveField.tsx +50 -0
- package/bizar-dash/src/web/components/DoctorPanel.tsx +160 -0
- package/bizar-dash/src/web/components/EnvVarManager.tsx +245 -62
- package/bizar-dash/src/web/components/ScheduleTemplateCard.tsx +71 -0
- package/bizar-dash/src/web/components/SettingsNav.tsx +101 -0
- package/bizar-dash/src/web/components/Sidebar.tsx +25 -1
- package/bizar-dash/src/web/components/StatusBadge.tsx +33 -2
- package/bizar-dash/src/web/components/Topbar.tsx +11 -0
- package/bizar-dash/src/web/components/chat/ChatInfoPanel.tsx +64 -0
- package/bizar-dash/src/web/components/chat/Composer.tsx +1 -1
- package/bizar-dash/src/web/components/chat/useChat.ts +118 -1
- package/bizar-dash/src/web/hooks/useAutosave.ts +107 -0
- package/bizar-dash/src/web/lib/types.ts +134 -0
- package/bizar-dash/src/web/styles/chat.css +8 -5
- package/bizar-dash/src/web/styles/main.css +77 -2
- package/bizar-dash/src/web/styles/settings.css +265 -0
- package/bizar-dash/src/web/views/Chat.tsx +15 -1
- package/bizar-dash/src/web/views/Doctor.tsx +317 -0
- package/bizar-dash/src/web/views/MiniMaxUsage.tsx +1 -0
- package/bizar-dash/src/web/views/Schedules.tsx +94 -12
- package/bizar-dash/src/web/views/Settings.tsx +76 -46
- package/bizar-dash/src/web/views/memory/ConfigPanel.tsx +114 -133
- package/bizar-dash/src/web/views/settings/AgentSection.tsx +23 -12
- package/bizar-dash/src/web/views/settings/EnvVarsSection.tsx +5 -6
- package/bizar-dash/src/web/views/settings/GeneralSection.tsx +38 -15
- package/bizar-dash/src/web/views/settings/MemorySection.tsx +92 -8
- package/bizar-dash/tests/autosave.test.tsx +276 -0
- package/bizar-dash/tests/chat-composer.test.tsx +140 -0
- package/bizar-dash/tests/components/doctor-panel.test.tsx +105 -0
- package/bizar-dash/tests/diagnostics-store.test.mjs +206 -0
- package/bizar-dash/tests/env-vars-extended.test.mjs +190 -0
- package/bizar-dash/tests/layout-spacing.test.mjs +101 -0
- package/bizar-dash/tests/memory-default-vault.test.mjs +98 -0
- package/bizar-dash/tests/minimax-bar.test.tsx +113 -0
- package/bizar-dash/tests/minimax-models.test.mjs +123 -0
- package/bizar-dash/tests/opencode-sessions-detail.test.mjs +158 -0
- package/bizar-dash/tests/routes-doctor.test.mjs +159 -0
- package/bizar-dash/tests/schedules-templates.test.mjs +144 -0
- package/bizar-dash/tests/settings-layout.test.tsx +129 -0
- package/bizar-dash/tests/settings-mode-wiring.test.tsx +151 -0
- package/bizar-dash/tests/settings-nav.test.tsx +126 -0
- package/cli/commands/dash.mjs +5 -1
- package/cli/commands/minimax.mjs +5 -2
- package/cli/tests/minimax-cli.test.mjs +79 -0
- package/config/agents/frigg.md +1 -1
- package/config/agents/heimdall.md +1 -1
- package/config/agents/mimir.md +1 -1
- package/config/agents/quick.md +1 -1
- package/config/agents/semble-search.md +1 -1
- package/config/agents/vor.md +1 -1
- package/config/opencode.json +28 -21
- package/config/opencode.json.template +8 -1
- package/package.json +1 -1
- package/plugins/bizar/index.ts +77 -0
- package/plugins/bizar/src/compaction.d.mts +48 -0
- package/plugins/bizar/src/compaction.mjs +192 -0
- package/plugins/bizar/tests/compaction.test.ts +264 -0
- package/templates/schedules/daily-backup.json +12 -0
- package/templates/schedules/daily-cleanup.json +12 -0
- package/templates/schedules/hourly-health-check.json +12 -0
- package/templates/schedules/webhook-on-push.json +13 -0
- package/templates/schedules/weekly-digest.json +13 -0
- package/bizar-dash/dist/assets/icons-CFqu2M-c.js.map +0 -1
- package/bizar-dash/dist/assets/main-Dl8yY5_H.js +0 -16
- package/bizar-dash/dist/assets/main-Dl8yY5_H.js.map +0 -1
- package/bizar-dash/dist/assets/main-ZAfGKENE.css +0 -1
- package/bizar-dash/dist/assets/mobile-C2gysFOZ.js +0 -2
- package/bizar-dash/dist/assets/mobile-C2gysFOZ.js.map +0 -1
- package/bizar-dash/src/web/views/settings/BackupSection.tsx +0 -16
- package/bizar-dash/src/web/views/settings/ProvidersSection.tsx +0 -16
- package/bizar-dash/src/web/views/settings/SkillsSection.tsx +0 -16
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// src/web/components/EnvVarManager.tsx — manage BIZAR_* env vars from ~/.config/bizar/env.json
|
|
2
|
-
import { useCallback, useEffect, useState } from 'react';
|
|
3
|
-
import { Plus, Pencil, Trash2, Eye, EyeOff, RefreshCw, KeyRound, CheckCircle, X } from 'lucide-react';
|
|
2
|
+
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
3
|
+
import { Plus, Pencil, Trash2, Eye, EyeOff, RefreshCw, KeyRound, CheckCircle, X, Search, Download, Upload, ChevronDown, ChevronRight, AlertCircle } from 'lucide-react';
|
|
4
4
|
import { Button } from './Button';
|
|
5
5
|
import { useToast } from './Toast';
|
|
6
6
|
import { api } from '../lib/api';
|
|
@@ -13,6 +13,8 @@ export type EnvVarEntry = {
|
|
|
13
13
|
source: string;
|
|
14
14
|
};
|
|
15
15
|
|
|
16
|
+
export type EnvVarGrouped = Record<string, EnvVarEntry[]>;
|
|
17
|
+
|
|
16
18
|
type Props = {
|
|
17
19
|
onError?: (msg: string) => void;
|
|
18
20
|
};
|
|
@@ -31,6 +33,14 @@ export function EnvVarManager({ onError }: Props) {
|
|
|
31
33
|
const [showAdd, setShowAdd] = useState(false);
|
|
32
34
|
const [newName, setNewName] = useState('');
|
|
33
35
|
const [newValue, setNewValue] = useState('');
|
|
36
|
+
// Bulk operations
|
|
37
|
+
const [showBulk, setShowBulk] = useState(false);
|
|
38
|
+
const [bulkText, setBulkText] = useState('');
|
|
39
|
+
const [bulkImporting, setBulkImporting] = useState(false);
|
|
40
|
+
// Search / filter
|
|
41
|
+
const [searchQ, setSearchQ] = useState('');
|
|
42
|
+
// Group by prefix (collapsed state)
|
|
43
|
+
const [collapsed, setCollapsed] = useState<Set<string>>(new Set());
|
|
34
44
|
|
|
35
45
|
const load = useCallback(async () => {
|
|
36
46
|
setLoading(true);
|
|
@@ -121,21 +131,139 @@ export function EnvVarManager({ onError }: Props) {
|
|
|
121
131
|
}
|
|
122
132
|
};
|
|
123
133
|
|
|
134
|
+
// ─── Bulk import ───────────────────────────────────────────────────────────
|
|
135
|
+
const handleBulkImport = async () => {
|
|
136
|
+
if (!bulkText.trim()) { toast.warning('Nothing to import.'); return; }
|
|
137
|
+
setBulkImporting(true);
|
|
138
|
+
try {
|
|
139
|
+
const r = await api.post<{ ok: boolean; imported: number; skipped: number; errors: string[] }>('/env-vars/bulk-import', { envContent: bulkText });
|
|
140
|
+
toast.success(`Imported ${r.imported}, skipped ${r.skipped}.`);
|
|
141
|
+
if (r.errors.length > 0) {
|
|
142
|
+
toast.warning(`${r.errors.length} lines had errors: ${r.errors.slice(0, 3).join('; ')}${r.errors.length > 3 ? '…' : ''}`);
|
|
143
|
+
}
|
|
144
|
+
setBulkText('');
|
|
145
|
+
setShowBulk(false);
|
|
146
|
+
await load();
|
|
147
|
+
} catch (err) {
|
|
148
|
+
toast.error(`Bulk import failed: ${(err as Error).message}`);
|
|
149
|
+
} finally {
|
|
150
|
+
setBulkImporting(false);
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
// ─── Bulk export ───────────────────────────────────────────────────────────
|
|
155
|
+
const handleExport = async () => {
|
|
156
|
+
try {
|
|
157
|
+
const r = await api.get<string>('/env-vars/export');
|
|
158
|
+
// r is the raw text content
|
|
159
|
+
const blob = new Blob([r as unknown as string], { type: 'text/plain' });
|
|
160
|
+
const url = URL.createObjectURL(blob);
|
|
161
|
+
const a = document.createElement('a');
|
|
162
|
+
a.href = url;
|
|
163
|
+
a.download = '.env';
|
|
164
|
+
a.click();
|
|
165
|
+
URL.revokeObjectURL(url);
|
|
166
|
+
toast.success('Exported .env file.');
|
|
167
|
+
} catch (err) {
|
|
168
|
+
toast.error(`Export failed: ${(err as Error).message}`);
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
// ─── Group by prefix ───────────────────────────────────────────────────────
|
|
173
|
+
const filtered = useMemo(() => {
|
|
174
|
+
if (!searchQ.trim()) return vars;
|
|
175
|
+
const q = searchQ.toLowerCase();
|
|
176
|
+
return vars.filter((v) => v.name.toLowerCase().includes(q));
|
|
177
|
+
}, [vars, searchQ]);
|
|
178
|
+
|
|
179
|
+
const grouped = useMemo(() => {
|
|
180
|
+
const groups: Record<string, EnvVarEntry[]> = {};
|
|
181
|
+
for (const v of filtered) {
|
|
182
|
+
const prefix = v.name.split('_')[0] + '_';
|
|
183
|
+
(groups[prefix] ??= []).push(v);
|
|
184
|
+
}
|
|
185
|
+
return groups;
|
|
186
|
+
}, [filtered]);
|
|
187
|
+
|
|
188
|
+
const toggleGroup = (prefix: string) => {
|
|
189
|
+
setCollapsed((prev) => {
|
|
190
|
+
const next = new Set(prev);
|
|
191
|
+
if (next.has(prefix)) next.delete(prefix);
|
|
192
|
+
else next.add(prefix);
|
|
193
|
+
return next;
|
|
194
|
+
});
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const sortedGroupKeys = useMemo(() => {
|
|
198
|
+
const known = ['BIZAR_', 'PROVIDER_', 'MODEL_', 'API_'];
|
|
199
|
+
const keys = Object.keys(grouped).sort((a, b) => {
|
|
200
|
+
const ai = known.indexOf(a); const bi = known.indexOf(b);
|
|
201
|
+
if (ai === -1 && bi === -1) return a.localeCompare(b);
|
|
202
|
+
if (ai === -1) return 1;
|
|
203
|
+
if (bi === -1) return -1;
|
|
204
|
+
return ai - bi;
|
|
205
|
+
});
|
|
206
|
+
return keys;
|
|
207
|
+
}, [grouped]);
|
|
208
|
+
|
|
124
209
|
if (loading) {
|
|
125
210
|
return <div className="muted" style={{ padding: '16px 0', fontSize: 13 }}>Loading env vars…</div>;
|
|
126
211
|
}
|
|
127
212
|
|
|
213
|
+
const hasGroups = sortedGroupKeys.length > 1;
|
|
214
|
+
const groupMode = hasGroups;
|
|
215
|
+
|
|
128
216
|
return (
|
|
129
217
|
<div className="env-var-manager">
|
|
130
218
|
<div className="env-var-toolbar">
|
|
131
219
|
<Button variant="primary" size="sm" onClick={() => setShowAdd(true)}>
|
|
132
220
|
<Plus size={12} /> New variable
|
|
133
221
|
</Button>
|
|
222
|
+
<Button variant="secondary" size="sm" onClick={() => setShowBulk((v) => !v)} className={showBulk ? 'active' : ''}>
|
|
223
|
+
<Upload size={12} /> Bulk import
|
|
224
|
+
</Button>
|
|
225
|
+
<Button variant="ghost" size="sm" onClick={handleExport} title="Export as .env">
|
|
226
|
+
<Download size={12} /> Export
|
|
227
|
+
</Button>
|
|
228
|
+
<div className="env-var-search-wrap">
|
|
229
|
+
<Search size={12} className="env-var-search-icon" />
|
|
230
|
+
<input
|
|
231
|
+
className="env-var-search input"
|
|
232
|
+
placeholder="Filter…"
|
|
233
|
+
value={searchQ}
|
|
234
|
+
onChange={(e) => setSearchQ(e.target.value)}
|
|
235
|
+
/>
|
|
236
|
+
</div>
|
|
134
237
|
<Button variant="ghost" size="sm" onClick={load} title="Reload">
|
|
135
238
|
<RefreshCw size={12} />
|
|
136
239
|
</Button>
|
|
137
240
|
</div>
|
|
138
241
|
|
|
242
|
+
{/* Bulk import panel */}
|
|
243
|
+
{showBulk && (
|
|
244
|
+
<div className="env-var-bulk-panel">
|
|
245
|
+
<div className="env-var-bulk-header">
|
|
246
|
+
<span>Paste <code>KEY=value</code> lines (one per line):</span>
|
|
247
|
+
<Button variant="ghost" size="sm" onClick={() => setShowBulk(false)}><X size={12} /></Button>
|
|
248
|
+
</div>
|
|
249
|
+
<textarea
|
|
250
|
+
className="input mono env-var-bulk-textarea"
|
|
251
|
+
placeholder={"BIZAR_API_KEY=your-key-here\nPROVIDER_OPENAI_KEY=sk-...\nMODEL_KEY=gem-..."}
|
|
252
|
+
value={bulkText}
|
|
253
|
+
onChange={(e) => setBulkText(e.target.value)}
|
|
254
|
+
rows={6}
|
|
255
|
+
/>
|
|
256
|
+
<div className="env-var-bulk-footer">
|
|
257
|
+
<Button variant="primary" size="sm" onClick={handleBulkImport} disabled={bulkImporting || !bulkText.trim()}>
|
|
258
|
+
{bulkImporting ? 'Importing…' : 'Import'}
|
|
259
|
+
</Button>
|
|
260
|
+
<span className="muted" style={{ fontSize: 11 }}>
|
|
261
|
+
Existing keys are skipped unless re-created individually. Invalid lines are reported.
|
|
262
|
+
</span>
|
|
263
|
+
</div>
|
|
264
|
+
</div>
|
|
265
|
+
)}
|
|
266
|
+
|
|
139
267
|
{vars.length === 0 && !showAdd && (
|
|
140
268
|
<div className="env-var-empty">
|
|
141
269
|
<KeyRound size={20} />
|
|
@@ -178,70 +306,125 @@ export function EnvVarManager({ onError }: Props) {
|
|
|
178
306
|
</div>
|
|
179
307
|
)}
|
|
180
308
|
|
|
181
|
-
{/* Var list */}
|
|
182
|
-
{
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
<
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
className="
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
{!isEditing && (
|
|
211
|
-
<button
|
|
212
|
-
type="button"
|
|
213
|
-
className="icon-btn"
|
|
214
|
-
title={isRevealed ? 'Hide' : 'Reveal'}
|
|
215
|
-
onClick={() => toggleReveal(v.name)}
|
|
216
|
-
>
|
|
217
|
-
{isRevealed ? <EyeOff size={12} /> : <Eye size={12} />}
|
|
218
|
-
</button>
|
|
219
|
-
)}
|
|
220
|
-
{isEditing ? (
|
|
221
|
-
<>
|
|
222
|
-
<Button variant="primary" size="sm" onClick={() => handleUpdate(v.name)} disabled={saving}>
|
|
223
|
-
Save
|
|
224
|
-
</Button>
|
|
225
|
-
<Button variant="ghost" size="sm" onClick={() => setEditing(null)}>
|
|
226
|
-
Cancel
|
|
227
|
-
</Button>
|
|
228
|
-
</>
|
|
229
|
-
) : (
|
|
230
|
-
<>
|
|
231
|
-
<button type="button" className="icon-btn" title="Edit" onClick={() => startEdit(v)}>
|
|
232
|
-
<Pencil size={12} />
|
|
233
|
-
</button>
|
|
234
|
-
<button type="button" className="icon-btn icon-btn-danger" title="Delete" onClick={() => handleDelete(v.name)}>
|
|
235
|
-
<Trash2 size={12} />
|
|
236
|
-
</button>
|
|
237
|
-
</>
|
|
309
|
+
{/* Var list — grouped by prefix */}
|
|
310
|
+
{filtered.length > 0 && (
|
|
311
|
+
groupMode ? (
|
|
312
|
+
<div className="env-var-groups">
|
|
313
|
+
{sortedGroupKeys.map((prefix) => {
|
|
314
|
+
const items = grouped[prefix];
|
|
315
|
+
const isCollapsed = collapsed.has(prefix);
|
|
316
|
+
return (
|
|
317
|
+
<div key={prefix} className="env-var-group">
|
|
318
|
+
<button
|
|
319
|
+
type="button"
|
|
320
|
+
className="env-var-group-header"
|
|
321
|
+
onClick={() => toggleGroup(prefix)}
|
|
322
|
+
>
|
|
323
|
+
{isCollapsed ? <ChevronRight size={12} /> : <ChevronDown size={12} />}
|
|
324
|
+
<span className="env-var-group-prefix mono">{prefix}</span>
|
|
325
|
+
<span className="env-var-group-count muted">({items.length})</span>
|
|
326
|
+
</button>
|
|
327
|
+
{!isCollapsed && (
|
|
328
|
+
<div className="env-var-list">
|
|
329
|
+
{items.map((v) => (
|
|
330
|
+
<VarRow key={v.name} v={v} editing={editing} editValue={editValue}
|
|
331
|
+
isRevealed={revealed.has(v.name)}
|
|
332
|
+
onToggleReveal={toggleReveal} onStartEdit={startEdit}
|
|
333
|
+
onEditValueChange={setEditValue} onHandleUpdate={handleUpdate}
|
|
334
|
+
onCancelEdit={() => setEditing(null)} onDelete={handleDelete}
|
|
335
|
+
saving={saving} />
|
|
336
|
+
))}
|
|
337
|
+
</div>
|
|
238
338
|
)}
|
|
239
339
|
</div>
|
|
240
|
-
|
|
241
|
-
)
|
|
242
|
-
|
|
340
|
+
);
|
|
341
|
+
})}
|
|
342
|
+
</div>
|
|
343
|
+
) : (
|
|
344
|
+
<div className="env-var-list">
|
|
345
|
+
{filtered.map((v) => (
|
|
346
|
+
<VarRow key={v.name} v={v} editing={editing} editValue={editValue}
|
|
347
|
+
isRevealed={revealed.has(v.name)}
|
|
348
|
+
onToggleReveal={toggleReveal} onStartEdit={startEdit}
|
|
349
|
+
onEditValueChange={setEditValue} onHandleUpdate={handleUpdate}
|
|
350
|
+
onCancelEdit={() => setEditing(null)} onDelete={handleDelete}
|
|
351
|
+
saving={saving} />
|
|
352
|
+
))}
|
|
353
|
+
</div>
|
|
354
|
+
)
|
|
355
|
+
)}
|
|
356
|
+
|
|
357
|
+
{filtered.length === 0 && vars.length > 0 && (
|
|
358
|
+
<div className="env-var-empty">
|
|
359
|
+
<Search size={16} />
|
|
360
|
+
<span>No env vars match "{searchQ}"</span>
|
|
243
361
|
</div>
|
|
244
362
|
)}
|
|
245
363
|
</div>
|
|
246
364
|
);
|
|
247
365
|
}
|
|
366
|
+
|
|
367
|
+
// ─── Individual var row (extracted for reuse in group mode) ───────────────────
|
|
368
|
+
type VarRowProps = {
|
|
369
|
+
v: EnvVarEntry;
|
|
370
|
+
editing: string | null;
|
|
371
|
+
editValue: string;
|
|
372
|
+
isRevealed: boolean;
|
|
373
|
+
onToggleReveal: (name: string) => void;
|
|
374
|
+
onStartEdit: (v: EnvVarEntry) => void;
|
|
375
|
+
onEditValueChange: (v: string) => void;
|
|
376
|
+
onHandleUpdate: (name: string) => void;
|
|
377
|
+
onCancelEdit: () => void;
|
|
378
|
+
onDelete: (name: string) => void;
|
|
379
|
+
saving: boolean;
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
function VarRow({ v, editing, editValue, isRevealed, onToggleReveal, onStartEdit, onEditValueChange, onHandleUpdate, onCancelEdit, onDelete, saving }: VarRowProps) {
|
|
383
|
+
const isEditing = editing === v.name;
|
|
384
|
+
return (
|
|
385
|
+
<div className={cn('env-var-row', isEditing && 'env-var-row-editing')}>
|
|
386
|
+
<span className="env-var-name mono">{v.name}</span>
|
|
387
|
+
{isEditing ? (
|
|
388
|
+
<input
|
|
389
|
+
className="input mono"
|
|
390
|
+
type="password"
|
|
391
|
+
style={{ flex: 1, minWidth: 200 }}
|
|
392
|
+
value={editValue}
|
|
393
|
+
onChange={(e) => onEditValueChange(e.target.value)}
|
|
394
|
+
onKeyDown={(e) => {
|
|
395
|
+
if (e.key === 'Enter') onHandleUpdate(v.name);
|
|
396
|
+
if (e.key === 'Escape') onCancelEdit();
|
|
397
|
+
}}
|
|
398
|
+
autoFocus
|
|
399
|
+
/>
|
|
400
|
+
) : (
|
|
401
|
+
<span className={cn('env-var-value mono', !isRevealed && 'env-var-value-masked')}>
|
|
402
|
+
{v.value}
|
|
403
|
+
</span>
|
|
404
|
+
)}
|
|
405
|
+
<span className="env-var-source muted">{v.source}</span>
|
|
406
|
+
<div className="env-var-actions">
|
|
407
|
+
{!isEditing && (
|
|
408
|
+
<button type="button" className="icon-btn" title={isRevealed ? 'Hide' : 'Reveal'} onClick={() => onToggleReveal(v.name)}>
|
|
409
|
+
{isRevealed ? <EyeOff size={12} /> : <Eye size={12} />}
|
|
410
|
+
</button>
|
|
411
|
+
)}
|
|
412
|
+
{isEditing ? (
|
|
413
|
+
<>
|
|
414
|
+
<Button variant="primary" size="sm" onClick={() => onHandleUpdate(v.name)} disabled={saving}>Save</Button>
|
|
415
|
+
<Button variant="ghost" size="sm" onClick={onCancelEdit}>Cancel</Button>
|
|
416
|
+
</>
|
|
417
|
+
) : (
|
|
418
|
+
<>
|
|
419
|
+
<button type="button" className="icon-btn" title="Edit" onClick={() => onStartEdit(v)}>
|
|
420
|
+
<Pencil size={12} />
|
|
421
|
+
</button>
|
|
422
|
+
<button type="button" className="icon-btn icon-btn-danger" title="Delete" onClick={() => onDelete(v.name)}>
|
|
423
|
+
<Trash2 size={12} />
|
|
424
|
+
</button>
|
|
425
|
+
</>
|
|
426
|
+
)}
|
|
427
|
+
</div>
|
|
428
|
+
</div>
|
|
429
|
+
);
|
|
430
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// src/components/ScheduleTemplateCard.tsx — template preview card for the Schedules Templates tab.
|
|
2
|
+
|
|
3
|
+
import { Card, CardTitle, CardMeta } from './Card';
|
|
4
|
+
import { Button } from './Button';
|
|
5
|
+
|
|
6
|
+
export type ScheduleTemplate = {
|
|
7
|
+
id: string;
|
|
8
|
+
name: string;
|
|
9
|
+
description: string;
|
|
10
|
+
type: 'cron' | 'interval' | 'once' | 'webhook';
|
|
11
|
+
schedule?: string;
|
|
12
|
+
intervalMs?: number;
|
|
13
|
+
action: {
|
|
14
|
+
type: 'command' | 'agent' | 'webhook';
|
|
15
|
+
target?: string;
|
|
16
|
+
prompt?: string;
|
|
17
|
+
method?: string;
|
|
18
|
+
};
|
|
19
|
+
tags?: string[];
|
|
20
|
+
source?: string;
|
|
21
|
+
templateFile?: string;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
type Props = {
|
|
25
|
+
template: ScheduleTemplate;
|
|
26
|
+
onUse: (template: ScheduleTemplate) => void;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
function formatSchedule(template: ScheduleTemplate): string {
|
|
30
|
+
if (template.type === 'cron' && template.schedule) {
|
|
31
|
+
return `cron: ${template.schedule}`;
|
|
32
|
+
}
|
|
33
|
+
if (template.type === 'interval') {
|
|
34
|
+
const ms = template.intervalMs || 0;
|
|
35
|
+
const secs = ms / 1000;
|
|
36
|
+
if (secs >= 3600) return `every ${secs / 3600}h`;
|
|
37
|
+
if (secs >= 60) return `every ${secs / 60}m`;
|
|
38
|
+
return `every ${secs}s`;
|
|
39
|
+
}
|
|
40
|
+
return template.type;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function ScheduleTemplateCard({ template, onUse }: Props) {
|
|
44
|
+
return (
|
|
45
|
+
<Card className="schedule-template-card">
|
|
46
|
+
<div className="schedule-template-header">
|
|
47
|
+
<CardTitle>{template.name}</CardTitle>
|
|
48
|
+
{template.tags && template.tags.length > 0 && (
|
|
49
|
+
<div className="schedule-template-tags">
|
|
50
|
+
{template.tags.map((tag) => (
|
|
51
|
+
<span key={tag} className="tag">{tag}</span>
|
|
52
|
+
))}
|
|
53
|
+
</div>
|
|
54
|
+
)}
|
|
55
|
+
</div>
|
|
56
|
+
<CardMeta>{template.description}</CardMeta>
|
|
57
|
+
<div className="schedule-template-summary">
|
|
58
|
+
<code>{formatSchedule(template)}</code>
|
|
59
|
+
{' · '}
|
|
60
|
+
<code>{template.action.type}</code>
|
|
61
|
+
</div>
|
|
62
|
+
<Button
|
|
63
|
+
variant="secondary"
|
|
64
|
+
size="sm"
|
|
65
|
+
onClick={() => onUse(template)}
|
|
66
|
+
>
|
|
67
|
+
Use this template
|
|
68
|
+
</Button>
|
|
69
|
+
</Card>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// src/components/SettingsNav.tsx — v4.9.0 full-sidebar settings navigation.
|
|
2
|
+
// Shown in the sidebar rail when settingsMode is active. All sections are
|
|
3
|
+
// visible at once; clicking one scrolls the Settings view to that section.
|
|
4
|
+
import { ChevronRight, Palette, Terminal, Boxes, Brain, Cpu, RefreshCw, Sparkles, Gauge, Archive, ArrowLeft, LayoutGrid, type LucideIcon } from 'lucide-react';
|
|
5
|
+
import { cn } from '../lib/utils';
|
|
6
|
+
|
|
7
|
+
export type SettingsSection = {
|
|
8
|
+
id: string;
|
|
9
|
+
label: string;
|
|
10
|
+
icon?: LucideIcon;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type SettingsSectionGroup = {
|
|
14
|
+
label: string;
|
|
15
|
+
sections: SettingsSection[];
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const SECTION_GROUPS: SettingsSectionGroup[] = [
|
|
19
|
+
{
|
|
20
|
+
label: 'General',
|
|
21
|
+
sections: [
|
|
22
|
+
{ id: 'theme', label: 'Theme', icon: Palette },
|
|
23
|
+
{ id: 'layout', label: 'Layout', icon: LayoutGrid },
|
|
24
|
+
{ id: 'general', label: 'General', icon: LayoutGrid },
|
|
25
|
+
],
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
label: 'Core',
|
|
29
|
+
sections: [
|
|
30
|
+
{ id: 'env-vars', label: 'Env Vars', icon: Terminal },
|
|
31
|
+
{ id: 'providers', label: 'Providers', icon: Boxes },
|
|
32
|
+
{ id: 'memory', label: 'Memory', icon: Brain },
|
|
33
|
+
{ id: 'system-llm', label: 'System LLM', icon: Cpu },
|
|
34
|
+
],
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
label: 'Experience',
|
|
38
|
+
sections: [
|
|
39
|
+
{ id: 'updates', label: 'Updates', icon: RefreshCw },
|
|
40
|
+
{ id: 'skills', label: 'Skills', icon: Sparkles },
|
|
41
|
+
{ id: 'headroom', label: 'Headroom', icon: Gauge },
|
|
42
|
+
],
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
label: 'Data',
|
|
46
|
+
sections: [
|
|
47
|
+
{ id: 'backup', label: 'Backup', icon: Archive },
|
|
48
|
+
],
|
|
49
|
+
},
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
export type SettingsNavProps = {
|
|
53
|
+
activeSection: string | null;
|
|
54
|
+
onSectionChange: (id: string | null) => void;
|
|
55
|
+
onExitSettings: () => void;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export function SettingsNav({ activeSection, onSectionChange, onExitSettings }: SettingsNavProps) {
|
|
59
|
+
return (
|
|
60
|
+
<div className="settings-nav-root" aria-label="Settings sections">
|
|
61
|
+
{/* Back button */}
|
|
62
|
+
<button
|
|
63
|
+
type="button"
|
|
64
|
+
className="settings-nav-back"
|
|
65
|
+
onClick={onExitSettings}
|
|
66
|
+
aria-label="Exit settings"
|
|
67
|
+
>
|
|
68
|
+
<ArrowLeft size={14} />
|
|
69
|
+
<span>Back</span>
|
|
70
|
+
</button>
|
|
71
|
+
|
|
72
|
+
<div className="settings-nav-divider" aria-hidden="true" />
|
|
73
|
+
|
|
74
|
+
{/* Section groups */}
|
|
75
|
+
{SECTION_GROUPS.map((group) => (
|
|
76
|
+
<div key={group.label} className="settings-nav-group">
|
|
77
|
+
<div className="settings-nav-group-label">{group.label}</div>
|
|
78
|
+
{group.sections.map((s) => {
|
|
79
|
+
const Icon = s.icon;
|
|
80
|
+
const active = activeSection === s.id;
|
|
81
|
+
return (
|
|
82
|
+
<button
|
|
83
|
+
key={s.id}
|
|
84
|
+
type="button"
|
|
85
|
+
className={cn('settings-nav-item', active && 'settings-nav-item-active')}
|
|
86
|
+
onClick={() => onSectionChange(active ? null : s.id)}
|
|
87
|
+
aria-current={active ? 'page' : undefined}
|
|
88
|
+
>
|
|
89
|
+
{Icon && <Icon size={14} className="settings-nav-item-icon" aria-hidden />}
|
|
90
|
+
<span className="settings-nav-item-label">{s.label}</span>
|
|
91
|
+
{active && (
|
|
92
|
+
<ChevronRight size={12} className="settings-nav-item-chevron" aria-hidden />
|
|
93
|
+
)}
|
|
94
|
+
</button>
|
|
95
|
+
);
|
|
96
|
+
})}
|
|
97
|
+
</div>
|
|
98
|
+
))}
|
|
99
|
+
</div>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
@@ -1,14 +1,37 @@
|
|
|
1
1
|
// src/components/Sidebar.tsx — vertical navigation rail for sidebar/both layouts.
|
|
2
2
|
import type { TabDef } from './Topbar';
|
|
3
|
+
import { SettingsNav } from './SettingsNav';
|
|
3
4
|
import { cn } from '../lib/utils';
|
|
4
5
|
|
|
5
6
|
export type SidebarProps = {
|
|
6
7
|
tabs: TabDef[];
|
|
7
8
|
activeTab: string;
|
|
8
9
|
onTabChange: (id: string) => void;
|
|
10
|
+
/** v4.9.0 — When true, show the full settings sidebar nav instead of tabs. */
|
|
11
|
+
settingsMode?: boolean;
|
|
12
|
+
/** v4.9.0 — The currently highlighted settings section. */
|
|
13
|
+
settingsActiveSection?: string | null;
|
|
14
|
+
/** v4.9.0 — Called when user clicks a section in the settings sidebar. */
|
|
15
|
+
onSettingsSectionChange?: (id: string | null) => void;
|
|
16
|
+
/** v4.9.0 — Called when user clicks the back button in settings mode. */
|
|
17
|
+
onExitSettings?: () => void;
|
|
9
18
|
};
|
|
10
19
|
|
|
11
|
-
export function Sidebar({ tabs, activeTab, onTabChange }: SidebarProps) {
|
|
20
|
+
export function Sidebar({ tabs, activeTab, onTabChange, settingsMode, settingsActiveSection, onSettingsSectionChange, onExitSettings }: SidebarProps) {
|
|
21
|
+
// v4.9.0 — When settingsMode is active, render the full settings nav instead
|
|
22
|
+
// of the normal tab rail. This lets users see all sections at a glance.
|
|
23
|
+
if (settingsMode) {
|
|
24
|
+
return (
|
|
25
|
+
<aside className="sidebar sidebar-settings-mode" aria-label="Settings navigation">
|
|
26
|
+
<SettingsNav
|
|
27
|
+
activeSection={settingsActiveSection ?? null}
|
|
28
|
+
onSectionChange={onSettingsSectionChange ?? (() => {})}
|
|
29
|
+
onExitSettings={onExitSettings ?? (() => {})}
|
|
30
|
+
/>
|
|
31
|
+
</aside>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
12
35
|
// Split tabs into built-in (no `isMod` flag) and mod-added entries.
|
|
13
36
|
// Render a separator + small "Mods" label between them so users can
|
|
14
37
|
// tell what's part of Bizar vs what an installed mod contributed.
|
|
@@ -64,3 +87,4 @@ export function Sidebar({ tabs, activeTab, onTabChange }: SidebarProps) {
|
|
|
64
87
|
</aside>
|
|
65
88
|
);
|
|
66
89
|
}
|
|
90
|
+
|
|
@@ -3,21 +3,52 @@
|
|
|
3
3
|
import type { ReactNode } from 'react';
|
|
4
4
|
import { cn } from '../lib/utils';
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* v6.0.0 — Doctor variants. 'ok' / 'warn' / 'fail' map onto
|
|
8
|
+
* 'success' / 'warning' / 'error' respectively so existing CSS
|
|
9
|
+
* rules cover the Doctor page without a new palette entry. The
|
|
10
|
+
* `kind` union is widened (additive only) so every existing caller
|
|
11
|
+
* keeps type-checking against the same set.
|
|
12
|
+
*/
|
|
13
|
+
export type StatusKind =
|
|
14
|
+
| 'success'
|
|
15
|
+
| 'warning'
|
|
16
|
+
| 'error'
|
|
17
|
+
| 'info'
|
|
18
|
+
| 'accent'
|
|
19
|
+
| 'neutral'
|
|
20
|
+
| 'ok'
|
|
21
|
+
| 'warn'
|
|
22
|
+
| 'fail';
|
|
23
|
+
|
|
6
24
|
export type StatusBadgeProps = {
|
|
7
|
-
kind?:
|
|
25
|
+
kind?: StatusKind;
|
|
8
26
|
children: ReactNode;
|
|
9
27
|
className?: string;
|
|
10
28
|
dot?: boolean;
|
|
11
29
|
};
|
|
12
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Map a doctor-style status to the underlying badge kind. Done at
|
|
33
|
+
* render time so callers don't have to translate; the CSS class is
|
|
34
|
+
* always one of `badge-success | badge-warning | badge-error | …`.
|
|
35
|
+
*/
|
|
36
|
+
function normalizeKind(kind: StatusKind): Exclude<StatusKind, 'ok' | 'warn' | 'fail'> {
|
|
37
|
+
if (kind === 'ok') return 'success';
|
|
38
|
+
if (kind === 'warn') return 'warning';
|
|
39
|
+
if (kind === 'fail') return 'error';
|
|
40
|
+
return kind;
|
|
41
|
+
}
|
|
42
|
+
|
|
13
43
|
export function StatusBadge({
|
|
14
44
|
kind = 'neutral',
|
|
15
45
|
children,
|
|
16
46
|
className,
|
|
17
47
|
dot = false,
|
|
18
48
|
}: StatusBadgeProps) {
|
|
49
|
+
const normalized = normalizeKind(kind);
|
|
19
50
|
return (
|
|
20
|
-
<span className={cn('badge', `badge-${
|
|
51
|
+
<span className={cn('badge', `badge-${normalized}`, className)} title={`status: ${kind}`}>
|
|
21
52
|
{dot && <span className="badge-dot" />}
|
|
22
53
|
{children}
|
|
23
54
|
</span>
|
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
Radio,
|
|
24
24
|
Coins,
|
|
25
25
|
Brain,
|
|
26
|
+
Stethoscope,
|
|
26
27
|
type LucideIcon,
|
|
27
28
|
} from 'lucide-react';
|
|
28
29
|
import { cn } from '../lib/utils';
|
|
@@ -52,6 +53,11 @@ export const TABS: TabDef[] = [
|
|
|
52
53
|
{ id: 'schedules', label: 'Schedules', icon: Clock },
|
|
53
54
|
{ id: 'history', label: 'History', icon: HistoryIcon },
|
|
54
55
|
{ id: 'minimax', label: 'Usage', icon: Coins },
|
|
56
|
+
// v6.0.0 — Doctor page. Lives between Overview and Settings so the
|
|
57
|
+
// "is everything healthy?" question is always one click from the
|
|
58
|
+
// home screen and from the settings surface (where an operator
|
|
59
|
+
// typically arrives after something looks off).
|
|
60
|
+
{ id: 'doctor', label: 'Doctor', icon: Stethoscope },
|
|
55
61
|
{ id: 'settings', label: 'Settings', icon: Sliders },
|
|
56
62
|
];
|
|
57
63
|
|
|
@@ -155,6 +161,11 @@ export function Topbar({
|
|
|
155
161
|
>
|
|
156
162
|
<Icon size={14} className="tab-icon" />
|
|
157
163
|
<span className="tab-label">{tab.label}</span>
|
|
164
|
+
{tab.id === 'settings' && active && (
|
|
165
|
+
<span className="settings-mode-indicator" title="Settings mode active">
|
|
166
|
+
<Settings2 size={12} />
|
|
167
|
+
</span>
|
|
168
|
+
)}
|
|
158
169
|
</button>
|
|
159
170
|
);
|
|
160
171
|
})}
|