diff --git a/src/components/SasquatchGuide.tsx b/src/components/SasquatchGuide.tsx index 3df238a..0964ef4 100644 --- a/src/components/SasquatchGuide.tsx +++ b/src/components/SasquatchGuide.tsx @@ -1,12 +1,10 @@ /** * SasquatchGuide — AI-powered skate assistant * ───────────────────────────────────────────────────────────────────────────── - * Upgraded from static FAQ to a live AI agent that: - * 1. Uses Shakespeare AI (LLM) for natural language understanding - * 2. Calls the SkateSpots CVM server on Nostr for live data - * (spots, events, coaches, gear) when the question needs real data - * 3. Falls back to static FAQ answers for protocol/app questions - * (What is Nostr? What are zaps? etc.) + * Uses OpenRouter free model (no credits needed, works for any user) for + * natural language understanding, then calls the SkateSpots CVM server on + * Nostr for live data (spots, events, coaches, gear). + * Falls back to static FAQ if AI is unavailable. */ import { useState, useEffect, useRef, useCallback } from 'react'; @@ -16,10 +14,37 @@ import { ScrollArea } from '@/components/ui/scroll-area'; import { X, Minimize2, HelpCircle, Send, Loader2, Wifi, WifiOff } from 'lucide-react'; import { useLocalStorage } from '@/hooks/useLocalStorage'; import { useCurrentUser } from '@/hooks/useCurrentUser'; -import { useShakespeare } from '@/hooks/useShakespeare'; -import { useCVMClient, CVM_SERVER_PUBKEY } from '@/hooks/useCVMClient'; +import { useCVMClient } from '@/hooks/useCVMClient'; import { cn } from '@/lib/utils'; +// ─── OpenRouter free model — no credits, no NIP-98, works for everyone ──────── +const OPENROUTER_API_KEY = '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'; +const OPENROUTER_URL = 'https://openrouter.ai/api/v1/chat/completions'; +const FREE_MODEL = 'meta-llama/llama-3.1-8b-instruct:free'; + +async function callAI(messages: Array<{ role: 'user' | 'assistant'; content: string }>): Promise { + const response = await fetch(OPENROUTER_URL, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${OPENROUTER_API_KEY}`, + 'Content-Type': 'application/json', + 'HTTP-Referer': 'https://skate-spots.org', + 'X-Title': 'SkateSpots', + }, + body: JSON.stringify({ + model: FREE_MODEL, + messages, + max_tokens: 400, + }), + }); + if (!response.ok) { + const err = await response.text(); + throw new Error(`AI error ${response.status}: ${err}`); + } + const data = await response.json(); + return data.choices[0]?.message?.content ?? ''; +} + export type SasquatchContext = 'home' | 'onboarding' | 'map' | 'lab'; export interface GuideMessage { @@ -120,7 +145,6 @@ function CVMStatus({ connected }: { connected: boolean | null }) { export function SasquatchGuide({ message, onDismiss }: SasquatchGuideProps) { const { user } = useCurrentUser(); - const shakespeare = useShakespeare(); const cvm = useCVMClient(); const [isMinimized, setIsMinimized] = useLocalStorage('sasquatch-minimized', false); @@ -177,49 +201,33 @@ export function SasquatchGuide({ message, onDismiss }: SasquatchGuideProps) { return; } - // 2. Build conversation history for the AI + // 2. Build conversation history for AI const history = chatMessages.map((m) => ({ role: m.role === 'user' ? ('user' as const) : ('assistant' as const), content: m.text, })); - history.push({ role: 'user', content: question }); - // 3. If user is logged in, try Shakespeare AI (with graceful fallback) - if (user && shakespeare.isAuthenticated) { - let accumulated = ''; - let aiSucceeded = false; - try { + // 3. Try AI — free model via OpenRouter, works for everyone no credits needed + let accumulated = ''; + let aiSucceeded = false; + try { + // Embed system prompt into first user message (no system role needed) + const historyWithContext: Array<{ role: 'user' | 'assistant'; content: string }> = [ + { role: 'user', content: `${SYSTEM_PROMPT}\n\n---\n\nUser question: ${question}` }, + ...history.slice(0, -1).map((m) => ({ role: m.role, content: m.content })), + ]; // First pass: get AI response which may include a TOOL: call - // Prefix the system prompt into the first user message for compatibility - const historyWithContext = history.length > 0 - ? [ - { - role: 'user' as const, - content: `${SYSTEM_PROMPT}\n\n---\n\nUser question: ${history[0].content}`, - }, - ...history.slice(1), - ] - : [{ role: 'user' as const, content: SYSTEM_PROMPT }]; - - const firstResponse = await shakespeare.sendChatMessage( - historyWithContext, - 'shakespeare/claude-sonnet-4.6' - ); - - const aiText = firstResponse.choices[0]?.message?.content as string ?? ''; + const aiText = await callAI(historyWithContext); // 4. Check if AI wants to call a CVM tool const toolMatch = aiText.match(/TOOL:(\w+)\s+(\{.*?\})/s); if (toolMatch) { const toolName = toolMatch[1]; let toolArgs: Record = {}; - try { - toolArgs = JSON.parse(toolMatch[2]); - } catch { /* keep empty */ } + try { toolArgs = JSON.parse(toolMatch[2]); } catch { /* keep empty */ } - // Show "searching..." indicator - setStreamingText(`🔍 Searching Nostr for ${toolName.replace('_', ' ')}...`); + setStreamingText(`🔍 Searching Nostr for ${toolName.replace(/_/g, ' ')}...`); try { // Call the live CVM server @@ -245,59 +253,39 @@ export function SasquatchGuide({ message, onDismiss }: SasquatchGuideProps) { } setCvmConnected(true); + setStreamingText(''); // 5. Send tool result back to AI for a natural language response - setStreamingText(''); const toolResultStr = JSON.stringify(toolResult, null, 2); - const secondResponse = await shakespeare.sendChatMessage([ - { - role: 'user' as const, - content: `${SYSTEM_PROMPT}\n\n---\n\nUser asked: ${question}\n\nTool result from ${toolName}:\n${toolResultStr}\n\nNow answer the user's question naturally based on this live data. Be concise and skater-friendly.`, - }, - ], 'shakespeare/claude-sonnet-4.6'); - - accumulated = secondResponse.choices[0]?.message?.content as string ?? ''; + accumulated = await callAI([ + { role: 'user', content: `${SYSTEM_PROMPT}\n\n---\n\nUser asked: ${question}\n\nLive data from ${toolName}:\n${toolResultStr}\n\nAnswer naturally and concisely based on this data.` }, + ]); } catch (toolErr) { - // CVM server unreachable — answer from AI knowledge alone setCvmConnected(false); console.warn('[Sasquatch] CVM tool call failed:', toolErr); accumulated = aiText.replace(/TOOL:\w+\s+\{.*?\}/s, '').trim() + "\n\n*(Couldn't reach live data right now — try again in a moment!)*"; } } else { - // No tool call needed — AI answered from knowledge accumulated = aiText; } - setStreamingText(''); - appendMessage({ - role: 'sasquatch', - text: accumulated || "Hmm, couldn't process that. Try rephrasing!", - timestamp: Date.now(), - isLive: !!toolMatch && cvmConnected === true, - }); - aiSucceeded = true; - } catch (aiErr) { - // AI failed — fall back to static FAQ silently - console.warn('[Sasquatch] AI unavailable, using static FAQ:', aiErr); - } - - if (!aiSucceeded) { - const fallback = staticAnswer ?? - "Couldn't reach the AI right now. Check the map for spots, The Lab for coaches, or ask again in a moment! 🛹"; - appendMessage({ role: 'sasquatch', text: fallback, timestamp: Date.now() }); - } - - } else { - // 6. Not logged in — use static FAQ or generic fallback - const fallback = - staticAnswer ?? - "I can answer much more with a Nostr login! For now: browse the map to discover spots, check The Lab for coaches, and hit Settings to connect your Lightning wallet. 🛹"; + setStreamingText(''); appendMessage({ role: 'sasquatch', - text: fallback, + text: accumulated || "Hmm, couldn't process that. Try rephrasing!", timestamp: Date.now(), + isLive: !!toolMatch && cvmConnected === true, }); + aiSucceeded = true; + } catch (aiErr) { + console.warn('[Sasquatch] AI unavailable, using static FAQ:', aiErr); + } + + if (!aiSucceeded) { + const fallback = staticAnswer ?? + "Couldn't reach the AI right now. Check the map for spots, The Lab for coaches, or ask again in a moment! 🛹"; + appendMessage({ role: 'sasquatch', text: fallback, timestamp: Date.now() }); } } catch (err) { console.error('[Sasquatch] Error:', err); @@ -365,9 +353,7 @@ export function SasquatchGuide({ message, onDismiss }: SasquatchGuideProps) {
The Sasquatch - {user && ( - AI - )} + AI
@@ -495,11 +481,7 @@ export function SasquatchGuide({ message, onDismiss }: SasquatchGuideProps) { value={userInput} onChange={(e) => setUserInput(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && !inputDisabled && handleSendMessage()} - placeholder={ - !user - ? 'Login for AI answers...' - : 'Ask anything about skating...' - } + placeholder="Ask anything about skating..." disabled={inputDisabled} className="text-sm border-amber-300 focus-visible:ring-amber-400" /> @@ -517,12 +499,10 @@ export function SasquatchGuide({ message, onDismiss }: SasquatchGuideProps) {
- {/* Not logged in nudge */} - {!user && ( -

- Login with Nostr for AI-powered answers + live data 🛹 -

- )} + {/* AI badge */} +

+ Powered by AI — free for everyone 🛹 +

{/* Reset */} {chatMessages.length > 0 && (