feat: redesign Sasquatch UI - dark jungle aesthetic, mobile-first
- Dark forest green + neon lime (#ccff00) theme matching app's THUG2 aesthetic - Mobile: compact strip above bottom nav, no more full-width overlay - Minimized: small round button with neon pulse, lives at bottom-left - Jungle texture strips top/bottom, dark gradient background - Chat bubbles: neon red for user, neon lime for Sasquatch - Input styled to match dark app theme - Neon glow effects, live dot indicator - Works cleanly on all screen sizes Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
This commit is contained in:
parent
c31f79e8b0
commit
f9c0f3c5ac
1 changed files with 125 additions and 101 deletions
|
|
@ -200,15 +200,15 @@ function CVMStatus({ connected }: { connected: boolean | null }) {
|
|||
if (connected === null) return null;
|
||||
return (
|
||||
<div className={cn(
|
||||
'flex items-center gap-1 text-[9px] px-1.5 py-0.5 rounded-full',
|
||||
'flex items-center gap-0.5 text-[8px] px-1 py-0.5 rounded',
|
||||
connected
|
||||
? 'bg-green-500/20 text-green-400'
|
||||
: 'bg-gray-500/20 text-gray-500'
|
||||
? 'bg-[#ccff00]/15 text-[#ccff00]'
|
||||
: 'bg-white/5 text-gray-500'
|
||||
)}>
|
||||
{connected ? (
|
||||
<><Wifi className="h-2.5 w-2.5" /> Live</>
|
||||
<><Wifi className="h-2 w-2" /> live</>
|
||||
) : (
|
||||
<><WifiOff className="h-2.5 w-2.5" /> Offline</>
|
||||
<><WifiOff className="h-2 w-2" /> offline</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
|
@ -403,148 +403,168 @@ Instructions:
|
|||
|
||||
return (
|
||||
<>
|
||||
{/* Minimized bubble */}
|
||||
{isMinimized ? (
|
||||
<div className="fixed bottom-20 md:bottom-4 left-4 z-40">
|
||||
<Button
|
||||
{/* ── Minimized bubble — always visible, bottom-left ── */}
|
||||
{isMinimized && (
|
||||
<div className="fixed bottom-20 md:bottom-6 left-3 md:left-4 z-50">
|
||||
<button
|
||||
onClick={() => setIsMinimized(false)}
|
||||
className="flex items-center gap-2 bg-gradient-to-r from-amber-700 to-amber-900 hover:from-amber-600 hover:to-amber-800 text-white shadow-lg h-12 px-4 border-2 border-amber-500/30"
|
||||
className="relative group flex items-center justify-center w-12 h-12 md:w-14 md:h-14 rounded-full shadow-2xl border-2 border-[#ccff00]/60 hover:border-[#ccff00] transition-all duration-200 hover:scale-110 active:scale-95"
|
||||
style={{
|
||||
background: 'radial-gradient(circle at 40% 35%, #2d4a1e 0%, #1a2e0f 50%, #0d1a07 100%)',
|
||||
boxShadow: '0 0 20px rgba(204,255,0,0.2), 0 4px 20px rgba(0,0,0,0.8)',
|
||||
}}
|
||||
aria-label="Open Sasquatch guide"
|
||||
>
|
||||
<img
|
||||
src="/images/sasquatch-guide.webp"
|
||||
alt="The Sasquatch"
|
||||
className="h-6 w-6 rounded-full"
|
||||
className="w-8 h-8 md:w-9 md:h-9 rounded-full object-cover"
|
||||
/>
|
||||
<HelpCircle className="h-4 w-4" />
|
||||
</Button>
|
||||
{/* Neon pulse ring */}
|
||||
<span className="absolute inset-0 rounded-full animate-ping opacity-20 border-2 border-[#ccff00]" />
|
||||
{/* Live dot */}
|
||||
<span className="absolute top-0 right-0 w-3 h-3 bg-[#ccff00] rounded-full border-2 border-black shadow-[0_0_6px_#ccff00]" />
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
/* Full guide panel */
|
||||
)}
|
||||
|
||||
{/* ── Full panel ── */}
|
||||
{!isMinimized && (
|
||||
<div
|
||||
className={cn(
|
||||
'fixed bottom-20 md:bottom-4 left-4 right-4 md:right-auto z-40 transition-all duration-300',
|
||||
isVisible ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-4'
|
||||
'fixed z-50 transition-all duration-300 ease-out',
|
||||
// Mobile: full-width strip at bottom above nav
|
||||
'bottom-[4.5rem] left-2 right-2',
|
||||
// Desktop: compact panel bottom-left
|
||||
'md:bottom-6 md:left-4 md:right-auto md:w-[340px]',
|
||||
isVisible ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-3 pointer-events-none'
|
||||
)}
|
||||
>
|
||||
<div className="bg-gradient-to-br from-amber-50 to-orange-50 border-2 border-amber-500/50 rounded-lg shadow-2xl max-w-sm mx-auto md:mx-0">
|
||||
{/* Jungle/dark card */}
|
||||
<div
|
||||
className="rounded-xl border border-[#ccff00]/20 shadow-2xl overflow-hidden"
|
||||
style={{
|
||||
background: 'linear-gradient(160deg, #0f1f08 0%, #0a1505 50%, #060d03 100%)',
|
||||
boxShadow: '0 0 40px rgba(0,0,0,0.9), 0 0 20px rgba(204,255,0,0.06), inset 0 1px 0 rgba(204,255,0,0.08)',
|
||||
}}
|
||||
>
|
||||
{/* Jungle texture strip at top */}
|
||||
<div
|
||||
className="h-1 w-full"
|
||||
style={{ background: 'linear-gradient(90deg, #1a3d0a, #ccff00, #2d5c10, #ccff00, #1a3d0a)' }}
|
||||
/>
|
||||
|
||||
{/* Header */}
|
||||
<div className="flex items-start gap-3 p-4 pb-3 border-b border-amber-200">
|
||||
<div className="flex items-center gap-2.5 px-3 py-2.5 border-b border-[#ccff00]/10">
|
||||
<div className="relative flex-shrink-0">
|
||||
<img
|
||||
src="/images/sasquatch-guide.webp"
|
||||
alt="The Sasquatch"
|
||||
className="h-12 w-12 rounded-full border-2 border-amber-400 shadow-md"
|
||||
className="h-9 w-9 rounded-full object-cover border border-[#ccff00]/40"
|
||||
style={{ boxShadow: '0 0 10px rgba(204,255,0,0.2)' }}
|
||||
/>
|
||||
<span className="absolute -bottom-1 -right-1 flex h-4 w-4">
|
||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75" />
|
||||
<span className="relative inline-flex rounded-full h-4 w-4 bg-green-500 border-2 border-white" />
|
||||
</span>
|
||||
<span className="absolute -bottom-0.5 -right-0.5 w-3 h-3 bg-[#ccff00] rounded-full border border-black shadow-[0_0_6px_#ccff00]" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-bold text-sm text-amber-900 flex items-center gap-2 flex-wrap">
|
||||
{message.title}
|
||||
<span className="text-xs px-1.5 py-0.5 bg-amber-200 text-amber-800 rounded-full">🛹</span>
|
||||
<div className="flex items-center gap-1.5 flex-wrap">
|
||||
<span className="font-black text-xs text-[#ccff00] uppercase tracking-wider" style={{ fontFamily: 'Impact, Arial Black, sans-serif' }}>
|
||||
🦧 Sasquatch
|
||||
</span>
|
||||
<CVMStatus connected={cvmConnected} />
|
||||
</div>
|
||||
<div className="text-xs text-amber-700 flex items-center gap-1">
|
||||
The Sasquatch
|
||||
<span className="text-[9px] bg-green-100 text-green-700 px-1 rounded">AI</span>
|
||||
<div className="text-[10px] text-[#ccff00]/50 leading-none mt-0.5">
|
||||
{message.title}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-1">
|
||||
<Button
|
||||
variant="ghost" size="icon"
|
||||
className="h-6 w-6 text-amber-600 hover:text-amber-800 hover:bg-amber-100"
|
||||
<div className="flex gap-1 flex-shrink-0">
|
||||
<button
|
||||
onClick={() => setIsMinimized(true)}
|
||||
className="w-6 h-6 flex items-center justify-center rounded text-[#ccff00]/40 hover:text-[#ccff00] hover:bg-[#ccff00]/10 transition-colors"
|
||||
aria-label="Minimize"
|
||||
>
|
||||
<Minimize2 className="h-3 w-3" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost" size="icon"
|
||||
className="h-6 w-6 text-amber-600 hover:text-red-600 hover:bg-red-50"
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setIsMinimized(true)}
|
||||
className="w-6 h-6 flex items-center justify-center rounded text-[#ccff00]/40 hover:text-[#ff0040] hover:bg-[#ff0040]/10 transition-colors"
|
||||
aria-label="Close"
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</Button>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Greeting message */}
|
||||
<div className="p-4">
|
||||
<p className="text-sm text-gray-800 leading-relaxed">
|
||||
{/* Body */}
|
||||
<div className="p-3">
|
||||
{/* Greeting */}
|
||||
<p className="text-xs text-gray-300 leading-relaxed mb-3">
|
||||
{message.message}
|
||||
</p>
|
||||
|
||||
{/* Action button */}
|
||||
{message.action && (
|
||||
<Button
|
||||
<button
|
||||
onClick={message.action.onClick}
|
||||
size="sm"
|
||||
className="mt-3 w-full bg-gradient-to-r from-amber-600 to-orange-600 hover:from-amber-700 hover:to-orange-700 text-white font-bold"
|
||||
className="w-full py-2 px-3 rounded-lg text-xs font-bold text-black bg-[#ccff00] hover:bg-[#d4ff1a] transition-colors mb-3"
|
||||
>
|
||||
{message.action.label}
|
||||
</Button>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Chat section */}
|
||||
{message.enableChat && (
|
||||
<div className="mt-4 pt-4 border-t border-amber-200">
|
||||
<div className="border-t border-[#ccff00]/10 pt-3">
|
||||
{!showChat ? (
|
||||
<Button
|
||||
<button
|
||||
onClick={() => setShowChat(true)}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="w-full border-amber-400 text-amber-800 hover:bg-amber-100"
|
||||
className="w-full py-2 px-3 rounded-lg text-xs font-bold text-[#ccff00] border border-[#ccff00]/30 hover:border-[#ccff00]/60 hover:bg-[#ccff00]/5 transition-all flex items-center justify-center gap-1.5"
|
||||
>
|
||||
<HelpCircle className="h-3 w-3 mr-2" />
|
||||
<HelpCircle className="h-3 w-3" />
|
||||
{message.context === 'lab' && 'Ask about Coaching & Filming'}
|
||||
{message.context === 'map' && 'Ask about Spots, Shops & Gear'}
|
||||
{(message.context === 'home' || message.context === 'onboarding') && 'Ask me anything 🛹'}
|
||||
{!message.context && 'Ask me anything'}
|
||||
</Button>
|
||||
{!message.context && 'Ask me anything 🛹'}
|
||||
</button>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-2.5">
|
||||
|
||||
{/* Suggested questions (before any chat) */}
|
||||
{/* Suggested questions */}
|
||||
{chatMessages.length === 0 && message.suggestedQuestions && (
|
||||
<div className="space-y-1.5">
|
||||
<div className="text-[10px] text-amber-700 font-semibold mb-2">
|
||||
Quick Questions:
|
||||
<div className="space-y-1">
|
||||
<div className="text-[9px] text-[#ccff00]/50 font-bold uppercase tracking-wider mb-1.5">
|
||||
Quick questions:
|
||||
</div>
|
||||
{message.suggestedQuestions.map((q) => (
|
||||
<Button
|
||||
<button
|
||||
key={q}
|
||||
onClick={() => handleSendMessage(q)}
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
disabled={isThinking}
|
||||
className="w-full justify-start text-xs text-amber-800 hover:bg-amber-100 h-auto py-1.5"
|
||||
className="w-full text-left text-[11px] text-gray-300 hover:text-[#ccff00] px-2.5 py-1.5 rounded border border-white/5 hover:border-[#ccff00]/20 hover:bg-[#ccff00]/5 transition-all disabled:opacity-50"
|
||||
>
|
||||
{q}
|
||||
</Button>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Chat history */}
|
||||
{(chatMessages.length > 0 || streamingText) && (
|
||||
<ScrollArea className="h-52 rounded border border-amber-200 bg-white p-2">
|
||||
<ScrollArea className="h-48 md:h-52 rounded-lg border border-white/5 bg-black/40 p-2">
|
||||
<div className="space-y-2" ref={scrollRef}>
|
||||
{chatMessages.map((msg, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className={cn(
|
||||
'text-xs p-2 rounded leading-relaxed',
|
||||
'text-[11px] p-2 rounded-lg leading-relaxed',
|
||||
msg.role === 'user'
|
||||
? 'bg-blue-100 text-blue-900 ml-4'
|
||||
: 'bg-amber-100 text-amber-900 mr-4'
|
||||
? 'bg-[#ff0040]/10 text-gray-200 ml-4 border border-[#ff0040]/20'
|
||||
: 'bg-[#ccff00]/5 text-gray-200 mr-4 border border-[#ccff00]/10'
|
||||
)}
|
||||
>
|
||||
<div className="font-bold text-[10px] mb-1 opacity-70 flex items-center gap-1">
|
||||
<div className="font-bold text-[9px] mb-1 flex items-center gap-1 opacity-60">
|
||||
{msg.role === 'user' ? '🧑 You' : '🦧 Sasquatch'}
|
||||
{msg.isLive && (
|
||||
<span className="text-[9px] bg-green-100 text-green-600 px-1 rounded">
|
||||
<span className="text-[#ccff00] bg-[#ccff00]/10 px-1 rounded text-[8px]">
|
||||
live
|
||||
</span>
|
||||
)}
|
||||
|
|
@ -553,16 +573,16 @@ Instructions:
|
|||
</div>
|
||||
))}
|
||||
|
||||
{/* Streaming / thinking indicator */}
|
||||
{/* Thinking indicator */}
|
||||
{(isThinking || streamingText) && (
|
||||
<div className="bg-amber-100 text-amber-900 mr-4 text-xs p-2 rounded">
|
||||
<div className="font-bold text-[10px] mb-1 opacity-70">🦧 Sasquatch</div>
|
||||
<div className="bg-[#ccff00]/5 text-gray-300 mr-4 text-[11px] p-2 rounded-lg border border-[#ccff00]/10">
|
||||
<div className="font-bold text-[9px] mb-1 opacity-60">🦧 Sasquatch</div>
|
||||
{streamingText ? (
|
||||
<span className="italic text-amber-700">{streamingText}</span>
|
||||
<span className="italic text-[#ccff00]/70 text-[10px]">{streamingText}</span>
|
||||
) : (
|
||||
<span className="flex items-center gap-1 text-amber-600">
|
||||
<Loader2 className="h-3 w-3 animate-spin" />
|
||||
thinking...
|
||||
<span className="flex items-center gap-1.5 text-[#ccff00]/60">
|
||||
<Loader2 className="h-2.5 w-2.5 animate-spin" />
|
||||
<span className="text-[10px]">thinking...</span>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -572,50 +592,54 @@ Instructions:
|
|||
)}
|
||||
|
||||
{/* Input row */}
|
||||
<div className="flex gap-2">
|
||||
<div className="flex gap-1.5">
|
||||
<Input
|
||||
value={userInput}
|
||||
onChange={(e) => setUserInput(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && !inputDisabled && handleSendMessage()}
|
||||
placeholder="Ask anything about skating..."
|
||||
placeholder="Ask about spots, events, coaches..."
|
||||
disabled={inputDisabled}
|
||||
className="text-sm border-amber-300 focus-visible:ring-amber-400"
|
||||
className="text-xs h-8 bg-black/40 border-[#ccff00]/20 text-white placeholder:text-gray-600 focus-visible:ring-[#ccff00]/40 focus-visible:border-[#ccff00]/40"
|
||||
/>
|
||||
<Button
|
||||
<button
|
||||
onClick={() => handleSendMessage()}
|
||||
size="sm"
|
||||
disabled={!userInput.trim() || inputDisabled}
|
||||
className="bg-amber-600 hover:bg-amber-700"
|
||||
className="h-8 w-8 flex-shrink-0 flex items-center justify-center rounded-md bg-[#ccff00] hover:bg-[#d4ff1a] disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
{isThinking ? (
|
||||
<Loader2 className="h-3 w-3 animate-spin" />
|
||||
<Loader2 className="h-3 w-3 animate-spin text-black" />
|
||||
) : (
|
||||
<Send className="h-3 w-3" />
|
||||
<Send className="h-3 w-3 text-black" />
|
||||
)}
|
||||
</Button>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* AI badge */}
|
||||
<p className="text-[10px] text-amber-600 text-center">
|
||||
Powered by AI — free for everyone 🛹
|
||||
</p>
|
||||
{/* Footer */}
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-[9px] text-gray-600">
|
||||
Powered by AI · live Nostr data 🛹
|
||||
</p>
|
||||
{chatMessages.length > 0 && (
|
||||
<button
|
||||
onClick={() => { setChatMessages([]); setShowChat(false); setCvmConnected(null); }}
|
||||
className="text-[9px] text-gray-600 hover:text-[#ccff00] transition-colors"
|
||||
>
|
||||
clear chat
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Reset */}
|
||||
{chatMessages.length > 0 && (
|
||||
<Button
|
||||
onClick={() => { setChatMessages([]); setShowChat(false); setCvmConnected(null); }}
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="w-full text-xs text-amber-600 hover:text-amber-800"
|
||||
>
|
||||
Start over
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Bottom jungle strip */}
|
||||
<div
|
||||
className="h-0.5 w-full opacity-30"
|
||||
style={{ background: 'linear-gradient(90deg, transparent, #ccff00, transparent)' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
Loading…
Reference in a new issue