bloby-bot 0.56.0 → 0.57.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/dist-bloby/assets/{bloby-pIi5JIgB.js → bloby-C9KZKz5D.js} +77 -77
- package/dist-bloby/assets/globals-B2MTxIvu.js +21 -0
- package/dist-bloby/assets/globals-mDbk7EdD.css +2 -0
- package/dist-bloby/assets/{highlighted-body-OFNGDK62-DoHcZIYO.js → highlighted-body-OFNGDK62-CBsGYiqi.js} +1 -1
- package/dist-bloby/assets/mermaid-GHXKKRXX-D1dLke0q.js +1 -0
- package/dist-bloby/assets/{onboard-CeuzZ0Ku.js → onboard-BmDzpzBl.js} +1 -1
- package/dist-bloby/bloby.html +3 -3
- package/dist-bloby/onboard.html +3 -3
- package/package.json +1 -1
- package/supervisor/chat/OnboardWizard.tsx +459 -29
- package/supervisor/index.ts +68 -1
- package/dist-bloby/assets/globals-BqjrOUer.css +0 -2
- package/dist-bloby/assets/globals-DSmE2rZO.js +0 -21
- package/dist-bloby/assets/mermaid-GHXKKRXX-hIqAdC54.js +0 -1
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { useState, useRef, useEffect, type KeyboardEvent } from 'react';
|
|
1
|
+
import { useState, useRef, useEffect, type KeyboardEvent, type MutableRefObject } from 'react';
|
|
2
2
|
import { createPortal } from 'react-dom';
|
|
3
|
-
import { ArrowRight, ArrowLeft, LoaderCircle, ExternalLink, ClipboardPaste, RefreshCw, Check, ChevronDown, Mic, Eye, EyeOff, Shield, ShieldCheck, ShieldOff, Copy, Smartphone, Globe, Wifi, WifiOff, TriangleAlert } from 'lucide-react';
|
|
3
|
+
import { ArrowRight, ArrowLeft, LoaderCircle, ExternalLink, ClipboardPaste, RefreshCw, Check, ChevronDown, Mic, Eye, EyeOff, Shield, ShieldCheck, ShieldOff, Copy, Smartphone, Globe, Wifi, WifiOff, TriangleAlert, X, Plus, KeyRound } from 'lucide-react';
|
|
4
4
|
import { motion, AnimatePresence } from 'framer-motion';
|
|
5
|
+
import { authFetch } from './src/lib/auth';
|
|
5
6
|
|
|
6
7
|
/* ── Access detection ── */
|
|
7
8
|
|
|
@@ -86,7 +87,7 @@ const HANDLES = [
|
|
|
86
87
|
|
|
87
88
|
/* ── Dropdown ── */
|
|
88
89
|
|
|
89
|
-
function ModelDropdown({ models, value, onChange }: { models: { id: string; label: string }[]; value: string; onChange: (id: string) => void }) {
|
|
90
|
+
function ModelDropdown({ models, value, onChange, placeholder = 'Choose a model...' }: { models: { id: string; label: string }[]; value: string; onChange: (id: string) => void; placeholder?: string }) {
|
|
90
91
|
const [open, setOpen] = useState(false);
|
|
91
92
|
const [pos, setPos] = useState<{ top: number; left: number; width: number } | null>(null);
|
|
92
93
|
const btnRef = useRef<HTMLButtonElement>(null);
|
|
@@ -126,7 +127,7 @@ function ModelDropdown({ models, value, onChange }: { models: { id: string; labe
|
|
|
126
127
|
className="w-full flex items-center justify-between gap-2 bg-white/[0.03] border border-white/[0.08] text-white rounded-xl px-4 py-2.5 text-[13px] outline-none hover:border-white/15 focus:border-[#0069FE]/30 transition-colors overflow-hidden"
|
|
127
128
|
>
|
|
128
129
|
<span className={`min-w-0 truncate text-left ${selected ? 'text-white' : 'text-white/20'}`}>
|
|
129
|
-
{selected ? selected.label :
|
|
130
|
+
{selected ? selected.label : placeholder}
|
|
130
131
|
</span>
|
|
131
132
|
<ChevronDown className={`h-4 w-4 shrink-0 text-white/30 transition-transform ${open ? 'rotate-180' : ''}`} />
|
|
132
133
|
</button>
|
|
@@ -156,6 +157,370 @@ function ModelDropdown({ models, value, onChange }: { models: { id: string; labe
|
|
|
156
157
|
);
|
|
157
158
|
}
|
|
158
159
|
|
|
160
|
+
/* ── Settings mode: screen registry + jump menu ── */
|
|
161
|
+
|
|
162
|
+
// The jumpable settings screens (settings re-run only). `step` maps to the wizard step index.
|
|
163
|
+
// Environment Variables (step 6) reuses the slot the onboarding-only "All Set" screen occupies.
|
|
164
|
+
const SETTINGS_SCREENS: { step: number; label: string }[] = [
|
|
165
|
+
{ step: 1, label: 'Personal Info' },
|
|
166
|
+
{ step: 2, label: 'Agent Name & Access' },
|
|
167
|
+
{ step: 3, label: 'Security' },
|
|
168
|
+
{ step: 4, label: 'AI Provider' },
|
|
169
|
+
{ step: 5, label: 'Voice Messages' },
|
|
170
|
+
{ step: 6, label: 'Environment Variables' },
|
|
171
|
+
];
|
|
172
|
+
|
|
173
|
+
// Compact "Go to" dropdown shown in the settings header so the user can jump to any screen.
|
|
174
|
+
function GoToMenu({ onJump }: { onJump: (step: number) => void }) {
|
|
175
|
+
const [open, setOpen] = useState(false);
|
|
176
|
+
const [pos, setPos] = useState<{ top: number; right: number } | null>(null);
|
|
177
|
+
const btnRef = useRef<HTMLButtonElement>(null);
|
|
178
|
+
const menuRef = useRef<HTMLDivElement>(null);
|
|
179
|
+
|
|
180
|
+
useEffect(() => {
|
|
181
|
+
if (!open) return;
|
|
182
|
+
const recalc = () => {
|
|
183
|
+
const r = btnRef.current?.getBoundingClientRect();
|
|
184
|
+
if (r) setPos({ top: r.bottom + 6, right: window.innerWidth - r.right });
|
|
185
|
+
};
|
|
186
|
+
recalc();
|
|
187
|
+
const handler = (e: MouseEvent) => {
|
|
188
|
+
const t = e.target as Node;
|
|
189
|
+
if (btnRef.current?.contains(t) || menuRef.current?.contains(t)) return;
|
|
190
|
+
setOpen(false);
|
|
191
|
+
};
|
|
192
|
+
document.addEventListener('mousedown', handler);
|
|
193
|
+
window.addEventListener('resize', recalc);
|
|
194
|
+
window.addEventListener('scroll', recalc, true);
|
|
195
|
+
return () => {
|
|
196
|
+
document.removeEventListener('mousedown', handler);
|
|
197
|
+
window.removeEventListener('resize', recalc);
|
|
198
|
+
window.removeEventListener('scroll', recalc, true);
|
|
199
|
+
};
|
|
200
|
+
}, [open]);
|
|
201
|
+
|
|
202
|
+
return (
|
|
203
|
+
<>
|
|
204
|
+
<button
|
|
205
|
+
ref={btnRef}
|
|
206
|
+
type="button"
|
|
207
|
+
onClick={() => setOpen((o) => !o)}
|
|
208
|
+
className="flex items-center gap-1 text-white/50 hover:text-white/80 text-[12px] font-medium px-2.5 py-1 rounded-lg hover:bg-white/[0.05] transition-colors"
|
|
209
|
+
>
|
|
210
|
+
Go to
|
|
211
|
+
<ChevronDown className={`h-3.5 w-3.5 transition-transform ${open ? 'rotate-180' : ''}`} />
|
|
212
|
+
</button>
|
|
213
|
+
{open && pos && createPortal(
|
|
214
|
+
<div
|
|
215
|
+
ref={menuRef}
|
|
216
|
+
style={{ position: 'fixed', top: pos.top, right: pos.right, zIndex: 1000 }}
|
|
217
|
+
className="bg-[#222] border border-white/[0.08] rounded-xl shadow-xl py-1 min-w-[200px]"
|
|
218
|
+
>
|
|
219
|
+
{SETTINGS_SCREENS.map((s) => (
|
|
220
|
+
<button
|
|
221
|
+
key={s.step}
|
|
222
|
+
onClick={() => { onJump(s.step); setOpen(false); }}
|
|
223
|
+
className="w-full text-left px-4 py-2 text-[13px] text-white/70 hover:bg-white/[0.04] hover:text-white transition-colors"
|
|
224
|
+
>
|
|
225
|
+
{s.label}
|
|
226
|
+
</button>
|
|
227
|
+
))}
|
|
228
|
+
</div>,
|
|
229
|
+
document.body,
|
|
230
|
+
)}
|
|
231
|
+
</>
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/* ── Settings mode: Environment Variables screen ── */
|
|
236
|
+
|
|
237
|
+
interface EnvVarRow { name: string; value: string }
|
|
238
|
+
interface EnvVarGroup { title: string; vars: EnvVarRow[] }
|
|
239
|
+
interface EnvCache {
|
|
240
|
+
groups: EnvVarGroup[];
|
|
241
|
+
values: Record<string, string>;
|
|
242
|
+
originals: Record<string, string>;
|
|
243
|
+
revealed: string[];
|
|
244
|
+
newRows: { id: number; name: string; value: string }[];
|
|
245
|
+
newRowSeq: number;
|
|
246
|
+
saved: boolean;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const ENV_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
250
|
+
|
|
251
|
+
// A read/write view of the agent-controlled `workspace/.env`, grouped by `# Section` headers.
|
|
252
|
+
// Reads via GET /api/env, writes changed/added keys via POST /api/env (which restarts the backend).
|
|
253
|
+
// Self-saving and self-contained — not part of the wizard's batched onboard payload.
|
|
254
|
+
// `cacheRef` lets the working state survive this screen's unmount/remount when the user
|
|
255
|
+
// navigates to another settings screen and back (the wizard keys each step's motion.div by
|
|
256
|
+
// `step`, so it remounts). It lives in the parent and dies with the wizard, so each fresh
|
|
257
|
+
// open re-fetches from /api/env.
|
|
258
|
+
function EnvSettings({ cacheRef }: { cacheRef: MutableRefObject<EnvCache | null> }) {
|
|
259
|
+
// Whether we already have cached working state at mount — captured once per instance.
|
|
260
|
+
const hadCache = useRef(cacheRef.current != null).current;
|
|
261
|
+
const cached = cacheRef.current;
|
|
262
|
+
const [groups, setGroups] = useState<EnvVarGroup[]>(cached?.groups ?? []);
|
|
263
|
+
const [loading, setLoading] = useState(!hadCache);
|
|
264
|
+
const [loadError, setLoadError] = useState('');
|
|
265
|
+
const [values, setValues] = useState<Record<string, string>>(cached?.values ?? {});
|
|
266
|
+
const [originals, setOriginals] = useState<Record<string, string>>(cached?.originals ?? {});
|
|
267
|
+
const [revealed, setRevealed] = useState<Set<string>>(new Set(cached?.revealed ?? []));
|
|
268
|
+
const [newRows, setNewRows] = useState<{ id: number; name: string; value: string }[]>(cached?.newRows ?? []);
|
|
269
|
+
const newRowId = useRef(cached?.newRowSeq ?? 0);
|
|
270
|
+
const [saving, setSaving] = useState(false);
|
|
271
|
+
const [saved, setSaved] = useState(cached?.saved ?? false);
|
|
272
|
+
const [saveError, setSaveError] = useState('');
|
|
273
|
+
|
|
274
|
+
useEffect(() => {
|
|
275
|
+
if (hadCache) return; // hydrated from cache — don't clobber edits with a refetch
|
|
276
|
+
let cancelled = false;
|
|
277
|
+
authFetch('/api/env')
|
|
278
|
+
.then((r) => (r.ok ? r.json() : Promise.reject(new Error('Failed to load environment variables'))))
|
|
279
|
+
.then((data) => {
|
|
280
|
+
if (cancelled) return;
|
|
281
|
+
const gs: EnvVarGroup[] = Array.isArray(data.groups) ? data.groups : [];
|
|
282
|
+
const init: Record<string, string> = {};
|
|
283
|
+
for (const g of gs) for (const v of g.vars) init[v.name] = v.value;
|
|
284
|
+
setGroups(gs);
|
|
285
|
+
setValues(init);
|
|
286
|
+
setOriginals(init);
|
|
287
|
+
setLoading(false);
|
|
288
|
+
})
|
|
289
|
+
.catch((err) => { if (!cancelled) { setLoadError(err.message || 'Failed to load'); setLoading(false); } });
|
|
290
|
+
return () => { cancelled = true; };
|
|
291
|
+
}, [hadCache]);
|
|
292
|
+
|
|
293
|
+
// Persist working state to the parent ref so navigating away and back doesn't lose edits.
|
|
294
|
+
// Skip while loading so a navigate-away mid-fetch leaves the cache empty → refetch on return.
|
|
295
|
+
useEffect(() => {
|
|
296
|
+
if (loading) return;
|
|
297
|
+
cacheRef.current = { groups, values, originals, revealed: Array.from(revealed), newRows, newRowSeq: newRowId.current, saved };
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
const validNewRows = newRows.filter((r) => r.name.trim() && r.value.trim() && ENV_NAME_RE.test(r.name.trim()));
|
|
301
|
+
const changedExisting = Object.keys(values).filter((k) => values[k] !== originals[k]);
|
|
302
|
+
const dirty = changedExisting.length > 0 || validNewRows.length > 0;
|
|
303
|
+
|
|
304
|
+
const toggleReveal = (name: string) => {
|
|
305
|
+
setRevealed((prev) => {
|
|
306
|
+
const next = new Set(prev);
|
|
307
|
+
if (next.has(name)) next.delete(name); else next.add(name);
|
|
308
|
+
return next;
|
|
309
|
+
});
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
const handleSave = async () => {
|
|
313
|
+
if (!dirty || saving) return;
|
|
314
|
+
setSaving(true); setSaveError(''); setSaved(false);
|
|
315
|
+
const vars: Record<string, string> = {};
|
|
316
|
+
for (const k of changedExisting) vars[k] = values[k];
|
|
317
|
+
for (const r of validNewRows) vars[r.name.trim()] = r.value.trim();
|
|
318
|
+
try {
|
|
319
|
+
const res = await authFetch('/api/env', {
|
|
320
|
+
method: 'POST',
|
|
321
|
+
headers: { 'Content-Type': 'application/json' },
|
|
322
|
+
body: JSON.stringify({ vars }),
|
|
323
|
+
});
|
|
324
|
+
if (!res.ok) {
|
|
325
|
+
const d = await res.json().catch(() => ({ error: 'Save failed' }));
|
|
326
|
+
throw new Error(d.error || 'Save failed');
|
|
327
|
+
}
|
|
328
|
+
// Snapshot of values including the just-added rows, so `dirty` resets after save.
|
|
329
|
+
const merged = { ...values };
|
|
330
|
+
for (const r of validNewRows) merged[r.name.trim()] = r.value.trim();
|
|
331
|
+
// Surface newly-added vars under a "Custom" group in the UI (the writer appends them
|
|
332
|
+
// to the file; on next load they'll re-group under whatever section precedes them).
|
|
333
|
+
setGroups((prev) => {
|
|
334
|
+
const next = prev.map((g) => ({ ...g, vars: [...g.vars] }));
|
|
335
|
+
const existing = new Set(next.flatMap((g) => g.vars.map((v) => v.name)));
|
|
336
|
+
const toAdd = validNewRows.map((r) => r.name.trim()).filter((nm) => !existing.has(nm));
|
|
337
|
+
if (toAdd.length) {
|
|
338
|
+
let custom = next.find((g) => g.title === 'Custom');
|
|
339
|
+
if (!custom) { custom = { title: 'Custom', vars: [] }; next.push(custom); }
|
|
340
|
+
for (const nm of toAdd) custom.vars.push({ name: nm, value: merged[nm] });
|
|
341
|
+
}
|
|
342
|
+
return next;
|
|
343
|
+
});
|
|
344
|
+
setValues(merged);
|
|
345
|
+
setOriginals(merged);
|
|
346
|
+
setNewRows([]);
|
|
347
|
+
setSaved(true);
|
|
348
|
+
setSaving(false);
|
|
349
|
+
} catch (err: any) {
|
|
350
|
+
setSaveError(err.message || 'Save failed');
|
|
351
|
+
setSaving(false);
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
return (
|
|
356
|
+
<div>
|
|
357
|
+
<h1 className="text-xl font-bold text-white tracking-tight">Environment Variables</h1>
|
|
358
|
+
<p className="text-white/40 text-[13px] mt-1.5 leading-relaxed">
|
|
359
|
+
API keys and secrets stored in your workspace{' '}
|
|
360
|
+
<code className="text-white/55 font-mono text-[12px]">.env</code>. Your agent reads and writes these as you build.
|
|
361
|
+
</p>
|
|
362
|
+
|
|
363
|
+
{loading ? (
|
|
364
|
+
<div className="flex items-center justify-center py-12 text-white/40">
|
|
365
|
+
<LoaderCircle className="h-5 w-5 animate-spin" />
|
|
366
|
+
</div>
|
|
367
|
+
) : loadError ? (
|
|
368
|
+
<div className="mt-5 flex items-center gap-2 text-[13px] text-red-400">
|
|
369
|
+
<TriangleAlert className="h-4 w-4 shrink-0" />{loadError}
|
|
370
|
+
</div>
|
|
371
|
+
) : (
|
|
372
|
+
<div className="mt-5 space-y-5 max-h-[44vh] overflow-y-auto pr-1 -mr-1">
|
|
373
|
+
{groups.length === 0 && newRows.length === 0 && (
|
|
374
|
+
<div className="rounded-xl border border-white/[0.06] bg-white/[0.02] px-4 py-7 text-center">
|
|
375
|
+
<KeyRound className="h-5 w-5 text-white/25 mx-auto mb-2" />
|
|
376
|
+
<p className="text-white/40 text-[13px]">No environment variables yet.</p>
|
|
377
|
+
<p className="text-white/25 text-[12px] mt-1">Ask your agent to add an integration, or add one manually below.</p>
|
|
378
|
+
</div>
|
|
379
|
+
)}
|
|
380
|
+
|
|
381
|
+
{groups.map((g) => (
|
|
382
|
+
<div key={g.title || '__untitled'}>
|
|
383
|
+
<div className="flex items-center gap-2 mb-2">
|
|
384
|
+
<KeyRound className="h-3.5 w-3.5 text-[#0069FE]/70" />
|
|
385
|
+
<span className="text-[12px] font-semibold text-white/70 uppercase tracking-wide">{g.title || 'Other'}</span>
|
|
386
|
+
</div>
|
|
387
|
+
<div className="space-y-2.5">
|
|
388
|
+
{g.vars.map((v) => {
|
|
389
|
+
const changed = values[v.name] !== originals[v.name];
|
|
390
|
+
const show = revealed.has(v.name);
|
|
391
|
+
return (
|
|
392
|
+
<div key={v.name}>
|
|
393
|
+
<label className="flex items-center gap-1.5 text-[11px] text-white/40 mb-1">
|
|
394
|
+
<code className="font-mono text-white/50">{v.name}</code>
|
|
395
|
+
{changed && <span className="text-[#0069FE] text-[10px] font-medium">• edited</span>}
|
|
396
|
+
</label>
|
|
397
|
+
<div className="relative">
|
|
398
|
+
<input
|
|
399
|
+
type={show ? 'text' : 'password'}
|
|
400
|
+
value={values[v.name] ?? ''}
|
|
401
|
+
onChange={(e) => setValues((p) => ({ ...p, [v.name]: e.target.value }))}
|
|
402
|
+
autoComplete="off"
|
|
403
|
+
spellCheck={false}
|
|
404
|
+
data-1p-ignore
|
|
405
|
+
data-lpignore="true"
|
|
406
|
+
className="w-full bg-white/[0.03] border border-white/[0.08] text-white rounded-xl pl-4 pr-10 py-2.5 text-[13px] font-mono outline-none focus:border-[#0069FE]/30 transition-colors"
|
|
407
|
+
/>
|
|
408
|
+
<button
|
|
409
|
+
type="button"
|
|
410
|
+
onClick={() => toggleReveal(v.name)}
|
|
411
|
+
className="absolute right-3 top-1/2 -translate-y-1/2 text-white/30 hover:text-white/60 transition-colors"
|
|
412
|
+
aria-label={show ? 'Hide value' : 'Reveal value'}
|
|
413
|
+
>
|
|
414
|
+
{show ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
|
|
415
|
+
</button>
|
|
416
|
+
</div>
|
|
417
|
+
</div>
|
|
418
|
+
);
|
|
419
|
+
})}
|
|
420
|
+
</div>
|
|
421
|
+
</div>
|
|
422
|
+
))}
|
|
423
|
+
|
|
424
|
+
{newRows.length > 0 && (
|
|
425
|
+
<div>
|
|
426
|
+
<div className="flex items-center gap-2 mb-2">
|
|
427
|
+
<Plus className="h-3.5 w-3.5 text-emerald-400/70" />
|
|
428
|
+
<span className="text-[12px] font-semibold text-white/70 uppercase tracking-wide">New</span>
|
|
429
|
+
</div>
|
|
430
|
+
<div className="space-y-3">
|
|
431
|
+
{newRows.map((r) => {
|
|
432
|
+
const nameInvalid = r.name.trim().length > 0 && !ENV_NAME_RE.test(r.name.trim());
|
|
433
|
+
return (
|
|
434
|
+
<div key={r.id} className="flex items-start gap-2">
|
|
435
|
+
<div className="flex-1 space-y-1.5">
|
|
436
|
+
<input
|
|
437
|
+
type="text"
|
|
438
|
+
value={r.name}
|
|
439
|
+
onChange={(e) => setNewRows((p) => p.map((x) => (x.id === r.id ? { ...x, name: e.target.value } : x)))}
|
|
440
|
+
placeholder="VARIABLE_NAME"
|
|
441
|
+
autoComplete="off"
|
|
442
|
+
spellCheck={false}
|
|
443
|
+
data-1p-ignore
|
|
444
|
+
data-lpignore="true"
|
|
445
|
+
className={`w-full bg-white/[0.03] border ${nameInvalid ? 'border-red-500/40' : 'border-white/[0.08]'} text-white rounded-xl px-4 py-2.5 text-[13px] font-mono outline-none focus:border-[#0069FE]/30 transition-colors`}
|
|
446
|
+
/>
|
|
447
|
+
<input
|
|
448
|
+
type="text"
|
|
449
|
+
value={r.value}
|
|
450
|
+
onChange={(e) => setNewRows((p) => p.map((x) => (x.id === r.id ? { ...x, value: e.target.value } : x)))}
|
|
451
|
+
placeholder="value"
|
|
452
|
+
autoComplete="off"
|
|
453
|
+
spellCheck={false}
|
|
454
|
+
data-1p-ignore
|
|
455
|
+
data-lpignore="true"
|
|
456
|
+
className="w-full bg-white/[0.03] border border-white/[0.08] text-white rounded-xl px-4 py-2.5 text-[13px] font-mono outline-none focus:border-[#0069FE]/30 transition-colors"
|
|
457
|
+
/>
|
|
458
|
+
{nameInvalid && (
|
|
459
|
+
<p className="text-red-400/80 text-[11px]">Use letters, numbers and underscores; can't start with a number.</p>
|
|
460
|
+
)}
|
|
461
|
+
</div>
|
|
462
|
+
<button
|
|
463
|
+
type="button"
|
|
464
|
+
onClick={() => setNewRows((p) => p.filter((x) => x.id !== r.id))}
|
|
465
|
+
className="mt-2 text-white/25 hover:text-red-400/80 transition-colors"
|
|
466
|
+
aria-label="Remove"
|
|
467
|
+
>
|
|
468
|
+
<X className="h-4 w-4" />
|
|
469
|
+
</button>
|
|
470
|
+
</div>
|
|
471
|
+
);
|
|
472
|
+
})}
|
|
473
|
+
</div>
|
|
474
|
+
</div>
|
|
475
|
+
)}
|
|
476
|
+
|
|
477
|
+
<button
|
|
478
|
+
type="button"
|
|
479
|
+
onClick={() => setNewRows((p) => [...p, { id: newRowId.current++, name: '', value: '' }])}
|
|
480
|
+
className="flex items-center gap-1.5 text-[12px] text-white/50 hover:text-white/80 transition-colors"
|
|
481
|
+
>
|
|
482
|
+
<Plus className="h-3.5 w-3.5" /> Add new
|
|
483
|
+
</button>
|
|
484
|
+
</div>
|
|
485
|
+
)}
|
|
486
|
+
|
|
487
|
+
{dirty && !saving && (
|
|
488
|
+
<div className="mt-4 flex items-start gap-2.5 bg-amber-500/[0.06] border border-amber-500/20 rounded-xl px-4 py-3">
|
|
489
|
+
<TriangleAlert className="h-4 w-4 text-amber-400 mt-0.5 shrink-0" />
|
|
490
|
+
<p className="text-amber-300/80 text-[12px] leading-relaxed">
|
|
491
|
+
API key change detected. Your workspace backend will be restarted when you save.
|
|
492
|
+
</p>
|
|
493
|
+
</div>
|
|
494
|
+
)}
|
|
495
|
+
|
|
496
|
+
{saved && !dirty && (
|
|
497
|
+
<div className="mt-4 flex items-center gap-2.5 bg-emerald-500/[0.06] border border-emerald-500/20 rounded-xl px-4 py-3">
|
|
498
|
+
<Check className="h-4 w-4 text-emerald-400 shrink-0" />
|
|
499
|
+
<p className="text-emerald-300/90 text-[12px]">Saved — your workspace backend is restarting.</p>
|
|
500
|
+
</div>
|
|
501
|
+
)}
|
|
502
|
+
|
|
503
|
+
{saveError && (
|
|
504
|
+
<div className="mt-3 flex items-center gap-1.5 text-[12px] text-red-400">
|
|
505
|
+
<TriangleAlert className="h-3.5 w-3.5 shrink-0" />{saveError}
|
|
506
|
+
</div>
|
|
507
|
+
)}
|
|
508
|
+
|
|
509
|
+
<button
|
|
510
|
+
onClick={handleSave}
|
|
511
|
+
disabled={!dirty || saving}
|
|
512
|
+
className="w-full mt-5 py-3 bg-gradient-brand hover:opacity-90 text-white text-[14px] font-semibold rounded-full transition-colors flex items-center justify-center gap-2 disabled:opacity-40 disabled:cursor-not-allowed"
|
|
513
|
+
>
|
|
514
|
+
{saving ? (
|
|
515
|
+
<><LoaderCircle className="h-4 w-4 animate-spin" />Saving...</>
|
|
516
|
+
) : (
|
|
517
|
+
<><RefreshCw className="h-4 w-4" />Save & restart backend</>
|
|
518
|
+
)}
|
|
519
|
+
</button>
|
|
520
|
+
</div>
|
|
521
|
+
);
|
|
522
|
+
}
|
|
523
|
+
|
|
159
524
|
/* ── Component ── */
|
|
160
525
|
|
|
161
526
|
interface Props {
|
|
@@ -166,7 +531,8 @@ interface Props {
|
|
|
166
531
|
}
|
|
167
532
|
|
|
168
533
|
export default function OnboardWizard({ onComplete, isInitialSetup = false, onSave, onTunnelSwitch }: Props) {
|
|
169
|
-
|
|
534
|
+
// 0..5 shared. Step 6 = "All Set" (onboarding) OR "Environment Variables" (settings re-run).
|
|
535
|
+
const TOTAL_STEPS = 7;
|
|
170
536
|
|
|
171
537
|
const [step, setStep] = useState(0);
|
|
172
538
|
const [userName, setUserName] = useState('');
|
|
@@ -317,6 +683,10 @@ export default function OnboardWizard({ onComplete, isInitialSetup = false, onSa
|
|
|
317
683
|
// Pre-fill guard
|
|
318
684
|
const prefillDone = useRef(false);
|
|
319
685
|
|
|
686
|
+
// Environment Variables screen working-state cache (settings mode). Survives the per-step
|
|
687
|
+
// remount so edits aren't lost when navigating between settings screens; dies with the wizard.
|
|
688
|
+
const envCacheRef = useRef<EnvCache | null>(null);
|
|
689
|
+
|
|
320
690
|
const isConnected = authState[provider] === 'connected';
|
|
321
691
|
|
|
322
692
|
// Persist/restore TOTP setup state across page reloads (mobile: OS suspends PWA on app-switch)
|
|
@@ -946,6 +1316,7 @@ export default function OnboardWizard({ onComplete, isInitialSetup = false, onSa
|
|
|
946
1316
|
}
|
|
947
1317
|
case 4: return !!(provider && model && isConnected);
|
|
948
1318
|
case 5: return true;
|
|
1319
|
+
case 6: return true; // Environment Variables (settings) — self-saving, last screen
|
|
949
1320
|
default: return false;
|
|
950
1321
|
}
|
|
951
1322
|
})();
|
|
@@ -1011,21 +1382,46 @@ export default function OnboardWizard({ onComplete, isInitialSetup = false, onSa
|
|
|
1011
1382
|
transition={{ duration: 0.3 }}
|
|
1012
1383
|
className="relative w-full max-w-[480px] bg-[#181818] border border-white/[0.06] rounded-[24px] shadow-2xl overflow-hidden"
|
|
1013
1384
|
>
|
|
1014
|
-
{/*
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1385
|
+
{/* Onboarding: step dots. Settings: a header with hub-back, screen jump, and close. */}
|
|
1386
|
+
{isInitialSetup ? (
|
|
1387
|
+
<div className="flex justify-center gap-2 pt-6">
|
|
1388
|
+
{Array.from({ length: TOTAL_STEPS }, (_, i) => (
|
|
1389
|
+
<div
|
|
1390
|
+
key={i}
|
|
1391
|
+
className={`h-1.5 rounded-full transition-all duration-300 ${
|
|
1392
|
+
i === step
|
|
1393
|
+
? 'w-7 bg-gradient-brand'
|
|
1394
|
+
: i < step
|
|
1395
|
+
? 'w-1.5 bg-gradient-brand opacity-60'
|
|
1396
|
+
: 'w-1.5 bg-white/10'
|
|
1397
|
+
}`}
|
|
1398
|
+
/>
|
|
1399
|
+
))}
|
|
1400
|
+
</div>
|
|
1401
|
+
) : (
|
|
1402
|
+
<div className="flex items-center justify-between px-5 pt-4 pb-1">
|
|
1403
|
+
{step === 0 ? (
|
|
1404
|
+
<span className="text-[13px] font-semibold text-white/80 pl-1">Settings</span>
|
|
1405
|
+
) : (
|
|
1406
|
+
<button
|
|
1407
|
+
onClick={() => setStep(0)}
|
|
1408
|
+
className="flex items-center gap-1 text-white/50 hover:text-white/80 text-[13px] font-medium pl-1 pr-2 py-1 rounded-lg hover:bg-white/[0.05] transition-colors"
|
|
1409
|
+
>
|
|
1410
|
+
<ArrowLeft className="h-3.5 w-3.5" /> Settings
|
|
1411
|
+
</button>
|
|
1412
|
+
)}
|
|
1413
|
+
<div className="flex items-center gap-0.5">
|
|
1414
|
+
{step !== 0 && <GoToMenu onJump={(s) => setStep(s)} />}
|
|
1415
|
+
<button
|
|
1416
|
+
onClick={onComplete}
|
|
1417
|
+
className="text-white/40 hover:text-white/80 p-1.5 rounded-lg hover:bg-white/[0.05] transition-colors"
|
|
1418
|
+
aria-label="Close settings"
|
|
1419
|
+
>
|
|
1420
|
+
<X className="h-4 w-4" />
|
|
1421
|
+
</button>
|
|
1422
|
+
</div>
|
|
1423
|
+
</div>
|
|
1424
|
+
)}
|
|
1029
1425
|
|
|
1030
1426
|
{/* Content */}
|
|
1031
1427
|
<AnimatePresence mode="wait">
|
|
@@ -1037,8 +1433,8 @@ export default function OnboardWizard({ onComplete, isInitialSetup = false, onSa
|
|
|
1037
1433
|
transition={{ duration: 0.2, ease: 'easeOut' }}
|
|
1038
1434
|
className="px-8 pt-6 pb-8"
|
|
1039
1435
|
>
|
|
1040
|
-
{/* ── Step 0: Welcome ── */}
|
|
1041
|
-
{step === 0 && (
|
|
1436
|
+
{/* ── Step 0: Welcome (onboarding) ── */}
|
|
1437
|
+
{step === 0 && isInitialSetup && (
|
|
1042
1438
|
<div className="flex flex-col items-center text-center">
|
|
1043
1439
|
<video autoPlay loop muted playsInline className="h-[180px] mb-4">
|
|
1044
1440
|
<source src="/bloby_say_hi.mov" type='video/mp4; codecs="hvc1"' />
|
|
@@ -1060,6 +1456,37 @@ export default function OnboardWizard({ onComplete, isInitialSetup = false, onSa
|
|
|
1060
1456
|
</div>
|
|
1061
1457
|
)}
|
|
1062
1458
|
|
|
1459
|
+
{/* ── Step 0: Settings hub (settings re-run) ── */}
|
|
1460
|
+
{step === 0 && !isInitialSetup && (
|
|
1461
|
+
<div className="flex flex-col items-center text-center">
|
|
1462
|
+
<video autoPlay loop muted playsInline className="h-[150px] mb-4">
|
|
1463
|
+
<source src="/bloby_say_hi.mov" type='video/mp4; codecs="hvc1"' />
|
|
1464
|
+
<source src="/bloby_say_hi.webm" type="video/webm" />
|
|
1465
|
+
</video>
|
|
1466
|
+
<h1 className="text-2xl font-bold text-white tracking-tight">
|
|
1467
|
+
Settings
|
|
1468
|
+
</h1>
|
|
1469
|
+
<p className="text-white/40 text-[14px] mt-2 leading-relaxed max-w-[320px]">
|
|
1470
|
+
What would you like to change?
|
|
1471
|
+
</p>
|
|
1472
|
+
<div className="w-full mt-5">
|
|
1473
|
+
<ModelDropdown
|
|
1474
|
+
models={SETTINGS_SCREENS.map((s) => ({ id: String(s.step), label: s.label }))}
|
|
1475
|
+
value=""
|
|
1476
|
+
placeholder="Select a setting…"
|
|
1477
|
+
onChange={(id) => setStep(Number(id))}
|
|
1478
|
+
/>
|
|
1479
|
+
</div>
|
|
1480
|
+
<button
|
|
1481
|
+
onClick={next}
|
|
1482
|
+
className="mt-4 px-7 py-3 bg-gradient-brand hover:opacity-90 text-white text-[14px] font-semibold rounded-full transition-colors flex items-center gap-2"
|
|
1483
|
+
>
|
|
1484
|
+
Continue
|
|
1485
|
+
<ArrowRight className="h-4 w-4" />
|
|
1486
|
+
</button>
|
|
1487
|
+
</div>
|
|
1488
|
+
)}
|
|
1489
|
+
|
|
1063
1490
|
{/* ── Step 1: Your name ── */}
|
|
1064
1491
|
{step === 1 && (
|
|
1065
1492
|
<div>
|
|
@@ -1097,7 +1524,7 @@ export default function OnboardWizard({ onComplete, isInitialSetup = false, onSa
|
|
|
1097
1524
|
{step === 2 && tunnelMode === 'off' && (
|
|
1098
1525
|
<div>
|
|
1099
1526
|
<h1 className="text-xl font-bold text-white tracking-tight">
|
|
1100
|
-
|
|
1527
|
+
Agent Name & Access
|
|
1101
1528
|
</h1>
|
|
1102
1529
|
<p className="text-white/40 text-[13px] mt-1.5 leading-relaxed">
|
|
1103
1530
|
Give your bot a name. This is used throughout the app as your bot's identity.
|
|
@@ -1219,7 +1646,7 @@ export default function OnboardWizard({ onComplete, isInitialSetup = false, onSa
|
|
|
1219
1646
|
{step === 2 && tunnelMode === 'named' && (
|
|
1220
1647
|
<div>
|
|
1221
1648
|
<h1 className="text-xl font-bold text-white tracking-tight">
|
|
1222
|
-
|
|
1649
|
+
Agent Name & Access
|
|
1223
1650
|
</h1>
|
|
1224
1651
|
<p className="text-white/40 text-[13px] mt-1.5 leading-relaxed">
|
|
1225
1652
|
This is your bot's identity. Your named tunnel domain is already configured.
|
|
@@ -1266,7 +1693,7 @@ export default function OnboardWizard({ onComplete, isInitialSetup = false, onSa
|
|
|
1266
1693
|
{step === 2 && tunnelMode === 'quick' && (
|
|
1267
1694
|
<div>
|
|
1268
1695
|
<h1 className="text-xl font-bold text-white tracking-tight">
|
|
1269
|
-
|
|
1696
|
+
Agent Name & Access
|
|
1270
1697
|
</h1>
|
|
1271
1698
|
<p className="text-white/40 text-[13px] mt-1.5 leading-relaxed">
|
|
1272
1699
|
This is your bot's name and permanent handle — access it from anywhere.
|
|
@@ -2821,9 +3248,9 @@ export default function OnboardWizard({ onComplete, isInitialSetup = false, onSa
|
|
|
2821
3248
|
className="w-full mt-5 py-3 bg-gradient-brand hover:opacity-90 text-white text-[14px] font-semibold rounded-full transition-colors flex items-center justify-center gap-2 disabled:opacity-40"
|
|
2822
3249
|
>
|
|
2823
3250
|
{saving ? (
|
|
2824
|
-
<><LoaderCircle className="h-4 w-4 animate-spin" />Setting up
|
|
3251
|
+
<><LoaderCircle className="h-4 w-4 animate-spin" />{isInitialSetup ? 'Setting up...' : 'Saving...'}</>
|
|
2825
3252
|
) : (
|
|
2826
|
-
<>Complete Setup<ArrowRight className="h-4 w-4" /></>
|
|
3253
|
+
<>{isInitialSetup ? 'Complete Setup' : 'Save Changes'}<ArrowRight className="h-4 w-4" /></>
|
|
2827
3254
|
)}
|
|
2828
3255
|
</button>
|
|
2829
3256
|
|
|
@@ -2943,11 +3370,14 @@ export default function OnboardWizard({ onComplete, isInitialSetup = false, onSa
|
|
|
2943
3370
|
</div>
|
|
2944
3371
|
);
|
|
2945
3372
|
})()}
|
|
3373
|
+
|
|
3374
|
+
{/* ── Step 6: Environment Variables (settings re-run only) ── */}
|
|
3375
|
+
{step === 6 && !isInitialSetup && <EnvSettings cacheRef={envCacheRef} />}
|
|
2946
3376
|
</motion.div>
|
|
2947
3377
|
</AnimatePresence>
|
|
2948
3378
|
|
|
2949
|
-
{/* Back
|
|
2950
|
-
{step > 0 && step < TOTAL_STEPS - 1 &&
|
|
3379
|
+
{/* Back link — onboarding only; settings mode navigates via the header. */}
|
|
3380
|
+
{isInitialSetup && step > 0 && step < TOTAL_STEPS - 1 && (
|
|
2951
3381
|
<div className="px-8 pb-5 -mt-3">
|
|
2952
3382
|
<button
|
|
2953
3383
|
onClick={back}
|