import { useEffect, useRef, useState } from 'react'; import { Link } from 'react-router-dom'; import { ImageWithFallback } from '@/components/ImageWithFallback'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, } from '@/components/ui/dialog'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; import { Send, Loader2, MapPin, ChevronRight, Sparkles } from 'lucide-react'; import { cn } from '@/lib/utils'; import { CREW_AGENT_AVATAR } from '@/lib/crewAgent'; import { useCrewAgentChat, type CrewContext, type CrewAgentMessage } from '@/hooks/useCrewAgentChat'; interface CrewAgentPanelProps { open: boolean; onOpenChange: (open: boolean) => void; ctx: CrewContext; } /** Turn a skate-spots.org URL into an in-app path so links stay inside the app. */ function toAppPath(url: string): string { try { return new URL(url).pathname; } catch { return url; } } interface ReceiptItem { key: string; title: string; subtitle?: string; image?: string; to: string; } /** Normalise a CVM tool result into flat receipt cards. */ function toReceiptItems(tool: string, data: unknown): ReceiptItem[] { const d = data as Record; const take = (arr: unknown): T[] => (Array.isArray(arr) ? (arr as T[]).slice(0, 6) : []); if (tool === 'find_spots') { return take>(d.spots).map((s, i) => ({ key: `${s.url}-${i}`, title: s.name || 'Spot', subtitle: [s.difficulty, s.location].filter(Boolean).join(' · ') || undefined, image: s.image, to: toAppPath(s.url), })); } if (tool === 'find_events') { return take>(d.events).map((e, i) => ({ key: `${e.url}-${i}`, title: e.title || 'Event', subtitle: e.location, image: e.image, to: toAppPath(e.url), })); } if (tool === 'find_coaches') { return take>(d.profiles).map((p, i) => ({ key: `${p.url}-${i}`, title: p.title || p.role || 'Lab pro', subtitle: [p.role, p.location].filter(Boolean).join(' · ') || undefined, image: p.image, to: toAppPath(p.url), })); } if (tool === 'find_gear') { return take>(d.listings).map((g, i) => ({ key: `${g.url}-${i}`, title: g.title || 'Gear', subtitle: g.condition, image: g.image, to: toAppPath(g.url), })); } if (tool === 'find_crews') { return take>(d.crews).map((c, i) => ({ key: `${c.url}-${i}`, title: c.name || 'Crew', subtitle: c.location, image: c.image, to: toAppPath(c.url), })); } return []; } function ReceiptCards({ tool, data, onNavigate }: { tool: string; data: unknown; onNavigate: () => void }) { const items = toReceiptItems(tool, data); if (items.length === 0) return null; return (
{items.map((item) => (
} />

{item.title}

{item.subtitle &&

{item.subtitle}

}
))}
); } function MessageRow({ m, onNavigate }: { m: CrewAgentMessage; onNavigate: () => void }) { const isAgent = m.role === 'agent'; return (
{isAgent && ( SQ )}
{m.text}
{isAgent && m.receipts && ( )}
); } const SUGGESTIONS = [ "What's our next jam?", 'Who runs this crew?', 'What are we looking for?', 'Best spots near us?', "What's the weather for skating?", ]; export function CrewAgentPanel({ open, onOpenChange, ctx }: CrewAgentPanelProps) { const { messages, isThinking, send } = useCrewAgentChat(ctx); const [draft, setDraft] = useState(''); const scrollRef = useRef(null); useEffect(() => { scrollRef.current?.scrollTo({ top: scrollRef.current.scrollHeight, behavior: 'smooth' }); }, [messages, isThinking]); const submit = (text: string) => { const t = text.trim(); if (!t || isThinking) return; setDraft(''); send(t); }; return ( SQ Ask Sasquatch Crew Agent Knows this crew — its jams, roster & open call — plus the wider scene with live data.
{messages.length === 0 && !isThinking && (
SQ

Yo! I'm Sasquatch, {ctx.crewName}'s guide. Ask me about the crew — our next jam, who runs it, what we're after — or the wider scene.

{SUGGESTIONS.map((s) => ( ))}
)} {messages.map((m) => ( onOpenChange(false)} /> ))} {isThinking && (
Sasquatch is thinking…
)}
{ e.preventDefault(); submit(draft); }} className="flex items-center gap-2" > setDraft(e.target.value)} placeholder="Ask about your local scene…" maxLength={300} className="flex-1" />

AI can be wrong — links go to real listings.

); }