const FLASK_API_BASE = 'https://llm.sirblob.co'; export interface WeatherData { temperature: number; description: string; humidity: number; windSpeed: number; precipitation?: number; visibility?: number; summary?: string; timeOfDay?: string; } export interface CrashAnalysisData { riskLevel: string; crashSummary?: { totalCrashes: number; totalCasualties: number; severityBreakdown: Record; }; recommendations: string[]; safetyAnalysis?: string; } export interface SafeRouteData { success: boolean; start_coordinates: { lat: number; lon: number }; end_coordinates: { lat: number; lon: number }; recommended_route: { coordinates: [number, number][]; distance_km: number; duration_min: number; geometry?: any; // GeoJSON for Mapbox safety_score: number; crashes_nearby: number; max_danger_score: number; }; safety_analysis: string; weather_summary?: string; route_comparison?: any; alternative_routes: Array<{ route_id: string; coordinates: [number, number][]; distance_km: number; duration_min: number; geometry?: any; safety_score: number; crashes_nearby: number; }>; } export const fetchWeatherData = async (lat: number, lng: number): Promise => { const response = await fetch(`${FLASK_API_BASE}/api/weather?lat=${lat}&lon=${lng}`); if (!response.ok) { throw new Error('Failed to fetch weather data'); } const data = await response.json(); return transformWeatherData(data); }; export const fetchCrashAnalysis = async (lat: number, lng: number): Promise => { const response = await fetch(`${FLASK_API_BASE}/api/analyze-crashes`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ lat, lon: lng }), }); if (!response.ok) { throw new Error('Failed to fetch crash analysis'); } const data = await response.json(); return transformCrashAnalysis(data); }; export const fetchSafeRoute = async ( startLat: number, startLon: number, endLat: number, endLon: number ): Promise => { const response = await fetch(`${FLASK_API_BASE}/api/find-safe-route`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ start_lat: startLat, start_lon: startLon, end_lat: endLat, end_lon: endLon, }), }); if (!response.ok) { throw new Error('Failed to fetch safe route'); } const data = await response.json(); if (!data.success) { throw new Error(data.error || 'Unknown error occurred'); } return data; }; // Transform Flask weather API response to our WeatherData interface const transformWeatherData = (apiResponse: any): WeatherData => { // Extract summary if available let summary = ''; let timeOfDay = ''; if (apiResponse.summary) { summary = apiResponse.summary; // Extract time of day from summary if (summary.includes('Time: ')) { const timeMatch = summary.match(/Time: (\w+)/); if (timeMatch) { timeOfDay = timeMatch[1]; } } } // Extract data from weather_data.current if available const current = apiResponse.weather_data?.current; return { temperature: current?.temperature_2m || apiResponse.temperature || 0, description: current?.weather_description || apiResponse.description || (summary.includes('Conditions: ') ? summary.split('Conditions: ')[1]?.split(' |')[0] || 'N/A' : 'N/A'), humidity: current?.relative_humidity_2m || apiResponse.humidity || 0, windSpeed: current?.wind_speed_10m || apiResponse.windSpeed || 0, precipitation: current?.precipitation || apiResponse.precipitation || 0, visibility: current?.visibility || apiResponse.visibility, summary: summary, timeOfDay: timeOfDay || (current?.is_day === 0 ? 'night' : current?.is_day === 1 ? 'day' : '') }; }; // Transform Flask crash analysis API response to our CrashAnalysisData interface const transformCrashAnalysis = (apiResponse: any): CrashAnalysisData => { const data = apiResponse; // Extract risk level from safety analysis text with multiple fallback strategies let riskLevel = 'unknown'; let recommendations: string[] = []; if (data.safety_analysis) { const safetyText = data.safety_analysis; const safetyTextLower = safetyText.toLowerCase(); // Strategy 1: Look for explicit danger level assessment const dangerLevelMatch = safetyText.match(/danger level assessment[:\s]*([^.\n]+)/i); if (dangerLevelMatch) { const level = dangerLevelMatch[1].trim().toLowerCase(); console.log('🎯 Found danger level assessment:', level); if (level.includes('very high') || level.includes('extreme')) { riskLevel = 'high'; } else if (level.includes('high')) { riskLevel = 'high'; } else if (level.includes('moderate') || level.includes('medium')) { riskLevel = 'medium'; } else if (level.includes('low')) { riskLevel = 'low'; } } // Strategy 2: Look for risk level patterns if first strategy failed if (riskLevel === 'unknown') { const riskPatterns = [ { keywords: ['very high risk', 'extremely dangerous', 'very dangerous', 'extreme danger'], level: 'high' }, { keywords: ['high risk', 'high danger', 'dangerous area', 'significant risk'], level: 'high' }, { keywords: ['moderate risk', 'medium risk', 'moderate danger', 'moderately dangerous'], level: 'medium' }, { keywords: ['low risk', 'relatively safe', 'low danger', 'minimal risk'], level: 'low' } ]; for (const pattern of riskPatterns) { if (pattern.keywords.some(keyword => safetyTextLower.includes(keyword))) { riskLevel = pattern.level; console.log('🎯 Found risk pattern:', pattern.keywords[0], '→', pattern.level); break; } } } // Strategy 3: Fallback to crash data severity if text parsing fails if (riskLevel === 'unknown' && data.crash_summary) { const summary = data.crash_summary; const totalCrashes = summary.total_crashes || 0; const totalCasualties = summary.total_casualties || 0; if (totalCasualties > 10 || totalCrashes > 20) { riskLevel = 'high'; console.log('🎯 Fallback: High risk based on casualties/crashes', { totalCasualties, totalCrashes }); } else if (totalCasualties > 3 || totalCrashes > 8) { riskLevel = 'medium'; console.log('🎯 Fallback: Medium risk based on casualties/crashes', { totalCasualties, totalCrashes }); } else if (totalCrashes > 0) { riskLevel = 'low'; console.log('🎯 Fallback: Low risk based on crashes', { totalCrashes }); } } console.log('📊 Final risk level determination:', riskLevel); // Extract recommendations from safety analysis (more flexible approach) const recommendationsMatch = safetyText.match(/(?:specific recommendations|recommendations)[^:]*:([\s\S]*?)(?=\n\n|\d+\.|$)/i); if (recommendationsMatch) { const recommendationsText = recommendationsMatch[1]; // Split by lines and filter for meaningful recommendations const lines = recommendationsText.split('\n') .map((line: string) => line.trim()) .filter((line: string) => line.length > 20 && !line.match(/^\d+\./)) .slice(0, 4); recommendations = lines; } // If no specific recommendations section found, try to extract key sentences if (recommendations.length === 0) { const sentences = safetyText.split(/[.!?]/) .map((sentence: string) => sentence.trim()) .filter((sentence: string) => sentence.length > 30 && (sentence.toLowerCase().includes('recommend') || sentence.toLowerCase().includes('should') || sentence.toLowerCase().includes('consider') || sentence.toLowerCase().includes('avoid')) ) .slice(0, 3); recommendations = sentences.map((s: string) => s + (s.endsWith('.') ? '' : '.')); } } return { riskLevel: riskLevel, crashSummary: data.crash_summary ? { totalCrashes: data.crash_summary.total_crashes || 0, totalCasualties: data.crash_summary.total_casualties || 0, severityBreakdown: data.crash_summary.severity_breakdown || {} } : undefined, recommendations: recommendations.slice(0, 5), // Limit to 5 recommendations safetyAnalysis: data.safety_analysis || '' }; };