fix: Sasquatch AI fallback chain + greeting responses + better error message
- Try 4 free OpenRouter models in order (rate limit resilience) - Add greeting responses to static FAQ so hi/hello/hey never need AI - Fix static FAQ to exact-match short words like "hi" before partial match - Better error message explaining free tier rate limits Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
This commit is contained in:
parent
a9c677b58d
commit
cb7714d875
1 changed files with 49 additions and 21 deletions
|
|
@ -22,27 +22,47 @@ const OPENROUTER_API_KEY = 'sk-or-v1-ce29b4c58cd842212e4b2f1fae7eb31ab85ce93870b
|
|||
const OPENROUTER_URL = 'https://openrouter.ai/api/v1/chat/completions';
|
||||
const FREE_MODEL = 'meta-llama/llama-3.3-70b-instruct:free';
|
||||
|
||||
// Try multiple free models in order — if one is rate-limited, fall back to the next
|
||||
const FREE_MODELS = [
|
||||
'meta-llama/llama-3.3-70b-instruct:free',
|
||||
'meta-llama/llama-3.1-8b-instruct:free',
|
||||
'google/gemma-3-4b-it:free',
|
||||
'mistralai/mistral-7b-instruct:free',
|
||||
];
|
||||
|
||||
async function callAI(messages: Array<{ role: 'user' | 'assistant'; content: string }>): Promise<string> {
|
||||
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}`);
|
||||
let lastError = '';
|
||||
for (const model of FREE_MODELS) {
|
||||
try {
|
||||
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,
|
||||
messages,
|
||||
max_tokens: 400,
|
||||
}),
|
||||
});
|
||||
if (!response.ok) {
|
||||
const err = await response.text();
|
||||
console.warn(`[Sasquatch] model ${model} failed ${response.status}:`, err);
|
||||
lastError = `${response.status}: ${err}`;
|
||||
continue; // try next model
|
||||
}
|
||||
const data = await response.json();
|
||||
const content = data.choices?.[0]?.message?.content ?? '';
|
||||
if (content) return content;
|
||||
} catch (err) {
|
||||
console.warn(`[Sasquatch] model ${model} threw:`, err);
|
||||
lastError = String(err);
|
||||
}
|
||||
}
|
||||
const data = await response.json();
|
||||
return data.choices[0]?.message?.content ?? '';
|
||||
throw new Error(`All AI models failed. Last error: ${lastError}`);
|
||||
}
|
||||
|
||||
export type SasquatchContext = 'home' | 'onboarding' | 'map' | 'lab';
|
||||
|
|
@ -155,6 +175,11 @@ function needsLiveData(question: string): boolean {
|
|||
// ─── Static FAQ fallback (for protocol/app questions without AI) ──────────────
|
||||
|
||||
const STATIC_FAQ: Record<string, string> = {
|
||||
'hi': "Hey! I'm The Sasquatch 🦧 Your AI skate guide. Ask me about spots, events, coaches, gear — or anything about the app. What do you need?",
|
||||
'hello': "Wassup! 🛹 I'm The Sasquatch. Ask me about skate spots, events, coaches, or how the app works. What's good?",
|
||||
'hey': "Hey skater! 🤙 Ask me about spots near you, upcoming events, coaches, or anything SkateSpots.",
|
||||
'who are you': "I'm The Sasquatch 🦧 — the AI guide for SkateSpots. I can find skate spots worldwide, look up events, coaches, filmers, and gear listings. All powered by live Nostr data. Ask away!",
|
||||
'what can you do': "I can find skate spots by city or country, look up upcoming events, find coaches and filmers in The Lab, search gear listings, and answer questions about how the app works. Try: 'spots in Bangkok' or 'find a coach'! 🛹",
|
||||
'what is nostr': "Nostr is a simple, open protocol for decentralized social networks. Think Twitter but no one can ban you. You use cryptographic keys instead of usernames — one account works everywhere across all Nostr apps! 🔑",
|
||||
'what are zaps': "Zaps are instant Bitcoin Lightning tips sent over Nostr! Click ⚡ on any content, send sats directly to the creator's wallet. Like hitting 'like' but with real money. Instant, peer-to-peer, no middlemen. ⚡",
|
||||
'how to login': "Two ways:\n1. **Browser extension** (Desktop): Install Alby or nos2x, then click 'Log in'\n2. **New user**: Click 'Sign up' to generate keys. Download your nsec (secret key) and SAVE IT — this is your account forever!",
|
||||
|
|
@ -168,7 +193,10 @@ const STATIC_FAQ: Record<string, string> = {
|
|||
};
|
||||
|
||||
function getStaticAnswer(question: string): string | null {
|
||||
const q = question.toLowerCase();
|
||||
const q = question.toLowerCase().trim();
|
||||
// Exact match first (for greetings like "hi", "hey")
|
||||
if (STATIC_FAQ[q]) return STATIC_FAQ[q];
|
||||
// Then partial match
|
||||
for (const [key, answer] of Object.entries(STATIC_FAQ)) {
|
||||
if (q.includes(key)) return answer;
|
||||
}
|
||||
|
|
@ -348,7 +376,7 @@ export function SasquatchGuide({ message, onDismiss }: SasquatchGuideProps) {
|
|||
|
||||
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! 🛹";
|
||||
"The AI is a bit slow right now (free tier rate limits). Try again in a moment, or check the 🗺️ Map for spots, 🧪 The Lab for coaches. 🛹";
|
||||
appendMessage({ role: 'sasquatch', text: fallback, timestamp: Date.now() });
|
||||
}
|
||||
} catch (err) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue