Files
VTHacks13/web/src/lib/flaskApi.ts
2025-09-28 07:18:33 -04:00

248 lines
8.4 KiB
TypeScript

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<string, number>;
};
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<WeatherData> => {
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<CrashAnalysisData> => {
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<SafeRouteData> => {
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 || ''
};
};