bloby-bot 0.39.1 → 0.40.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.
Files changed (28) hide show
  1. package/package.json +1 -1
  2. package/workspace/client/index.html +0 -3
  3. package/workspace/client/src/App.tsx +9 -20
  4. package/workspace/client/src/components/Dashboard/DashboardPage.tsx +113 -117
  5. package/workspace/client/src/components/Layout/DashboardLayout.tsx +34 -32
  6. package/workspace/client/src/components/Layout/MobileNav.tsx +6 -103
  7. package/workspace/client/src/components/Layout/Sidebar.tsx +10 -11
  8. package/workspace/client/src/styles/globals.css +4 -89
  9. package/workspace/client/public/.well-known/assetlinks.json +0 -8
  10. package/workspace/client/public/bloby-cyberpunk.png +0 -0
  11. package/workspace/client/public/brand/blackrock.svg +0 -8
  12. package/workspace/client/public/kid-breakfast.png +0 -0
  13. package/workspace/client/public/wallpapers/bg.jpg +0 -0
  14. package/workspace/client/public/wallpapers/crypto_bg.png +0 -0
  15. package/workspace/client/public/wallpapers/wp-dusk.jpg +0 -0
  16. package/workspace/client/public/wallpapers/wp-mountain.jpg +0 -0
  17. package/workspace/client/public/wallpapers/wp-ocean.jpg +0 -0
  18. package/workspace/client/src/components/Dashboard/AiChatPage.tsx +0 -145
  19. package/workspace/client/src/components/Dashboard/CryptoPage.tsx +0 -470
  20. package/workspace/client/src/components/Dashboard/WishlistPage.tsx +0 -464
  21. package/workspace/client/src/components/Layout/MiniSidebar.tsx +0 -64
  22. package/workspace/client/src/components/Lock/PinInput.tsx +0 -107
  23. package/workspace/client/src/components/Lock/WorkspaceLock.tsx +0 -484
  24. package/workspace/client/src/components/StickyNotes/StickyNotesOverlay.tsx +0 -396
  25. package/workspace/client/src/components/StickyNotes/StickyNotesSettingsPage.tsx +0 -427
  26. package/workspace/client/src/components/Wallpaper/WallpaperBackground.tsx +0 -12
  27. package/workspace/client/src/components/Wallpaper/WallpaperContext.tsx +0 -160
  28. package/workspace/client/src/components/Wallpaper/WallpaperPicker.tsx +0 -67
