bloby-bot 0.39.1 → 0.41.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/package.json +1 -1
- package/supervisor/channels/manager.ts +31 -0
- package/supervisor/channels/whatsapp.ts +43 -22
- package/workspace/client/index.html +0 -3
- package/workspace/client/src/App.tsx +9 -20
- package/workspace/client/src/components/Dashboard/DashboardPage.tsx +113 -117
- package/workspace/client/src/components/Layout/DashboardLayout.tsx +34 -32
- package/workspace/client/src/components/Layout/MobileNav.tsx +6 -103
- package/workspace/client/src/components/Layout/Sidebar.tsx +10 -11
- package/workspace/client/src/styles/globals.css +4 -89
- package/workspace/client/public/.well-known/assetlinks.json +0 -8
- package/workspace/client/public/bloby-cyberpunk.png +0 -0
- package/workspace/client/public/brand/blackrock.svg +0 -8
- package/workspace/client/public/kid-breakfast.png +0 -0
- package/workspace/client/public/wallpapers/bg.jpg +0 -0
- package/workspace/client/public/wallpapers/crypto_bg.png +0 -0
- package/workspace/client/public/wallpapers/wp-dusk.jpg +0 -0
- package/workspace/client/public/wallpapers/wp-mountain.jpg +0 -0
- package/workspace/client/public/wallpapers/wp-ocean.jpg +0 -0
- package/workspace/client/src/components/Dashboard/AiChatPage.tsx +0 -145
- package/workspace/client/src/components/Dashboard/CryptoPage.tsx +0 -470
- package/workspace/client/src/components/Dashboard/WishlistPage.tsx +0 -464
- package/workspace/client/src/components/Layout/MiniSidebar.tsx +0 -64
- package/workspace/client/src/components/Lock/PinInput.tsx +0 -107
- package/workspace/client/src/components/Lock/WorkspaceLock.tsx +0 -484
- package/workspace/client/src/components/StickyNotes/StickyNotesOverlay.tsx +0 -396
- package/workspace/client/src/components/StickyNotes/StickyNotesSettingsPage.tsx +0 -427
- package/workspace/client/src/components/Wallpaper/WallpaperBackground.tsx +0 -12
- package/workspace/client/src/components/Wallpaper/WallpaperContext.tsx +0 -160
- package/workspace/client/src/components/Wallpaper/WallpaperPicker.tsx +0 -67
|
@@ -1,127 +1,30 @@
|
|
|
1
1
|
import { useState } from 'react';
|
|
2
|
-
import {
|
|
3
|
-
import { Menu, LayoutDashboard, StickyNote, MessageCircle, Heart, Settings, Bitcoin, X } from 'lucide-react';
|
|
2
|
+
import { Menu } from 'lucide-react';
|
|
4
3
|
import {
|
|
5
4
|
Sheet,
|
|
6
5
|
SheetContent,
|
|
7
6
|
SheetTitle,
|
|
8
7
|
} from '@/components/ui/sheet';
|
|
9
|
-
import
|
|
10
|
-
import { cn } from '@/lib/utils';
|
|
8
|
+
import Sidebar from './Sidebar';
|
|
11
9
|
|
|
12
|
-
|
|
13
|
-
userName?: string;
|
|
14
|
-
botName?: string;
|
|
15
|
-
backendStatus?: 'healthy' | 'restarting';
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export default function MobileNav({ userName, botName = 'Bloby', backendStatus = 'healthy' }: Props) {
|
|
10
|
+
export default function MobileNav({ userName, botName, backendStatus }: { userName?: string; botName?: string; backendStatus?: 'healthy' | 'restarting' }) {
|
|
19
11
|
const [open, setOpen] = useState(false);
|
|
20
|
-
const [pickerOpen, setPickerOpen] = useState(false);
|
|
21
|
-
const firstName = userName?.split(/\s+/)[0] || '';
|
|
22
|
-
|
|
23
|
-
const close = () => setOpen(false);
|
|
24
12
|
|
|
25
13
|
return (
|
|
26
14
|
<>
|
|
27
15
|
<button
|
|
28
16
|
onClick={() => setOpen(true)}
|
|
29
|
-
className="flex items-center justify-center h-10 w-10 rounded-lg text-
|
|
17
|
+
className="flex items-center justify-center h-10 w-10 rounded-lg text-muted-foreground hover:text-foreground transition-colors md:hidden"
|
|
30
18
|
>
|
|
31
19
|
<Menu className="h-5 w-5" />
|
|
32
20
|
<span className="sr-only">Open navigation</span>
|
|
33
21
|
</button>
|
|
34
22
|
<Sheet open={open} onOpenChange={setOpen}>
|
|
35
|
-
<SheetContent side="left" className="p-0 w-
|
|
23
|
+
<SheetContent side="left" className="p-0 w-64" showCloseButton={false}>
|
|
36
24
|
<SheetTitle className="sr-only">Navigation</SheetTitle>
|
|
37
|
-
<
|
|
38
|
-
<div className="glass-card h-full p-5 flex flex-col" style={{ borderRadius: 24 }}>
|
|
39
|
-
<div className="flex items-center justify-between mb-6">
|
|
40
|
-
<div>
|
|
41
|
-
<p className="text-xs text-white/60">Workspace</p>
|
|
42
|
-
<p className="text-lg font-semibold tracking-tight">{botName}</p>
|
|
43
|
-
</div>
|
|
44
|
-
<button
|
|
45
|
-
type="button"
|
|
46
|
-
onClick={close}
|
|
47
|
-
className="h-9 w-9 rounded-full glass-pill flex items-center justify-center text-white/80"
|
|
48
|
-
aria-label="Close navigation"
|
|
49
|
-
>
|
|
50
|
-
<X className="h-4 w-4" />
|
|
51
|
-
</button>
|
|
52
|
-
</div>
|
|
53
|
-
|
|
54
|
-
{firstName && (
|
|
55
|
-
<p className="text-sm text-white/70 mb-4">Hi, {firstName}.</p>
|
|
56
|
-
)}
|
|
57
|
-
|
|
58
|
-
<nav className="flex-1 space-y-1">
|
|
59
|
-
<Item to="/" icon={LayoutDashboard} label="Dashboard" onNavigate={close} />
|
|
60
|
-
<Item to="/sticky-notes" icon={StickyNote} label="Sticky Notes" onNavigate={close} />
|
|
61
|
-
<Item to="/aichat" icon={MessageCircle} label="AI Chat" onNavigate={close} />
|
|
62
|
-
<Item to="/wishlist" icon={Heart} label="Wishlist" onNavigate={close} />
|
|
63
|
-
<Item to="/crypto" icon={Bitcoin} label="Crypto" onNavigate={close} />
|
|
64
|
-
|
|
65
|
-
<button
|
|
66
|
-
type="button"
|
|
67
|
-
onClick={() => {
|
|
68
|
-
setPickerOpen(true);
|
|
69
|
-
close();
|
|
70
|
-
}}
|
|
71
|
-
className="w-full mt-2 flex items-center gap-3 px-3 py-2.5 rounded-xl text-sm text-white/80 hover:text-white hover:bg-white/10 transition"
|
|
72
|
-
>
|
|
73
|
-
<Settings className="h-[18px] w-[18px]" />
|
|
74
|
-
<span>Wallpaper</span>
|
|
75
|
-
</button>
|
|
76
|
-
</nav>
|
|
77
|
-
|
|
78
|
-
<div className="rounded-xl glass-pill px-3.5 py-2.5 flex items-center gap-2 mt-4">
|
|
79
|
-
<span
|
|
80
|
-
className={`h-2 w-2 rounded-full shrink-0 ${
|
|
81
|
-
backendStatus === 'healthy' ? 'bg-emerald-400' : 'bg-orange-400 animate-pulse'
|
|
82
|
-
}`}
|
|
83
|
-
/>
|
|
84
|
-
<span className="text-xs text-white/70">
|
|
85
|
-
Workspace: {backendStatus === 'healthy' ? 'Live' : 'Restarting'}
|
|
86
|
-
</span>
|
|
87
|
-
</div>
|
|
88
|
-
</div>
|
|
89
|
-
</div>
|
|
25
|
+
<Sidebar userName={userName} botName={botName} backendStatus={backendStatus} onNavigate={() => setOpen(false)} />
|
|
90
26
|
</SheetContent>
|
|
91
27
|
</Sheet>
|
|
92
|
-
|
|
93
|
-
<WallpaperPicker open={pickerOpen} onClose={() => setPickerOpen(false)} />
|
|
94
28
|
</>
|
|
95
29
|
);
|
|
96
30
|
}
|
|
97
|
-
|
|
98
|
-
function Item({
|
|
99
|
-
to,
|
|
100
|
-
icon: Icon,
|
|
101
|
-
label,
|
|
102
|
-
onNavigate,
|
|
103
|
-
}: {
|
|
104
|
-
to: string;
|
|
105
|
-
icon: React.ComponentType<{ className?: string }>;
|
|
106
|
-
label: string;
|
|
107
|
-
onNavigate: () => void;
|
|
108
|
-
}) {
|
|
109
|
-
return (
|
|
110
|
-
<NavLink
|
|
111
|
-
to={to}
|
|
112
|
-
end
|
|
113
|
-
onClick={onNavigate}
|
|
114
|
-
className={({ isActive }) =>
|
|
115
|
-
cn(
|
|
116
|
-
'flex items-center gap-3 px-3 py-2.5 rounded-xl text-sm transition',
|
|
117
|
-
isActive
|
|
118
|
-
? 'bg-white/15 text-white font-medium'
|
|
119
|
-
: 'text-white/70 hover:text-white hover:bg-white/10',
|
|
120
|
-
)
|
|
121
|
-
}
|
|
122
|
-
>
|
|
123
|
-
<Icon className="h-[18px] w-[18px]" />
|
|
124
|
-
<span>{label}</span>
|
|
125
|
-
</NavLink>
|
|
126
|
-
);
|
|
127
|
-
}
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { NavLink } from 'react-router';
|
|
2
2
|
import {
|
|
3
3
|
LayoutDashboard,
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
Bitcoin,
|
|
4
|
+
AppWindow,
|
|
5
|
+
Search,
|
|
6
|
+
CircleHelp,
|
|
8
7
|
} from 'lucide-react';
|
|
9
8
|
import { cn } from '@/lib/utils';
|
|
10
9
|
|
|
@@ -27,8 +26,9 @@ export default function Sidebar({ userName, botName = 'Bloby', backendStatus = '
|
|
|
27
26
|
return (
|
|
28
27
|
<aside className="flex flex-col h-full w-64 bg-transparent p-5 pt-8">
|
|
29
28
|
{/* Logo */}
|
|
30
|
-
<div className="flex items-center mb-8">
|
|
31
|
-
<img src="/
|
|
29
|
+
<div className="flex items-center gap-2.5 mb-8">
|
|
30
|
+
<img src="/bloby.png" alt={botName} className="h-7 w-auto" />
|
|
31
|
+
<span className="font-bold text-lg" style={{ fontFamily: "'Space Grotesk', sans-serif" }}>{botName}</span>
|
|
32
32
|
</div>
|
|
33
33
|
|
|
34
34
|
{/* Greeting */}
|
|
@@ -40,7 +40,7 @@ export default function Sidebar({ userName, botName = 'Bloby', backendStatus = '
|
|
|
40
40
|
<h2
|
|
41
41
|
className="text-4xl font-bold mt-0.5 tracking-tight leading-[1.08] w-fit"
|
|
42
42
|
style={{
|
|
43
|
-
backgroundImage: 'linear-gradient(to right, #
|
|
43
|
+
backgroundImage: 'linear-gradient(to right, #4FF2FE, #BC20DE, #FE546B)',
|
|
44
44
|
WebkitBackgroundClip: 'text',
|
|
45
45
|
WebkitTextFillColor: 'transparent',
|
|
46
46
|
backgroundClip: 'text',
|
|
@@ -54,10 +54,9 @@ export default function Sidebar({ userName, botName = 'Bloby', backendStatus = '
|
|
|
54
54
|
{/* Navigation */}
|
|
55
55
|
<nav className="flex-1 space-y-0.5">
|
|
56
56
|
<NavItem to="/" icon={LayoutDashboard} label="Dashboard" onNavigate={onNavigate} />
|
|
57
|
-
<NavItem to="/
|
|
58
|
-
<NavItem to="/
|
|
59
|
-
<NavItem to="/
|
|
60
|
-
<NavItem to="/crypto" icon={Bitcoin} label="Crypto" onNavigate={onNavigate} />
|
|
57
|
+
<NavItem to="/app1" icon={AppWindow} label="App 1" sub="Ask Bloby to create your first app and remove this placeholder" onNavigate={onNavigate} />
|
|
58
|
+
<NavItem to="/research" icon={Search} label="Research" sub="Ask Bloby to research anything for you, maybe even daily" onNavigate={onNavigate} />
|
|
59
|
+
<NavItem to="/whatelse" icon={CircleHelp} label="What Else?" sub="The sky is the limit, just ask and Bloby will do it!" onNavigate={onNavigate} />
|
|
61
60
|
</nav>
|
|
62
61
|
|
|
63
62
|
{/* Backend status */}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
@custom-variant dark (&:is(.dark *));
|
|
4
4
|
|
|
5
5
|
@theme inline {
|
|
6
|
-
--font-sans:
|
|
6
|
+
--font-sans: system-ui, -apple-system, sans-serif;
|
|
7
7
|
--color-background: #0A0A0A;
|
|
8
8
|
--color-foreground: #f5f5f5;
|
|
9
9
|
--color-card: #2a2a2a;
|
|
@@ -44,76 +44,16 @@ html {
|
|
|
44
44
|
-ms-touch-action: manipulation;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
html {
|
|
48
|
-
background-color: #000;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
47
|
body {
|
|
52
|
-
background-color:
|
|
53
|
-
|
|
48
|
+
background-color: var(--color-background);
|
|
49
|
+
background-image: radial-gradient(circle, #1f1f1f 1.2px, transparent 1.2px);
|
|
50
|
+
background-size: 20px 20px;
|
|
54
51
|
color: var(--color-foreground);
|
|
55
|
-
font-family: 'Inter', 'Inter Display', system-ui, -apple-system, sans-serif;
|
|
56
|
-
font-feature-settings: 'cv02', 'cv03', 'cv04', 'cv11';
|
|
57
52
|
-webkit-font-smoothing: antialiased;
|
|
58
53
|
-moz-osx-font-smoothing: grayscale;
|
|
59
54
|
overscroll-behavior: none;
|
|
60
55
|
}
|
|
61
56
|
|
|
62
|
-
h1, h2, h3, h4, h5, h6 {
|
|
63
|
-
letter-spacing: -0.02em;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/* ── Glass utilities ── */
|
|
67
|
-
.glass-card {
|
|
68
|
-
position: relative;
|
|
69
|
-
background: rgba(20, 20, 25, 0.35);
|
|
70
|
-
backdrop-filter: blur(24px) saturate(140%);
|
|
71
|
-
-webkit-backdrop-filter: blur(24px) saturate(140%);
|
|
72
|
-
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
73
|
-
border-radius: 28px;
|
|
74
|
-
box-shadow:
|
|
75
|
-
0 1px 0 0 rgba(255, 255, 255, 0.06) inset,
|
|
76
|
-
0 20px 60px -20px rgba(0, 0, 0, 0.45);
|
|
77
|
-
}
|
|
78
|
-
.glass-card::before {
|
|
79
|
-
content: '';
|
|
80
|
-
position: absolute;
|
|
81
|
-
inset: 0;
|
|
82
|
-
border-radius: inherit;
|
|
83
|
-
pointer-events: none;
|
|
84
|
-
background: linear-gradient(180deg, rgba(255, 255, 255, 0.10) 0%, rgba(255, 255, 255, 0.02) 35%, transparent 100%);
|
|
85
|
-
mask: linear-gradient(180deg, #000, transparent 60%);
|
|
86
|
-
-webkit-mask: linear-gradient(180deg, #000, transparent 60%);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
.glass-card-md {
|
|
90
|
-
border-radius: 20px;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
.glass-pill {
|
|
94
|
-
position: relative;
|
|
95
|
-
background: rgba(20, 20, 25, 0.35);
|
|
96
|
-
backdrop-filter: blur(24px) saturate(140%);
|
|
97
|
-
-webkit-backdrop-filter: blur(24px) saturate(140%);
|
|
98
|
-
border: 1px solid rgba(255, 255, 255, 0.10);
|
|
99
|
-
box-shadow:
|
|
100
|
-
0 1px 0 0 rgba(255, 255, 255, 0.08) inset,
|
|
101
|
-
0 10px 40px -15px rgba(0, 0, 0, 0.4);
|
|
102
|
-
}
|
|
103
|
-
.glass-pill::before {
|
|
104
|
-
content: '';
|
|
105
|
-
position: absolute;
|
|
106
|
-
inset: 0;
|
|
107
|
-
border-radius: inherit;
|
|
108
|
-
pointer-events: none;
|
|
109
|
-
background: linear-gradient(180deg, rgba(255, 255, 255, 0.10) 0%, rgba(255, 255, 255, 0.02) 35%, transparent 100%);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
.glass-inner-active {
|
|
113
|
-
background: rgba(255, 255, 255, 0.14);
|
|
114
|
-
box-shadow: 0 1px 0 0 rgba(255, 255, 255, 0.12) inset;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
57
|
::selection {
|
|
118
58
|
background-color: rgba(175, 39, 227, 0.25);
|
|
119
59
|
}
|
|
@@ -177,28 +117,3 @@ h1, h2, h3, h4, h5, h6 {
|
|
|
177
117
|
0% { transform: rotate(0deg); }
|
|
178
118
|
100% { transform: rotate(360deg); }
|
|
179
119
|
}
|
|
180
|
-
|
|
181
|
-
/* ── Workspace Lock animations ── */
|
|
182
|
-
@keyframes shake {
|
|
183
|
-
0%, 100% { transform: translateX(0); }
|
|
184
|
-
10%, 30%, 50%, 70%, 90% { transform: translateX(-4px); }
|
|
185
|
-
20%, 40%, 60%, 80% { transform: translateX(4px); }
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
.animate-shake {
|
|
189
|
-
animation: shake 0.45s cubic-bezier(.36,.07,.19,.97);
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
@keyframes scale-in-dot {
|
|
193
|
-
from { transform: scale(0); opacity: 0; }
|
|
194
|
-
to { transform: scale(1); opacity: 1; }
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
.animate-scale-in {
|
|
198
|
-
animation: scale-in-dot 0.15s ease-out forwards;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
@keyframes fadeIn {
|
|
202
|
-
from { opacity: 0; transform: translateY(4px); }
|
|
203
|
-
to { opacity: 1; transform: translateY(0); }
|
|
204
|
-
}
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
[{
|
|
2
|
-
"relation": ["delegate_permission/common.handle_all_urls"],
|
|
3
|
-
"target": {
|
|
4
|
-
"namespace": "android_app",
|
|
5
|
-
"package_name": "bot.bloby.consensus",
|
|
6
|
-
"sha256_cert_fingerprints": ["1A:5D:3C:F4:D3:60:A2:65:2D:34:8F:D5:7C:18:8F:3C:01:99:9E:5B:02:0C:D0:AA:E7:8F:F4:74:EE:C3:C0:7F"]
|
|
7
|
-
}
|
|
8
|
-
}]
|
|
Binary file
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 520 80" role="img" aria-label="BlackRock">
|
|
2
|
-
<text x="0" y="60"
|
|
3
|
-
font-family="'Times New Roman', Georgia, serif"
|
|
4
|
-
font-weight="700"
|
|
5
|
-
font-size="72"
|
|
6
|
-
letter-spacing="-2"
|
|
7
|
-
fill="#ffffff">BlackRock</text>
|
|
8
|
-
</svg>
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
import { useState, useRef, useEffect } from 'react';
|
|
2
|
-
import { Send, Loader2, Sparkles } from 'lucide-react';
|
|
3
|
-
|
|
4
|
-
interface Message {
|
|
5
|
-
role: 'user' | 'assistant';
|
|
6
|
-
content: string;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export default function AiChatPage() {
|
|
10
|
-
const [messages, setMessages] = useState<Message[]>([]);
|
|
11
|
-
const [input, setInput] = useState('');
|
|
12
|
-
const [loading, setLoading] = useState(false);
|
|
13
|
-
const [sessionId, setSessionId] = useState<string | undefined>();
|
|
14
|
-
const bottomRef = useRef<HTMLDivElement>(null);
|
|
15
|
-
const inputRef = useRef<HTMLTextAreaElement>(null);
|
|
16
|
-
|
|
17
|
-
useEffect(() => {
|
|
18
|
-
bottomRef.current?.scrollIntoView({ behavior: 'smooth' });
|
|
19
|
-
}, [messages, loading]);
|
|
20
|
-
|
|
21
|
-
useEffect(() => {
|
|
22
|
-
inputRef.current?.focus();
|
|
23
|
-
}, []);
|
|
24
|
-
|
|
25
|
-
const sendMessage = async () => {
|
|
26
|
-
const text = input.trim();
|
|
27
|
-
if (!text || loading) return;
|
|
28
|
-
|
|
29
|
-
setInput('');
|
|
30
|
-
setMessages((prev) => [...prev, { role: 'user', content: text }]);
|
|
31
|
-
setLoading(true);
|
|
32
|
-
|
|
33
|
-
try {
|
|
34
|
-
const res = await fetch('/app/api/aichat', {
|
|
35
|
-
method: 'POST',
|
|
36
|
-
headers: { 'Content-Type': 'application/json' },
|
|
37
|
-
body: JSON.stringify({ message: text, sessionId }),
|
|
38
|
-
});
|
|
39
|
-
const data = await res.json();
|
|
40
|
-
|
|
41
|
-
if (data.sessionId) setSessionId(data.sessionId);
|
|
42
|
-
|
|
43
|
-
const reply = data.response || data.error || 'No response';
|
|
44
|
-
setMessages((prev) => [...prev, { role: 'assistant', content: reply }]);
|
|
45
|
-
} catch {
|
|
46
|
-
setMessages((prev) => [
|
|
47
|
-
...prev,
|
|
48
|
-
{ role: 'assistant', content: 'Failed to reach the server.' },
|
|
49
|
-
]);
|
|
50
|
-
} finally {
|
|
51
|
-
setLoading(false);
|
|
52
|
-
inputRef.current?.focus();
|
|
53
|
-
}
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
57
|
-
if (e.key === 'Enter' && !e.shiftKey) {
|
|
58
|
-
e.preventDefault();
|
|
59
|
-
sendMessage();
|
|
60
|
-
}
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
return (
|
|
64
|
-
<div className="flex flex-col h-full px-4 sm:px-6 md:px-20 pt-10 sm:pt-16 pb-32 max-w-5xl mx-auto w-full">
|
|
65
|
-
{/* Header */}
|
|
66
|
-
<div className="flex items-start gap-4 mb-6 shrink-0">
|
|
67
|
-
<div className="h-12 w-12 rounded-2xl flex items-center justify-center bg-gradient-to-br from-violet-300/40 to-fuchsia-500/20 border border-white/10 backdrop-blur-xl shrink-0">
|
|
68
|
-
<Sparkles className="h-5 w-5 text-violet-200" />
|
|
69
|
-
</div>
|
|
70
|
-
<div>
|
|
71
|
-
<h1 className="text-2xl sm:text-3xl font-semibold text-white tracking-tight">AI Chat</h1>
|
|
72
|
-
<p className="text-sm text-white/60 mt-1">Chat with Claude.</p>
|
|
73
|
-
</div>
|
|
74
|
-
</div>
|
|
75
|
-
|
|
76
|
-
{/* Messages */}
|
|
77
|
-
<div className="flex-1 overflow-y-auto space-y-4 pr-1">
|
|
78
|
-
{messages.length === 0 && !loading && (
|
|
79
|
-
<div className="flex items-center justify-center h-full">
|
|
80
|
-
<div className="glass-card glass-card-md px-5 py-4">
|
|
81
|
-
<p className="relative text-sm text-white/60">Send a message to start chatting.</p>
|
|
82
|
-
</div>
|
|
83
|
-
</div>
|
|
84
|
-
)}
|
|
85
|
-
{messages.map((msg, i) => (
|
|
86
|
-
<div
|
|
87
|
-
key={i}
|
|
88
|
-
className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}
|
|
89
|
-
>
|
|
90
|
-
{msg.role === 'user' ? (
|
|
91
|
-
<div
|
|
92
|
-
className="relative max-w-[80%] rounded-2xl px-4 py-2.5 text-sm leading-relaxed whitespace-pre-wrap text-white border border-white/10 backdrop-blur-xl"
|
|
93
|
-
style={{
|
|
94
|
-
background:
|
|
95
|
-
'linear-gradient(135deg, rgba(236, 72, 153, 0.35), rgba(225, 29, 72, 0.25))',
|
|
96
|
-
boxShadow:
|
|
97
|
-
'0 1px 0 0 rgba(255,255,255,0.1) inset, 0 10px 30px -10px rgba(244, 63, 94, 0.35)',
|
|
98
|
-
}}
|
|
99
|
-
>
|
|
100
|
-
{msg.content}
|
|
101
|
-
</div>
|
|
102
|
-
) : (
|
|
103
|
-
<div className="relative max-w-[80%] glass-card glass-card-md px-4 py-2.5">
|
|
104
|
-
<p className="relative text-sm leading-relaxed whitespace-pre-wrap text-white/90">
|
|
105
|
-
{msg.content}
|
|
106
|
-
</p>
|
|
107
|
-
</div>
|
|
108
|
-
)}
|
|
109
|
-
</div>
|
|
110
|
-
))}
|
|
111
|
-
{loading && (
|
|
112
|
-
<div className="flex justify-start">
|
|
113
|
-
<div className="relative glass-card glass-card-md px-4 py-2.5 flex items-center gap-2">
|
|
114
|
-
<Loader2 className="h-4 w-4 animate-spin text-white/60 relative" />
|
|
115
|
-
<span className="text-sm text-white/60 relative">Thinking…</span>
|
|
116
|
-
</div>
|
|
117
|
-
</div>
|
|
118
|
-
)}
|
|
119
|
-
<div ref={bottomRef} />
|
|
120
|
-
</div>
|
|
121
|
-
|
|
122
|
-
{/* Input */}
|
|
123
|
-
<div className="shrink-0 pt-4">
|
|
124
|
-
<div className="glass-pill rounded-full flex items-end gap-2 max-w-3xl mx-auto px-2 py-2">
|
|
125
|
-
<textarea
|
|
126
|
-
ref={inputRef}
|
|
127
|
-
value={input}
|
|
128
|
-
onChange={(e) => setInput(e.target.value)}
|
|
129
|
-
onKeyDown={handleKeyDown}
|
|
130
|
-
placeholder="Type a message…"
|
|
131
|
-
rows={1}
|
|
132
|
-
className="relative flex-1 resize-none bg-transparent border-none outline-none px-3 py-2 text-sm text-white placeholder-white/40 max-h-40"
|
|
133
|
-
/>
|
|
134
|
-
<button
|
|
135
|
-
onClick={sendMessage}
|
|
136
|
-
disabled={loading || !input.trim()}
|
|
137
|
-
className="relative shrink-0 h-9 w-9 rounded-full bg-gradient-to-br from-violet-500 to-fuchsia-500 hover:brightness-110 disabled:opacity-40 disabled:hover:brightness-100 flex items-center justify-center transition shadow-lg shadow-fuchsia-500/20"
|
|
138
|
-
>
|
|
139
|
-
<Send className="h-4 w-4 text-white" />
|
|
140
|
-
</button>
|
|
141
|
-
</div>
|
|
142
|
-
</div>
|
|
143
|
-
</div>
|
|
144
|
-
);
|
|
145
|
-
}
|