248 lines
8.4 KiB
TypeScript
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 || ''
|
|
};
|
|
}; |