bloby-bot 0.37.1 → 0.39.1

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 (30) hide show
  1. package/package.json +1 -1
  2. package/supervisor/harnesses/codex.ts +34 -0
  3. package/worker/prompts/bloby-system-prompt.txt +2 -2
  4. package/workspace/client/index.html +3 -0
  5. package/workspace/client/public/.well-known/assetlinks.json +8 -0
  6. package/workspace/client/public/bloby-cyberpunk.png +0 -0
  7. package/workspace/client/public/brand/blackrock.svg +8 -0
  8. package/workspace/client/public/kid-breakfast.png +0 -0
  9. package/workspace/client/public/wallpapers/bg.jpg +0 -0
  10. package/workspace/client/public/wallpapers/crypto_bg.png +0 -0
  11. package/workspace/client/public/wallpapers/wp-dusk.jpg +0 -0
  12. package/workspace/client/public/wallpapers/wp-mountain.jpg +0 -0
  13. package/workspace/client/public/wallpapers/wp-ocean.jpg +0 -0
  14. package/workspace/client/src/App.tsx +20 -9
  15. package/workspace/client/src/components/Dashboard/AiChatPage.tsx +145 -0
  16. package/workspace/client/src/components/Dashboard/CryptoPage.tsx +470 -0
  17. package/workspace/client/src/components/Dashboard/DashboardPage.tsx +117 -113
  18. package/workspace/client/src/components/Dashboard/WishlistPage.tsx +464 -0
  19. package/workspace/client/src/components/Layout/DashboardLayout.tsx +32 -34
  20. package/workspace/client/src/components/Layout/MiniSidebar.tsx +64 -0
  21. package/workspace/client/src/components/Layout/MobileNav.tsx +103 -6
  22. package/workspace/client/src/components/Layout/Sidebar.tsx +11 -10
  23. package/workspace/client/src/components/Lock/PinInput.tsx +107 -0
  24. package/workspace/client/src/components/Lock/WorkspaceLock.tsx +484 -0
  25. package/workspace/client/src/components/StickyNotes/StickyNotesOverlay.tsx +396 -0
  26. package/workspace/client/src/components/StickyNotes/StickyNotesSettingsPage.tsx +427 -0
  27. package/workspace/client/src/components/Wallpaper/WallpaperBackground.tsx +12 -0
  28. package/workspace/client/src/components/Wallpaper/WallpaperContext.tsx +160 -0
  29. package/workspace/client/src/components/Wallpaper/WallpaperPicker.tsx +67 -0
  30. package/workspace/client/src/styles/globals.css +89 -4
