diff --git a/src/components/SasquatchGuide.tsx b/src/components/SasquatchGuide.tsx index f26f11d..f73c9db 100644 --- a/src/components/SasquatchGuide.tsx +++ b/src/components/SasquatchGuide.tsx @@ -16,6 +16,43 @@ import { useLocalStorage } from '@/hooks/useLocalStorage'; import { useCurrentUser } from '@/hooks/useCurrentUser'; import { useCVMClient } from '@/hooks/useCVMClient'; import { cn } from '@/lib/utils'; +import { fetchWeather, calculateSkateability } from '@/lib/weather'; +import ngeohash from 'ngeohash'; + +// ─── Weather helper — fetch current conditions for a city/spot via Open-Meteo ── + +async function fetchWeatherForCity(city: string): Promise { + // Geocode the city using Nominatim + const geoResp = await fetch( + `https://nominatim.openstreetmap.org/search?q=${encodeURIComponent(city)}&format=json&limit=1`, + { headers: { 'Accept-Language': 'en', 'User-Agent': 'SkateSpots/1.0' } } + ); + const geoData = await geoResp.json(); + if (!geoData?.length) return `No location found for "${city}"`; + + const lat = parseFloat(geoData[0].lat); + const lng = parseFloat(geoData[0].lon); + const weather = await fetchWeather(lat, lng); + const geohash = ngeohash.encode(lat, lng, 5); + const skateability = calculateSkateability(weather, undefined); + + return JSON.stringify({ + city, + temperature_c: weather.temperature, + feels_like_c: weather.feelsLike, + condition: weather.weatherDescription, + icon: weather.weatherIcon, + precipitation_mm: weather.precipitation, + wind_kmh: weather.windSpeed, + humidity_pct: weather.humidity, + uv_index: weather.uvIndex, + skateability_score: skateability.score, + skateability_label: skateability.label, + skateability_reasons: skateability.reasons, + geohash, + is_day: weather.isDay, + }); +} // ─── OpenRouter free model — no credits, no NIP-98, works for everyone ──────── const OPENROUTER_API_KEY = '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'; @@ -133,6 +170,12 @@ Example: {"tool":"find_gear","args":{"category":"deck"}} Args: none Example: {"tool":"get_community_stats","args":{}} +### get_weather +Get real-time weather + skateability score for any city or spot location. +Args: city(string) — city name or location +Example: {"tool":"get_weather","args":{"city":"Bangkok"}} +Example: {"tool":"get_weather","args":{"city":"Barcelona"}} + ## Rules 1. Data questions (spots/events/coaches/gear/stats) → ALWAYS use a tool, never guess 2. Tool call = ONE JSON object on its own line, nothing else before or after @@ -214,6 +257,55 @@ function CVMStatus({ connected }: { connected: boolean | null }) { ); } +// ─── ChatText — renders markdown links as real tags ────────────────────── + +function ChatText({ text }: { text: string }) { + // Split on markdown links [label](url) and bare URLs + const parts = text.split(/(\[([^\]]+)\]\((https?:\/\/[^\)]+)\)|https?:\/\/\S+)/g); + const elements: React.ReactNode[] = []; + let i = 0; + // Use a regex to find all links in order + const linkRegex = /\[([^\]]+)\]\((https?:\/\/[^\)]+)\)|👉\s*(https?:\/\/\S+)|(https?:\/\/\S+)/g; + let last = 0; + let match: RegExpExecArray | null; + while ((match = linkRegex.exec(text)) !== null) { + // Text before this match + if (match.index > last) { + elements.push({text.slice(last, match.index)}); + } + if (match[1] && match[2]) { + // [label](url) + elements.push( + + {match[1]} + + ); + } else if (match[3]) { + // 👉 url + elements.push( + + {match[3]} + + ); + } else if (match[4]) { + // bare url + elements.push( + + {match[4]} + + ); + } + last = match.index + match[0].length; + } + if (last < text.length) { + elements.push({text.slice(last)}); + } + return <>{elements}; +} + // ─── Main component ─────────────────────────────────────────────────────────── export function SasquatchGuide({ message, onDismiss }: SasquatchGuideProps) { @@ -327,6 +419,12 @@ export function SasquatchGuide({ message, onDismiss }: SasquatchGuideProps) { case 'get_community_stats': toolResult = await cvm.getCommunityStats(); break; + case 'get_weather': { + const weatherCity = (toolArgs as { city?: string }).city ?? ''; + const weatherJson = await fetchWeatherForCity(weatherCity); + toolResult = { content: [{ type: 'text', text: weatherJson }] }; + break; + } default: toolResult = { error: `Unknown tool: ${toolName}` }; } @@ -569,7 +667,7 @@ Instructions: )} -
{msg.text}
+
))} @@ -616,15 +714,25 @@ Instructions: {/* Footer */}
-

- Powered by AI · live Nostr data 🛹 +

+ Powered by{' '} + + ContextVM + + {' & '} + Nostr Relays Data

{chatMessages.length > 0 && ( )}