@@ -1,484 +0,0 @@
1
- import { useState, useEffect, useRef, useCallback } from 'react';
2
- import { Shield, KeyRound, Lock, ArrowLeft, Check, Loader2, Eye, EyeOff } from 'lucide-react';
3
- import { cn } from '@/lib/utils';
4
- import { PinInput } from './PinInput';
5
-
6
- // ── Types ─────────────────────────────────────────────────────────────
7
-
8
- type LockState =
9
- | 'loading'
10
- | 'setup-choose'
11
- | 'setup-create'
12
- | 'setup-confirm'
13
- | 'locked'
14
- | 'unlocked';
15
-
16
- type LockType = 'pin' | 'password';
17
-
18
- const SESSION_KEY = 'workspace_lock_session';
19
-
20
- // ── IMPORTANT ─────────────────────────────────────────────────────────
21
- // The API_BASE below must match how your workspace proxies requests to
22
- // the backend. Default Bloby workspaces use "/app/api" because the
23
- // supervisor proxies /app/api/* → backend. If your workspace serves the
24
- // backend directly (no proxy), change this to "/api".
25
- const API_BASE = '/app/api';
26
-
27
- // ── Component ─────────────────────────────────────────────────────────
28
-
29
- export function WorkspaceLock({ children }: { children: React.ReactNode }) {
30
- const [state, setState] = useState<LockState>('loading');
31
- const [lockType, setLockType] = useState<LockType | null>(null);
32
- const [selectedType, setSelectedType] = useState<LockType | null>(null);
33
- const [value, setValue] = useState('');
34
- const [error, setError] = useState('');
35
- const [shaking, setShaking] = useState(false);
36
- const [submitting, setSubmitting] = useState(false);
37
- const [showPassword, setShowPassword] = useState(false);
38
- const [fadeKey, setFadeKey] = useState(0);
39
- const firstEntry = useRef('');
40
-
41
- const transitionTo = useCallback((next: LockState) => {
42
- setFadeKey((k) => k + 1);
43
- setState(next);
44
- }, []);
45
-
46
- const triggerShake = useCallback(() => {
47
- setShaking(true);
48
- setTimeout(() => setShaking(false), 500);
49
- }, []);
50
-
51
- // ── Check lock status on mount ──────────────────────────────────────
52
- useEffect(() => {
53
- (async () => {
54
- try {
55
- const res = await fetch(`${API_BASE}/lock/status`, { signal: AbortSignal.timeout(3000) });
56
- const data = await res.json();
57
-
58
- if (!data.configured) {
59
- transitionTo('setup-choose');
60
- return;
61
- }
62
-
63
- setLockType(data.type);
64
-
65
- const token = localStorage.getItem(SESSION_KEY);
66
- if (token) {
67
- const vRes = await fetch(`${API_BASE}/lock/verify`, {
68
- method: 'POST',
69
- headers: { 'Content-Type': 'application/json' },
70
- body: JSON.stringify({ token }),
71
- });
72
- const vData = await vRes.json();
73
- if (vData.valid) {
74
- setState('unlocked');
75
- return;
76
- }
77
- localStorage.removeItem(SESSION_KEY);
78
- }
79
-
80
- transitionTo('locked');
81
- } catch {
82
- // Fail-open if backend is unreachable
83
- setState('unlocked');
84
- }
85
- })();
86
- }, [transitionTo]);
87
-
88
- // ── Actions ─────────────────────────────────────────────────────────
89
-
90
- const doSetup = async (type: LockType, val: string) => {
91
- setSubmitting(true);
92
- try {
93
- const res = await fetch(`${API_BASE}/lock/setup`, {
94
- method: 'POST',
95
- headers: { 'Content-Type': 'application/json' },
96
- body: JSON.stringify({ type, value: val }),
97
- });
98
- const data = await res.json();
99
- if (data.token) localStorage.setItem(SESSION_KEY, data.token);
100
- setState('unlocked');
101
- } catch {
102
- setError('Failed to save. Try again.');
103
- triggerShake();
104
- } finally {
105
- setSubmitting(false);
106
- }
107
- };
108
-
109
- const doVerify = async (val: string) => {
110
- setSubmitting(true);
111
- try {
112
- const res = await fetch(`${API_BASE}/lock/verify`, {
113
- method: 'POST',
114
- headers: { 'Content-Type': 'application/json' },
115
- body: JSON.stringify({ value: val }),
116
- });
117
- const data = await res.json();
118
-
119
- if (data.valid) {
120
- if (data.token) localStorage.setItem(SESSION_KEY, data.token);
121
- setState('unlocked');
122
- } else {
123
- setError(lockType === 'pin' ? 'Wrong PIN' : 'Wrong password');
124
- setValue('');
125
- triggerShake();
126
- }
127
- } catch {
128
- setError('Connection failed');
129
- } finally {
130
- setSubmitting(false);
131
- }
132
- };
133
-
134
- // ── PIN complete handler (auto-advances) ────────────────────────────
135
- const handlePinComplete = useCallback(
136
- (completed: string) => {
137
- if (state === 'setup-create') {
138
- firstEntry.current = completed;
139
- setValue('');
140
- setError('');
141
- transitionTo('setup-confirm');
142
- } else if (state === 'setup-confirm') {
143
- if (firstEntry.current === completed) {
144
- doSetup(selectedType!, completed);
145
- } else {
146
- setError("PINs don't match. Try again.");
147
- setValue('');
148
- triggerShake();
149
- }
150
- } else if (state === 'locked') {
151
- doVerify(completed);
152
- }
153
- },
154
- [state, selectedType, transitionTo, triggerShake],
155
- );
156
-
157
- // ── Password submit handler ─────────────────────────────────────────
158
- const handlePasswordSubmit = () => {
159
- if (submitting || !value) return;
160
-
161
- if (state === 'setup-create') {
162
- if (value.length < 4) {
163
- setError('At least 4 characters');
164
- triggerShake();
165
- return;
166
- }
167
- firstEntry.current = value;
168
- setValue('');
169
- setError('');
170
- setShowPassword(false);
171
- transitionTo('setup-confirm');
172
- } else if (state === 'setup-confirm') {
173
- if (firstEntry.current === value) {
174
- doSetup(selectedType!, value);
175
- } else {
176
- setError("Passwords don't match. Try again.");
177
- setValue('');
178
- triggerShake();
179
- }
180
- } else if (state === 'locked') {
181
- doVerify(value);
182
- }
183
- };
184
-
185
- // ── Navigation ──────────────────────────────────────────────────────
186
- const goBack = () => {
187
- if (state === 'setup-confirm') {
188
- transitionTo('setup-create');
189
- } else {
190
- transitionTo('setup-choose');
191
- }
192
- setValue('');
193
- setError('');
194
- setShowPassword(false);
195
- };
196
-
197
- const chooseType = (type: LockType) => {
198
- setSelectedType(type);
199
- setValue('');
200
- setError('');
201
- setShowPassword(false);
202
- transitionTo('setup-create');
203
- };
204
-
205
- // ── Render ──────────────────────────────────────────────────────────
206
-
207
- if (state === 'loading') {
208
- return (
209
- <div
210
- className="fixed inset-0 z-[300] flex items-center justify-center"
211
- style={{
212
- backgroundColor: '#0A0A0A',
213
- backgroundImage: 'radial-gradient(circle, #1f1f1f 1.2px, transparent 1.2px)',
214
- backgroundSize: '20px 20px',
215
- }}
216
- >
217
- <Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />
218
- </div>
219
- );
220
- }
221
-
222
- if (state === 'unlocked') return <>{children}</>;
223
-
224
- const isSetup = state === 'setup-choose' || state === 'setup-create' || state === 'setup-confirm';
225
- const currentType = isSetup ? selectedType : lockType;
226
- const isPin = currentType === 'pin';
227
-
228
- return (
229
- <div
230
- className="fixed inset-0 z-[300] flex items-center justify-center overflow-auto"
231
- style={{
232
- backgroundColor: '#0A0A0A',
233
- backgroundImage: 'radial-gradient(circle, #1f1f1f 1.2px, transparent 1.2px)',
234
- backgroundSize: '20px 20px',
235
- }}
236
- >
237
- <div className="w-full max-w-sm px-4 sm:px-5 py-8">
238
- {/* ── Logo + badge ── */}
239
- <div className="flex flex-col items-center mb-8">
240
- <img
241
- src="/bloby.png"
242
- alt=""
243
- className="h-14 mb-4 object-contain"
244
- draggable={false}
245
- />
246
- <div className="flex items-center gap-1.5">
247
- <Shield className="h-3.5 w-3.5 text-muted-foreground" />
248
- <span className="text-[11px] font-medium text-muted-foreground uppercase tracking-widest">
249
- Workspace Lock
250
- </span>
251
- </div>
252
- </div>
253
-
254
- {/* ── Main card ── */}
255
- <div className="animated-border animated-border-slow rounded-2xl p-px">
256
- <div
257
- key={fadeKey}
258
- className="rounded-2xl bg-[#111111] p-5 sm:p-7 animate-[fadeIn_0.2s_ease-out]"
259
- style={{ animationFillMode: 'backwards' }}
260
- >
261
- {/* ════════════════ Choose screen ════════════════ */}
262
- {state === 'setup-choose' && (
263
- <div className="space-y-6">
264
- <div className="text-center">
265
- <h1
266
- className="text-lg font-semibold text-foreground"
267
- style={{ fontFamily: "'Space Grotesk', sans-serif" }}
268
- >
269
- Secure your workspace
270
- </h1>
271
- <p className="text-sm text-muted-foreground mt-1">
272
- Choose your preferred lock method
273
- </p>
274
- </div>
275
-
276
- <div className="grid grid-cols-2 gap-3">
277
- <button
278
- onClick={() => chooseType('pin')}
279
- className="group rounded-xl border-2 border-[#252525] bg-[#161616] p-5 transition-all duration-200 hover:border-white/[0.12] hover:bg-[#1A1A1A] focus:outline-none focus:border-[rgba(175,39,227,0.45)] focus:shadow-[0_0_0_3px_rgba(175,39,227,0.1)]"
280
- >
281
- <div className="flex flex-col items-center gap-3">
282
- <div className="flex h-11 w-11 items-center justify-center rounded-xl bg-white/[0.03] group-hover:bg-white/[0.06] transition-colors">
283
- <KeyRound className="h-5 w-5 text-muted-foreground group-hover:text-foreground transition-colors" />
284
- </div>
285
- <div className="text-center">
286
- <div className="text-sm font-medium text-foreground">PIN Code</div>
287
- <div className="text-[11px] text-muted-foreground mt-0.5">6 digits, quick access</div>
288
- </div>
289
- </div>
290
- </button>
291
-
292
- <button
293
- onClick={() => chooseType('password')}
294
- className="group rounded-xl border-2 border-[#252525] bg-[#161616] p-5 transition-all duration-200 hover:border-white/[0.12] hover:bg-[#1A1A1A] focus:outline-none focus:border-[rgba(175,39,227,0.45)] focus:shadow-[0_0_0_3px_rgba(175,39,227,0.1)]"
295
- >
296
- <div className="flex flex-col items-center gap-3">
297
- <div className="flex h-11 w-11 items-center justify-center rounded-xl bg-white/[0.03] group-hover:bg-white/[0.06] transition-colors">
298
- <Lock className="h-5 w-5 text-muted-foreground group-hover:text-foreground transition-colors" />
299
- </div>
300
- <div className="text-center">
301
- <div className="text-sm font-medium text-foreground">Password</div>
302
- <div className="text-[11px] text-muted-foreground mt-0.5">Custom passphrase</div>
303
- </div>
304
- </div>
305
- </button>
306
- </div>
307
- </div>
308
- )}
309
-
310
- {/* ════════════════ Setup: create / confirm ════════════════ */}
311
- {(state === 'setup-create' || state === 'setup-confirm') && selectedType && (
312
- <div className="space-y-5">
313
- <div className="flex items-center gap-3">
314
- <button
315
- onClick={goBack}
316
- className="flex h-7 w-7 shrink-0 items-center justify-center rounded-lg bg-white/[0.04] hover:bg-white/[0.08] transition-colors"
317
- >
318
- <ArrowLeft className="h-3.5 w-3.5 text-muted-foreground" />
319
- </button>
320
- <div className="min-w-0">
321
- <h1
322
- className="text-base font-semibold text-foreground truncate"
323
- style={{ fontFamily: "'Space Grotesk', sans-serif" }}
324
- >
325
- {state === 'setup-create'
326
- ? isPin ? 'Set your PIN' : 'Set your password'
327
- : isPin ? 'Confirm your PIN' : 'Confirm your password'}
328
- </h1>
329
- <p className="text-xs text-muted-foreground">
330
- {state === 'setup-create'
331
- ? isPin ? 'Choose a 6-digit PIN' : 'Minimum 4 characters'
332
- : 'Enter it once more to confirm'}
333
- </p>
334
- </div>
335
- </div>
336
-
337
- <div className="flex flex-col items-center gap-3">
338
- {isPin ? (
339
- <PinInput
340
- key={state}
341
- value={value}
342
- onChange={(v) => { setValue(v); setError(''); }}
343
- onComplete={handlePinComplete}
344
- error={shaking}
345
- disabled={submitting}
346
- />
347
- ) : (
348
- <>
349
- <PasswordField
350
- key={state}
351
- value={value}
352
- onChange={(v) => { setValue(v); setError(''); }}
353
- onSubmit={handlePasswordSubmit}
354
- placeholder={state === 'setup-confirm' ? 'Re-enter password' : 'Enter password'}
355
- show={showPassword}
356
- onToggleShow={() => setShowPassword(!showPassword)}
357
- error={shaking}
358
- disabled={submitting}
359
- />
360
- <button
361
- onClick={handlePasswordSubmit}
362
- disabled={submitting || value.length < (state === 'setup-create' ? 4 : 1)}
363
- className="w-full h-10 rounded-xl bg-primary text-primary-foreground text-sm font-medium transition-all hover:bg-primary/90 disabled:opacity-25 disabled:cursor-not-allowed flex items-center justify-center gap-2"
364
- >
365
- {submitting ? (
366
- <Loader2 className="h-4 w-4 animate-spin" />
367
- ) : state === 'setup-confirm' ? (
368
- <><Check className="h-4 w-4" />Confirm</>
369
- ) : (
370
- 'Continue'
371
- )}
372
- </button>
373
- </>
374
- )}
375
- {error && <p className="text-xs text-destructive font-medium">{error}</p>}
376
- </div>
377
-
378
- <div className="flex items-center justify-center gap-1.5 pt-1">
379
- <div className="w-1.5 h-1.5 rounded-full bg-foreground" />
380
- <div className={cn('w-1.5 h-1.5 rounded-full transition-colors duration-300', state === 'setup-confirm' ? 'bg-foreground' : 'bg-white/15')} />
381
- </div>
382
- </div>
383
- )}
384
-
385
- {/* ════════════════ Locked ════════════════ */}
386
- {state === 'locked' && lockType && (
387
- <div className="space-y-6">
388
- <div className="text-center">
389
- <h1
390
- className="text-lg font-semibold text-foreground"
391
- style={{ fontFamily: "'Space Grotesk', sans-serif" }}
392
- >
393
- Welcome back
394
- </h1>
395
- <p className="text-sm text-muted-foreground mt-1">
396
- {lockType === 'pin' ? 'Enter your PIN to continue' : 'Enter your password to continue'}
397
- </p>
398
- </div>
399
-
400
- <div className="flex flex-col items-center gap-3">
401
- {lockType === 'pin' ? (
402
- <PinInput
403
- value={value}
404
- onChange={(v) => { setValue(v); setError(''); }}
405
- onComplete={handlePinComplete}
406
- error={shaking}
407
- disabled={submitting}
408
- />
409
- ) : (
410
- <>
411
- <PasswordField
412
- value={value}
413
- onChange={(v) => { setValue(v); setError(''); }}
414
- onSubmit={handlePasswordSubmit}
415
- placeholder="Enter password"
416
- show={showPassword}
417
- onToggleShow={() => setShowPassword(!showPassword)}
418
- error={shaking}
419
- disabled={submitting}
420
- />
421
- <button
422
- onClick={handlePasswordSubmit}
423
- disabled={submitting || !value}
424
- className="w-full h-10 rounded-xl bg-primary text-primary-foreground text-sm font-medium transition-all hover:bg-primary/90 disabled:opacity-25 disabled:cursor-not-allowed flex items-center justify-center gap-2"
425
- >
426
- {submitting ? <Loader2 className="h-4 w-4 animate-spin" /> : 'Unlock'}
427
- </button>
428
- </>
429
- )}
430
- {error && <p className="text-xs text-destructive font-medium">{error}</p>}
431
- </div>
432
- </div>
433
- )}
434
- </div>
435
- </div>
436
-
437
- <p className="text-center text-[11px] text-muted-foreground/40 mt-6">
438
- Ask your agent to reset if you forget your{' '}
439
- {lockType === 'pin' || selectedType === 'pin' ? 'PIN' : 'password'}
440
- </p>
441
- </div>
442
- </div>
443
- );
444
- }
445
-
446
- // ── Password field sub-component ──────────────────────────────────────
447
-
448
- function PasswordField({
449
- value, onChange, onSubmit, placeholder, show, onToggleShow, error, disabled,
450
- }: {
451
- value: string; onChange: (v: string) => void; onSubmit: () => void;
452
- placeholder: string; show: boolean; onToggleShow: () => void;
453
- error: boolean; disabled: boolean;
454
- }) {
455
- return (
456
- <div className={cn('relative w-full', error && 'animate-shake')}>
457
- <input
458
- type={show ? 'text' : 'password'}
459
- value={value}
460
- onChange={(e) => onChange(e.target.value)}
461
- onKeyDown={(e) => e.key === 'Enter' && onSubmit()}
462
- placeholder={placeholder}
463
- disabled={disabled}
464
- autoFocus
465
- autoComplete="off"
466
- className={cn(
467
- 'w-full h-12 rounded-xl border-2 bg-[#161616] px-4 pr-11 text-sm text-foreground placeholder:text-muted-foreground/50 outline-none transition-all duration-200',
468
- 'border-[#252525] focus:border-[rgba(175,39,227,0.45)] focus:shadow-[0_0_0_3px_rgba(175,39,227,0.1),0_0_24px_-6px_rgba(175,39,227,0.18)]',
469
- error && 'border-destructive/40',
470
- disabled && 'opacity-40',
471
- )}
472
- style={{ fontSize: '16px' }}
473
- />
474
- <button
475
- type="button"
476
- onClick={onToggleShow}
477
- className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground/50 hover:text-foreground transition-colors p-1"
478
- tabIndex={-1}
479
- >
480
- {show ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
481
- </button>
482
- </div>
483
- );
484
- }