@@ -0,0 +1,396 @@
1
+ import { useState, useEffect, useCallback, useRef } from 'react';
2
+ import { X, Plus, StickyNote } from 'lucide-react';
3
+
4
+ // Adjust this if your workspace proxies API calls differently.
5
+ // Default Bloby workspaces: '/app/api'. Direct backend: '/api'.
6
+ const API_BASE = '/app/api';
7
+
8
+ interface StickyNote {
9
+ id: number;
10
+ content: string;
11
+ color: string;
12
+ x: number;
13
+ y: number;
14
+ width: number;
15
+ height: number;
16
+ visible: number;
17
+ created_at: string;
18
+ }
19
+
20
+ const PASTEL_COLORS = ['#E6C97A', '#D4A0B0', '#8BBD9F', '#8BAFC4', '#B0A0C8', '#D4AD8A'];
21
+
22
+ function randomRotation(id: number): number {
23
+ const seed = ((id * 9301 + 49297) % 233280) / 233280;
24
+ return (seed - 0.5) * 6; // -3 to +3 degrees
25
+ }
26
+
27
+ export default function StickyNotesOverlay() {
28
+ const [notes, setNotes] = useState<StickyNote[]>([]);
29
+ const [allVisible, setAllVisible] = useState(true);
30
+ const [focusedId, setFocusedId] = useState<number | null>(null);
31
+ const containerRef = useRef<HTMLDivElement>(null);
32
+
33
+ const fetchNotes = useCallback(async () => {
34
+ try {
35
+ const res = await fetch(`${API_BASE}/sticky-notes`);
36
+ if (!res.ok) return;
37
+ const data: StickyNote[] = await res.json();
38
+ setNotes(data);
39
+ setAllVisible(data.length === 0 || data.some((n) => n.visible === 1));
40
+ } catch {
41
+ // silent
42
+ }
43
+ }, []);
44
+
45
+ useEffect(() => {
46
+ fetchNotes();
47
+ }, [fetchNotes]);
48
+
49
+ const createNote = useCallback(async () => {
50
+ // If notes are hidden, turn visibility on first
51
+ if (!allVisible) {
52
+ try {
53
+ await fetch(`${API_BASE}/sticky-notes/toggle-visibility`, { method: 'POST' });
54
+ } catch { /* silent */ }
55
+ }
56
+ const color = PASTEL_COLORS[Math.floor(Math.random() * PASTEL_COLORS.length)];
57
+ const x = 60 + Math.floor(Math.random() * 300);
58
+ const y = 60 + Math.floor(Math.random() * 200);
59
+ try {
60
+ const res = await fetch(`${API_BASE}/sticky-notes`, {
61
+ method: 'POST',
62
+ headers: { 'Content-Type': 'application/json' },
63
+ body: JSON.stringify({ content: '', color, x, y, width: 200, height: 180, visible: 1 }),
64
+ });
65
+ if (!res.ok) return;
66
+ const created = await res.json();
67
+ setAllVisible(true);
68
+ setFocusedId(created.id);
69
+ fetchNotes();
70
+ } catch {
71
+ // silent
72
+ }
73
+ }, [fetchNotes, allVisible]);
74
+
75
+ const toggleVisibility = useCallback(async () => {
76
+ try {
77
+ const res = await fetch(`${API_BASE}/sticky-notes/toggle-visibility`, { method: 'POST' });
78
+ if (!res.ok) return;
79
+ const data = await res.json();
80
+ setNotes(data.notes);
81
+ setAllVisible(data.visible === 1);
82
+ } catch {
83
+ // silent
84
+ }
85
+ }, []);
86
+
87
+ const deleteNote = useCallback(async (id: number) => {
88
+ try {
89
+ await fetch(`${API_BASE}/sticky-notes/${id}`, { method: 'DELETE' });
90
+ setNotes((prev) => prev.filter((n) => n.id !== id));
91
+ } catch {
92
+ // silent
93
+ }
94
+ }, []);
95
+
96
+ const updateNote = useCallback(async (id: number, updates: Partial<StickyNote>) => {
97
+ try {
98
+ await fetch(`${API_BASE}/sticky-notes/${id}`, {
99
+ method: 'PUT',
100
+ headers: { 'Content-Type': 'application/json' },
101
+ body: JSON.stringify(updates),
102
+ });
103
+ } catch {
104
+ // silent
105
+ }
106
+ }, []);
107
+
108
+ const visibleNotes = notes.filter((n) => n.visible === 1);
109
+
110
+ return (
111
+ <div ref={containerRef} className="absolute inset-0 pointer-events-none hidden md:block" style={{ zIndex: 30 }}>
112
+ {visibleNotes.map((note) => (
113
+ <DraggableNote
114
+ key={note.id}
115
+ note={note}
116
+ containerRef={containerRef}
117
+ onDelete={deleteNote}
118
+ onUpdate={updateNote}
119
+ isFocused={focusedId === note.id}
120
+ onFocus={() => setFocusedId(note.id)}
121
+ />
122
+ ))}
123
+ <FAB onCreate={createNote} onToggle={toggleVisibility} allVisible={allVisible} noteCount={notes.length} />
124
+ </div>
125
+ );
126
+ }
127
+
128
+ function DraggableNote({
129
+ note,
130
+ containerRef,
131
+ onDelete,
132
+ onUpdate,
133
+ isFocused,
134
+ onFocus,
135
+ }: {
136
+ note: StickyNote;
137
+ containerRef: React.RefObject<HTMLDivElement | null>;
138
+ onDelete: (id: number) => void;
139
+ onUpdate: (id: number, updates: Partial<StickyNote>) => void;
140
+ isFocused: boolean;
141
+ onFocus: () => void;
142
+ }) {
143
+ const [pos, setPos] = useState({ x: note.x, y: note.y });
144
+ const [dragging, setDragging] = useState(false);
145
+ const dragOffset = useRef({ x: 0, y: 0 });
146
+ const noteRef = useRef<HTMLDivElement>(null);
147
+ const rotation = randomRotation(note.id);
148
+
149
+ useEffect(() => {
150
+ setPos({ x: note.x, y: note.y });
151
+ }, [note.x, note.y]);
152
+
153
+ const onMouseDown = useCallback((e: React.MouseEvent) => {
154
+ onFocus();
155
+ if ((e.target as HTMLElement).closest('textarea') || (e.target as HTMLElement).closest('button')) return;
156
+ e.preventDefault();
157
+ const rect = noteRef.current?.getBoundingClientRect();
158
+ if (!rect) return;
159
+ dragOffset.current = { x: e.clientX - rect.left, y: e.clientY - rect.top };
160
+ setDragging(true);
161
+ }, [onFocus]);
162
+
163
+ useEffect(() => {
164
+ if (!dragging) return;
165
+
166
+ const onMove = (e: MouseEvent) => {
167
+ const container = containerRef.current;
168
+ if (!container) return;
169
+ const cr = container.getBoundingClientRect();
170
+ const newX = Math.max(0, Math.min(e.clientX - cr.left - dragOffset.current.x, cr.width - note.width));
171
+ const newY = Math.max(0, Math.min(e.clientY - cr.top - dragOffset.current.y, cr.height - note.height));
172
+ setPos({ x: newX, y: newY });
173
+ };
174
+
175
+ const onUp = () => {
176
+ setDragging(false);
177
+ setPos((current) => {
178
+ onUpdate(note.id, { x: Math.round(current.x), y: Math.round(current.y) });
179
+ return current;
180
+ });
181
+ };
182
+
183
+ window.addEventListener('mousemove', onMove);
184
+ window.addEventListener('mouseup', onUp);
185
+ return () => {
186
+ window.removeEventListener('mousemove', onMove);
187
+ window.removeEventListener('mouseup', onUp);
188
+ };
189
+ }, [dragging, containerRef, note.id, note.width, note.height, onUpdate]);
190
+
191
+ // Touch support
192
+ const onTouchStart = useCallback((e: React.TouchEvent) => {
193
+ onFocus();
194
+ if ((e.target as HTMLElement).closest('textarea') || (e.target as HTMLElement).closest('button')) return;
195
+ const touch = e.touches[0];
196
+ const rect = noteRef.current?.getBoundingClientRect();
197
+ if (!rect) return;
198
+ dragOffset.current = { x: touch.clientX - rect.left, y: touch.clientY - rect.top };
199
+ setDragging(true);
200
+ }, [onFocus]);
201
+
202
+ useEffect(() => {
203
+ if (!dragging) return;
204
+
205
+ const onTouchMove = (e: TouchEvent) => {
206
+ const container = containerRef.current;
207
+ if (!container) return;
208
+ const touch = e.touches[0];
209
+ const cr = container.getBoundingClientRect();
210
+ const newX = Math.max(0, Math.min(touch.clientX - cr.left - dragOffset.current.x, cr.width - note.width));
211
+ const newY = Math.max(0, Math.min(touch.clientY - cr.top - dragOffset.current.y, cr.height - note.height));
212
+ setPos({ x: newX, y: newY });
213
+ };
214
+
215
+ const onTouchEnd = () => {
216
+ setDragging(false);
217
+ setPos((current) => {
218
+ onUpdate(note.id, { x: Math.round(current.x), y: Math.round(current.y) });
219
+ return current;
220
+ });
221
+ };
222
+
223
+ window.addEventListener('touchmove', onTouchMove, { passive: false });
224
+ window.addEventListener('touchend', onTouchEnd);
225
+ return () => {
226
+ window.removeEventListener('touchmove', onTouchMove);
227
+ window.removeEventListener('touchend', onTouchEnd);
228
+ };
229
+ }, [dragging, containerRef, note.id, note.width, note.height, onUpdate]);
230
+
231
+ const handleBlur = useCallback((e: React.FocusEvent<HTMLTextAreaElement>) => {
232
+ const newContent = e.target.value;
233
+ if (newContent !== note.content) {
234
+ onUpdate(note.id, { content: newContent });
235
+ }
236
+ }, [note.id, note.content, onUpdate]);
237
+
238
+ // Darken the pastel for text readability
239
+ const textColor = '#1a1a1a';
240
+
241
+ return (
242
+ <div
243
+ ref={noteRef}
244
+ className="absolute pointer-events-auto group"
245
+ style={{
246
+ left: pos.x,
247
+ top: pos.y,
248
+ width: note.width,
249
+ height: note.height,
250
+ zIndex: dragging ? 50 : isFocused ? 45 : 35,
251
+ transform: `rotate(${rotation}deg)`,
252
+ transition: dragging ? 'none' : 'box-shadow 0.2s',
253
+ }}
254
+ onMouseDown={onMouseDown}
255
+ onTouchStart={onTouchStart}
256
+ >
257
+ <div
258
+ className="w-full h-full rounded-lg shadow-lg flex flex-col overflow-hidden"
259
+ style={{
260
+ backgroundColor: note.color,
261
+ boxShadow: dragging
262
+ ? '0 12px 28px rgba(0,0,0,0.4), 0 4px 8px rgba(0,0,0,0.2)'
263
+ : '0 4px 12px rgba(0,0,0,0.25), 0 1px 3px rgba(0,0,0,0.15)',
264
+ cursor: dragging ? 'grabbing' : 'grab',
265
+ }}
266
+ >
267
+ {/* Top bar with drag handle and delete */}
268
+ <div className="flex items-center justify-between px-2.5 pt-2 pb-1" style={{ color: textColor }}>
269
+ <div className="flex gap-0.5 opacity-40">
270
+ <span className="block w-1 h-1 rounded-full bg-current" />
271
+ <span className="block w-1 h-1 rounded-full bg-current" />
272
+ <span className="block w-1 h-1 rounded-full bg-current" />
273
+ </div>
274
+ <button
275
+ onClick={() => onDelete(note.id)}
276
+ className="opacity-0 group-hover:opacity-70 hover:!opacity-100 p-0.5 rounded transition-opacity"
277
+ style={{ color: textColor }}
278
+ >
279
+ <X className="h-3.5 w-3.5" />
280
+ </button>
281
+ </div>
282
+
283
+ {/* Content area */}
284
+ <textarea
285
+ defaultValue={note.content}
286
+ onBlur={handleBlur}
287
+ placeholder="Write something..."
288
+ className="flex-1 w-full px-2.5 pb-2.5 text-xs leading-relaxed resize-none bg-transparent border-none outline-none placeholder:opacity-40"
289
+ style={{ color: textColor, fontFamily: "'Caveat', 'Segoe Print', 'Comic Sans MS', cursive" }}
290
+ />
291
+ </div>
292
+ </div>
293
+ );
294
+ }
295
+
296
+ function FAB({
297
+ onCreate,
298
+ onToggle,
299
+ allVisible,
300
+ noteCount,
301
+ }: {
302
+ onCreate: () => void;
303
+ onToggle: () => void;
304
+ allVisible: boolean;
305
+ noteCount: number;
306
+ }) {
307
+ const [pressing, setPressing] = useState(false);
308
+ const [longPressed, setLongPressed] = useState(false);
309
+ const [hovered, setHovered] = useState(false);
310
+ const pressTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
311
+ const didLongPress = useRef(false);
312
+
313
+ const startPress = useCallback(() => {
314
+ didLongPress.current = false;
315
+ setPressing(true);
316
+ pressTimer.current = setTimeout(() => {
317
+ didLongPress.current = true;
318
+ setLongPressed(true);
319
+ onToggle();
320
+ setPressing(false);
321
+ setTimeout(() => setLongPressed(false), 600);
322
+ }, 500);
323
+ }, [onToggle]);
324
+
325
+ const endPress = useCallback(() => {
326
+ if (pressTimer.current) {
327
+ clearTimeout(pressTimer.current);
328
+ pressTimer.current = null;
329
+ }
330
+ if (!didLongPress.current) {
331
+ onCreate();
332
+ }
333
+ setPressing(false);
334
+ }, [onCreate]);
335
+
336
+ const cancelPress = useCallback(() => {
337
+ if (pressTimer.current) {
338
+ clearTimeout(pressTimer.current);
339
+ pressTimer.current = null;
340
+ }
341
+ setPressing(false);
342
+ }, []);
343
+
344
+ return (
345
+ <div className="absolute bottom-5 left-5 pointer-events-auto flex flex-col items-center gap-2">
346
+ {/* Tooltip on hover */}
347
+ <div
348
+ className="text-[9px] text-muted-foreground/60 bg-[#1a1a1a]/90 px-2.5 py-1 rounded-lg backdrop-blur-sm select-none transition-all duration-200 whitespace-nowrap"
349
+ style={{
350
+ opacity: hovered ? 1 : 0,
351
+ transform: hovered ? 'translateY(0)' : 'translateY(4px)',
352
+ pointerEvents: 'none',
353
+ }}
354
+ >
355
+ hold to toggle visibility
356
+ </div>
357
+ {noteCount > 0 && !hovered && (
358
+ <div className="text-[9px] text-muted-foreground/50 bg-[#1a1a1a]/80 px-2 py-0.5 rounded-full backdrop-blur-sm select-none">
359
+ {allVisible ? `${noteCount} note${noteCount !== 1 ? 's' : ''}` : 'hidden'}
360
+ </div>
361
+ )}
362
+ <div className="relative">
363
+ <button
364
+ onMouseDown={startPress}
365
+ onMouseUp={endPress}
366
+ onMouseLeave={() => { cancelPress(); setHovered(false); }}
367
+ onMouseEnter={() => setHovered(true)}
368
+ onTouchStart={startPress}
369
+ onTouchEnd={(e) => { e.preventDefault(); endPress(); }}
370
+ onTouchCancel={cancelPress}
371
+ className="h-11 w-11 rounded-xl flex items-center justify-center shadow-lg select-none"
372
+ style={{
373
+ background: 'linear-gradient(135deg, #E6C97A, #D4A0B0)',
374
+ transform: pressing ? 'scale(0.9)' : longPressed ? 'scale(1.15)' : 'scale(1)',
375
+ transition: 'transform 0.2s ease, box-shadow 0.2s ease',
376
+ boxShadow: longPressed
377
+ ? '0 0 20px rgba(230, 201, 122, 0.4), 0 4px 16px rgba(0,0,0,0.3)'
378
+ : '0 4px 12px rgba(0,0,0,0.3)',
379
+ }}
380
+ >
381
+ <StickyNote className="h-5 w-5 text-white/90" />
382
+ </button>
383
+ {/* + badge on hover */}
384
+ <div
385
+ className="absolute -top-1.5 -right-1.5 h-4.5 w-4.5 rounded-full bg-white flex items-center justify-center shadow-md transition-all duration-200"
386
+ style={{
387
+ opacity: hovered && !pressing && !longPressed ? 1 : 0,
388
+ transform: hovered && !pressing && !longPressed ? 'scale(1)' : 'scale(0.5)',
389
+ }}
390
+ >
391
+ <Plus className="h-3 w-3 text-[#1a1a1a]" strokeWidth={3} />
392
+ </div>
393
+ </div>
394
+ </div>
395
+ );
396
+ }