diff --git a/backend/models.py b/backend/models.py index b8a109d4..59bdbf3d 100644 --- a/backend/models.py +++ b/backend/models.py @@ -144,7 +144,10 @@ class Issue(Base): longitude = Column(Float, nullable=True, index=True) location = Column(String, nullable=True) action_plan = Column(JSONEncodedDict, nullable=True) + Emergency-and-High-Severity-#290 + severity = Column(Enum(SeverityLevel), default=SeverityLevel.MEDIUM, index=True) integrity_hash = Column(String, nullable=True) # Blockchain integrity seal + main class PushSubscription(Base): __tablename__ = "push_subscriptions" diff --git a/backend/routers/grievances.py b/backend/routers/grievances.py index 6de629f2..eedc882b 100644 --- a/backend/routers/grievances.py +++ b/backend/routers/grievances.py @@ -1,6 +1,6 @@ from fastapi import APIRouter, Depends, HTTPException, Query, Request from sqlalchemy.orm import Session, joinedload -from sqlalchemy import func +from sqlalchemy import func, case from typing import List, Optional import os import json @@ -8,7 +8,10 @@ from datetime import datetime, timezone from backend.database import get_db + Emergency-and-High-Severity-#290 +from backend.models import Grievance, EscalationAudit, SeverityLevel from backend.models import Grievance, EscalationAudit, GrievanceFollower, ClosureConfirmation + main from backend.schemas import ( GrievanceSummaryResponse, EscalationAuditResponse, EscalationStatsResponse, ResponsibilityMapResponse, @@ -44,6 +47,16 @@ def get_grievances( if category: query = query.filter(Grievance.category == category) + # Priority Queue Logic: Sort by Severity (Critical > High > Medium > Low) then by Date (Oldest first for resolution) + severity_order = case( + (Grievance.severity == SeverityLevel.CRITICAL, 1), + (Grievance.severity == SeverityLevel.HIGH, 2), + (Grievance.severity == SeverityLevel.MEDIUM, 3), + (Grievance.severity == SeverityLevel.LOW, 4), + else_=5 + ) + query = query.order_by(severity_order.asc(), Grievance.created_at.asc()) + grievances = query.offset(offset).limit(limit).all() # Convert to response format diff --git a/backend/routers/issues.py b/backend/routers/issues.py index e98c6e4f..f1d8c2e6 100644 --- a/backend/routers/issues.py +++ b/backend/routers/issues.py @@ -42,6 +42,7 @@ async def create_issue( background_tasks: BackgroundTasks, description: str = Form(..., min_length=10, max_length=1000), category: str = Form(..., pattern=f"^({'|'.join([cat.value for cat in IssueCategory])})$"), + severity: str = Form('medium', pattern="^(low|medium|high|critical)$"), language: str = Form('en'), user_email: str = Form(None), latitude: float = Form(None, ge=-90, le=90), @@ -187,7 +188,10 @@ async def create_issue( longitude=longitude, location=location, action_plan=None, + Emergency-and-High-Severity-#290 + severity=severity integrity_hash=integrity_hash + main ) # Offload blocking DB operations to threadpool @@ -592,6 +596,22 @@ def get_recent_issues( # Convert to Pydantic models for validation and serialization data = [] + Emergency-and-High-Severity-#290 + for i in issues: + data.append(IssueSummaryResponse( + id=i.id, + category=i.category, + description=i.description[:100] + "..." if len(i.description) > 100 else i.description, + created_at=i.created_at, + image_path=i.image_path, + status=i.status, + upvotes=i.upvotes if i.upvotes is not None else 0, + location=i.location, + latitude=i.latitude, + longitude=i.longitude, + severity=i.severity.value if hasattr(i.severity, 'value') else i.severity + # action_plan is deferred and excluded + ).model_dump(mode='json')) for row in results: # Manually construct dict from named tuple row to avoid full object overhead desc = row.description or "" @@ -609,6 +629,7 @@ def get_recent_issues( "latitude": row.latitude, "longitude": row.longitude }) + main # Thread-safe cache update recent_issues_cache.set(data, cache_key) diff --git a/backend/schemas.py b/backend/schemas.py index 277fa8f9..2b2c4a35 100644 --- a/backend/schemas.py +++ b/backend/schemas.py @@ -44,11 +44,17 @@ class IssueSummaryResponse(BaseModel): location: Optional[str] = None latitude: Optional[float] = None longitude: Optional[float] = None + Emergency-and-High-Severity-#290 + severity: Optional[str] = "medium" + action_plan: Optional[Any] = None + + model_config = ConfigDict(from_attributes=True) model_config = ConfigDict(from_attributes=True) class IssueResponse(IssueSummaryResponse): action_plan: Optional[Union[Dict[str, Any], Any]] = Field(None, description="Generated action plan") + main class IssueCreateRequest(BaseModel): description: str = Field(..., min_length=10, max_length=1000, description="Issue description") @@ -57,6 +63,7 @@ class IssueCreateRequest(BaseModel): latitude: Optional[float] = Field(None, ge=-90, le=90, description="Latitude coordinate") longitude: Optional[float] = Field(None, ge=-180, le=180, description="Longitude coordinate") location: Optional[str] = Field(None, max_length=200, description="Location description") + severity: Optional[str] = Field("medium", pattern="^(low|medium|high|critical)$", description="Severity level") @field_validator('description') @classmethod @@ -156,6 +163,7 @@ class NearbyIssueResponse(BaseModel): upvotes: int = Field(..., description="Number of upvotes") created_at: datetime = Field(..., description="Issue creation timestamp") status: str = Field(..., description="Issue status") + severity: str = Field(default="medium", description="Issue severity") class DeduplicationCheckResponse(BaseModel): diff --git a/backend/sla_config_service.py b/backend/sla_config_service.py index ff3afcbb..21f43e2a 100644 --- a/backend/sla_config_service.py +++ b/backend/sla_config_service.py @@ -83,6 +83,17 @@ def get_sla_hours(self, severity: SeverityLevel, jurisdiction_level: Jurisdictio return sla_config.sla_hours # Return default + # Return default based on severity if available + severity_defaults = { + SeverityLevel.CRITICAL: 6, + SeverityLevel.HIGH: 24, + SeverityLevel.MEDIUM: 48, + SeverityLevel.LOW: 72 + } + + if severity in severity_defaults: + return severity_defaults[severity] + return self.default_sla_hours finally: diff --git a/backend/tasks.py b/backend/tasks.py index af8b3286..1fe574b6 100644 --- a/backend/tasks.py +++ b/backend/tasks.py @@ -1,6 +1,7 @@ import logging import json import os +import hashlib from pywebpush import webpush, WebPushException from backend.database import SessionLocal from backend.models import Issue, PushSubscription @@ -59,7 +60,19 @@ async def create_grievance_from_issue_background(issue_id: int): 'vandalism': 'medium' } - severity = severity_mapping.get(issue.category.lower(), 'medium') + # Prefer issue's own severity if set, otherwise fallback to mapping + if hasattr(issue, 'severity') and issue.severity: + severity = issue.severity.value if hasattr(issue.severity, 'value') else issue.severity + else: + severity = severity_mapping.get(issue.category.lower(), 'medium') + + # Check for immediate escalation triggers (e.g. Critical severity) + if severity == 'critical': + # Log critical issue with safe identifiers only (no PII) + description_text = issue.description or "" + desc_hash = hashlib.sha256(description_text.encode('utf-8')).hexdigest()[:12] + logger.warning(f"CRITICAL ISSUE REPORTED: ID={issue.id} [Hash={desc_hash}]") + # Here we could trigger immediate SMS/Call alerts or specialized notifications # Create grievance data grievance_data = { diff --git a/frontend/src/locales/en.json b/frontend/src/locales/en.json index 8459c4fa..ed6fca58 100644 --- a/frontend/src/locales/en.json +++ b/frontend/src/locales/en.json @@ -14,7 +14,13 @@ "civicServices": "Civic Services", "management": "Management" }, + "urgent": "URGENT", "issues": { + "noise": "Noise", + "crowd": "Crowd", + "waterLeak": "Water Leak", + "wasteSorter": "Waste Sorter", + "civicEye": "Civic Eye", "pothole": "Pothole", "blockedRoad": "Blocked Road", "illegalParking": "Illegal Parking", diff --git a/frontend/src/views/Home.jsx b/frontend/src/views/Home.jsx index 610d6e33..573b5df5 100644 --- a/frontend/src/views/Home.jsx +++ b/frontend/src/views/Home.jsx @@ -78,40 +78,47 @@ const Home = ({ setView, fetchResponsibilityMap, recentIssues, handleUpvote, loa title: t('home.categories.roadTraffic'), icon: , items: [ - { id: 'pothole', label: t('home.issues.pothole'), icon: , color: 'text-red-600', bg: 'bg-red-50' }, - { id: 'blocked', label: t('home.issues.blockedRoad'), icon: , color: 'text-gray-600', bg: 'bg-gray-50' }, - { id: 'parking', label: t('home.issues.illegalParking'), icon: , color: 'text-rose-600', bg: 'bg-rose-50' }, - { id: 'streetlight', label: t('home.issues.darkStreet'), icon: , color: 'text-slate-600', bg: 'bg-slate-50' }, - { id: 'traffic-sign', label: t('home.issues.trafficSign'), icon: , color: 'text-yellow-600', bg: 'bg-yellow-50' }, - { id: 'abandoned-vehicle', label: t('home.issues.abandonedVehicle'), icon: , color: 'text-gray-600', bg: 'bg-gray-50' }, + { id: 'pothole', labelKey: 'home.issues.pothole', icon: , color: 'text-red-600', bg: 'bg-red-50' }, + { id: 'blocked', labelKey: 'home.issues.blockedRoad', icon: , color: 'text-gray-600', bg: 'bg-gray-50' }, + { id: 'parking', labelKey: 'home.issues.illegalParking', icon: , color: 'text-rose-600', bg: 'bg-rose-50' }, + { id: 'streetlight', labelKey: 'home.issues.darkStreet', icon: , color: 'text-slate-600', bg: 'bg-slate-50' }, + { id: 'traffic-sign', labelKey: 'home.issues.trafficSign', icon: , color: 'text-yellow-600', bg: 'bg-yellow-50' }, + { id: 'abandoned-vehicle', labelKey: 'home.issues.abandonedVehicle', icon: , color: 'text-gray-600', bg: 'bg-gray-50' }, ] }, { title: t('home.categories.environmentSafety'), icon: , items: [ - { id: 'garbage', label: t('home.issues.garbage'), icon: , color: 'text-orange-600', bg: 'bg-orange-50' }, - { id: 'flood', label: t('home.issues.flood'), icon: , color: 'text-cyan-600', bg: 'bg-cyan-50' }, - { id: 'fire', label: t('home.issues.fireSmoke'), icon: , color: 'text-red-600', bg: 'bg-red-50' }, - { id: 'tree', label: t('home.issues.treeHazard'), icon: , color: 'text-green-600', bg: 'bg-green-50' }, - { id: 'animal', label: t('home.issues.strayAnimal'), icon: , color: 'text-amber-600', bg: 'bg-amber-50' }, - { id: 'pest', label: t('home.issues.pestControl'), icon: , color: 'text-amber-800', bg: 'bg-amber-50' }, - { id: 'noise', label: "Noise", icon: , color: 'text-purple-600', bg: 'bg-purple-50' }, - { id: 'crowd', label: "Crowd", icon: , color: 'text-red-500', bg: 'bg-red-50' }, - { id: 'water-leak', label: "Water Leak", icon: , color: 'text-blue-500', bg: 'bg-blue-50' }, - { id: 'waste', label: "Waste Sorter", icon: , color: 'text-emerald-600', bg: 'bg-emerald-50' }, + { id: 'garbage', labelKey: 'home.issues.garbage', icon: , color: 'text-orange-600', bg: 'bg-orange-50' }, + { id: 'flood', labelKey: 'home.issues.flood', icon: , color: 'text-cyan-600', bg: 'bg-cyan-50' }, + { id: 'fire', labelKey: 'home.issues.fireSmoke', icon: , color: 'text-red-600', bg: 'bg-red-50' }, + { id: 'tree', labelKey: 'home.issues.treeHazard', icon: , color: 'text-green-600', bg: 'bg-green-50' }, + { id: 'animal', labelKey: 'home.issues.strayAnimal', icon: , color: 'text-amber-600', bg: 'bg-amber-50' }, + { id: 'pest', labelKey: 'home.issues.pestControl', icon: , color: 'text-amber-800', bg: 'bg-amber-50' }, + { id: 'noise', labelKey: 'home.issues.noise', icon: , color: 'text-purple-600', bg: 'bg-purple-50' }, + { id: 'crowd', labelKey: 'home.issues.crowd', icon: , color: 'text-red-500', bg: 'bg-red-50' }, + { id: 'water-leak', labelKey: 'home.issues.waterLeak', icon: , color: 'text-blue-500', bg: 'bg-blue-50' }, + { id: 'waste', labelKey: 'home.issues.wasteSorter', icon: , color: 'text-emerald-600', bg: 'bg-emerald-50' }, ] }, { title: t('home.categories.management'), icon: , items: [ + Emergency-and-High-Severity-#290 + { id: 'civic-eye', labelKey: 'home.issues.civicEye', icon: , color: 'text-blue-600', bg: 'bg-blue-50' }, + { id: 'grievance', labelKey: 'home.issues.grievanceManagement', icon: , color: 'text-orange-600', bg: 'bg-orange-50' }, + { id: 'stats', labelKey: 'home.issues.viewStats', icon: , color: 'text-indigo-600', bg: 'bg-indigo-50' }, + { id: 'leaderboard', labelKey: 'home.issues.leaderboard', icon: , color: 'text-yellow-600', bg: 'bg-yellow-50' }, + { id: 'map', labelKey: 'home.issues.responsibilityMap', icon: , color: 'text-green-600', bg: 'bg-green-50' }, { id: 'safety-check', label: "Civic Eye", icon: , color: 'text-blue-600', bg: 'bg-blue-50' }, { id: 'my-reports', label: "My Reports", icon: , color: 'text-teal-600', bg: 'bg-teal-50' }, { id: 'grievance', label: t('home.issues.grievanceManagement'), icon: , color: 'text-orange-600', bg: 'bg-orange-50' }, { id: 'stats', label: t('home.issues.viewStats'), icon: , color: 'text-indigo-600', bg: 'bg-indigo-50' }, { id: 'leaderboard', label: t('home.issues.leaderboard'), icon: , color: 'text-yellow-600', bg: 'bg-yellow-50' }, { id: 'map', label: t('home.issues.responsibilityMap'), icon: , color: 'text-green-600', bg: 'bg-green-50' }, + main ] } ]; @@ -148,6 +155,163 @@ const Home = ({ setView, fetchResponsibilityMap, recentIssues, handleUpvote, loa {totalImpact} {t('home.issuesSolved')} + Emergency-and-High-Severity-#290 + + + + {/* Quick Actions Grid */} +
+ + + + + + + + + + + + + + + {/* New Western Style Features */} + + + + + + + + + + + + + + + +
@@ -304,6 +468,7 @@ const Home = ({ setView, fetchResponsibilityMap, recentIssues, handleUpvote, loa Analyze Grievance + main {/* Smart Scanner CTA */} - + {/* Categorized Features */}
- {categories.map((cat, idx) => ( -
-
- {cat.icon} -

{cat.title}

-
-
- {cat.items.map((item) => ( - - ))} + { + categories.map((cat, idx) => ( +
+
+ {cat.icon} +

{cat.title}

+
+
+ {cat.items.map((item) => ( + + ))} +
-
- ))} + )) + }
{/* Additional Tools */} @@ -390,7 +557,8 @@ const Home = ({ setView, fetchResponsibilityMap, recentIssues, handleUpvote, loa
{recentIssues.length > 0 ? ( recentIssues.map((issue) => ( -
+
{issue.category} + {(issue.severity === 'high' || issue.severity === 'critical') && ( + + {t('home.urgent')} + + )} {new Date(issue.created_at).toLocaleDateString()} @@ -455,30 +628,32 @@ const Home = ({ setView, fetchResponsibilityMap, recentIssues, handleUpvote, loa
)}
-
+
{/* Scroll to Top Button - Appears on scroll */} {/* Scroll to Top Button - Portal to Body */} - {createPortal( - - {showScrollTop && ( - - - - )} - , - document.body - )} + { + createPortal( + + {showScrollTop && ( + + + + )} + , + document.body + ) + } ); }; diff --git a/frontend/src/views/ReportForm.jsx b/frontend/src/views/ReportForm.jsx index 8dbc60c3..b3fef086 100644 --- a/frontend/src/views/ReportForm.jsx +++ b/frontend/src/views/ReportForm.jsx @@ -20,7 +20,8 @@ const ReportForm = ({ setView, setLoading, setError, setActionPlan, loading }) = image: null, latitude: null, longitude: null, - location: '' + location: '', + severity: 'medium' }); const [gettingLocation, setGettingLocation] = useState(false); const [severity, setSeverity] = useState(null); @@ -54,50 +55,50 @@ const ReportForm = ({ setView, setLoading, setError, setActionPlan, loading }) = }, []); const analyzeUrgency = async () => { - if (!formData.description || formData.description.length < 5) return; - setAnalyzingUrgency(true); - try { - const response = await fetch(`${API_URL}/api/analyze-urgency`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ description: formData.description }), - }); - if (response.ok) { - const data = await response.json(); - setUrgencyAnalysis(data); - } - } catch (e) { - console.error("Urgency analysis failed", e); - } finally { - setAnalyzingUrgency(false); + if (!formData.description || formData.description.length < 5) return; + setAnalyzingUrgency(true); + try { + const response = await fetch(`${API_URL}/api/analyze-urgency`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ description: formData.description }), + }); + if (response.ok) { + const data = await response.json(); + setUrgencyAnalysis(data); } + } catch (e) { + console.error("Urgency analysis failed", e); + } finally { + setAnalyzingUrgency(false); + } }; const autoDescribe = async () => { - if (!formData.image) return; - setDescribing(true); + if (!formData.image) return; + setDescribing(true); - const uploadData = new FormData(); - uploadData.append('image', formData.image); + const uploadData = new FormData(); + uploadData.append('image', formData.image); - try { - const response = await fetch(`${API_URL}/api/generate-description`, { - method: 'POST', - body: uploadData - }); - if (response.ok) { - const data = await response.json(); - if (data.description) { - setFormData(prev => ({...prev, description: data.description})); - } - } - } catch (e) { - console.error("Auto description failed", e); - } finally { - setDescribing(false); + try { + const response = await fetch(`${API_URL}/api/generate-description`, { + method: 'POST', + body: uploadData + }); + if (response.ok) { + const data = await response.json(); + if (data.description) { + setFormData(prev => ({ ...prev, description: data.description })); + } } + } catch (e) { + console.error("Auto description failed", e); + } finally { + setDescribing(false); + } }; const analyzeImage = async (file) => { @@ -110,87 +111,90 @@ const ReportForm = ({ setView, setLoading, setError, setActionPlan, loading }) = uploadData.append('image', file); try { - const response = await fetch(`${API_URL}/api/detect-severity`, { - method: 'POST', - body: uploadData - }); - if (response.ok) { - const data = await response.json(); - setSeverity(data); - } else { - const errorData = await response.json().catch(() => ({ detail: 'Unknown error' })); - setAnalysisErrors(prev => ({ ...prev, severity: errorData.detail || 'Analysis failed' })); + const response = await fetch(`${API_URL}/api/detect-severity`, { + method: 'POST', + body: uploadData + }); + if (response.ok) { + const data = await response.json(); + setSeverity(data); + if (data.level) { + setFormData(prev => ({ ...prev, severity: data.level.toLowerCase() })); } + } else { + const errorData = await response.json().catch(() => ({ detail: 'Unknown error' })); + setAnalysisErrors(prev => ({ ...prev, severity: errorData.detail || 'Analysis failed' })); + } } catch (e) { - console.error("Severity analysis failed", e); - setAnalysisErrors(prev => ({ ...prev, severity: 'Network error - please try again' })); + console.error("Severity analysis failed", e); + setAnalysisErrors(prev => ({ ...prev, severity: 'Network error - please try again' })); } finally { - setAnalyzing(false); + setAnalyzing(false); } }; const analyzeDepth = async () => { - if (!formData.image) return; - setAnalyzingDepth(true); - setDepthMap(null); + if (!formData.image) return; + setAnalyzingDepth(true); + setDepthMap(null); - const uploadData = new FormData(); - uploadData.append('image', formData.image); + const uploadData = new FormData(); + uploadData.append('image', formData.image); - try { - const data = await detectorsApi.depth(uploadData); - if (data && data.depth_map) { - setDepthMap(data.depth_map); - } - } catch (e) { - console.error("Depth analysis failed", e); - } finally { - setAnalyzingDepth(false); + try { + const data = await detectorsApi.depth(uploadData); + if (data && data.depth_map) { + setDepthMap(data.depth_map); } + } catch (e) { + console.error("Depth analysis failed", e); + } finally { + setAnalyzingDepth(false); + } }; const mapSmartScanToCategory = (label) => { - const map = { - 'pothole': 'road', - 'garbage': 'garbage', - 'flooded street': 'water', - 'fire accident': 'road', - 'fallen tree': 'road', - 'stray animal': 'road', - 'blocked road': 'road', - 'broken streetlight': 'streetlight', - 'illegal parking': 'road', - 'graffiti vandalism': 'college_infra', - 'normal street': 'road' - }; - return map[label] || 'road'; + const map = { + 'pothole': 'road', + 'garbage': 'garbage', + 'flooded street': 'water', + 'fire accident': 'road', + 'fallen tree': 'road', + 'stray animal': 'road', + 'blocked road': 'road', + 'broken streetlight': 'streetlight', + 'illegal parking': 'road', + 'graffiti vandalism': 'college_infra', + 'normal street': 'road' + }; + return map[label] || 'road'; }; const analyzeSmartScan = async (file) => { - if (!file) return; - setAnalyzingSmartScan(true); - setSmartCategory(null); - setAnalysisErrors(prev => ({ ...prev, smartScan: null })); + if (!file) return; + setAnalyzingSmartScan(true); + setSmartCategory(null); + setAnalysisErrors(prev => ({ ...prev, smartScan: null })); - const uploadData = new FormData(); - uploadData.append('image', file); + const uploadData = new FormData(); + uploadData.append('image', file); - try { - const data = await detectorsApi.smartScan(uploadData); - if (data && data.category && data.category !== 'unknown') { - const mappedCategory = mapSmartScanToCategory(data.category); - setSmartCategory({ - original: data.category, - mapped: mappedCategory, - confidence: data.confidence - }); - } - } catch (e) { - console.error("Smart scan failed", e); - setAnalysisErrors(prev => ({ ...prev, smartScan: 'Smart scan failed - continuing with manual selection' })); - } finally { - setAnalyzingSmartScan(false); + try { + const data = await detectorsApi.smartScan(uploadData); + if (data && data.category && data.category !== 'unknown') { + const mappedCategory = mapSmartScanToCategory(data.category); + setSmartCategory({ + original: data.category, + mapped: mappedCategory, + confidence: data.confidence + }); } + } catch (e) { + console.error("Smart scan failed", e); + setAnalysisErrors(prev => ({ ...prev, smartScan: 'Smart scan failed - continuing with manual selection' })); + } finally { + setAnalyzingSmartScan(false); + } }; const compressImage = (file, maxWidth = 1024, maxHeight = 1024, quality = 0.8) => { @@ -243,7 +247,7 @@ const ReportForm = ({ setView, setLoading, setError, setActionPlan, loading }) = }); } - setFormData({...formData, image: processedFile}); + setFormData({ ...formData, image: processedFile }); // Analyze in parallel but with error handling await Promise.allSettled([ @@ -253,7 +257,7 @@ const ReportForm = ({ setView, setLoading, setError, setActionPlan, loading }) = } catch (error) { console.error('Image processing failed:', error); // Fallback to original file - setFormData({...formData, image: file}); + setFormData({ ...formData, image: file }); await Promise.allSettled([ analyzeImage(file), analyzeSmartScan(file) @@ -291,21 +295,21 @@ const ReportForm = ({ setView, setLoading, setError, setActionPlan, loading }) = const checkNearbyIssues = async () => { if (!formData.latitude || !formData.longitude) { - getLocation(); // Try to get location first - return; + getLocation(); // Try to get location first + return; } setCheckingNearby(true); try { - const response = await fetch(`${API_URL}/api/issues/nearby?latitude=${formData.latitude}&longitude=${formData.longitude}&radius=50`); - if (response.ok) { - const data = await response.json(); - setNearbyIssues(data); - setShowNearbyModal(true); - } + const response = await fetch(`${API_URL}/api/issues/nearby?latitude=${formData.latitude}&longitude=${formData.longitude}&radius=50`); + if (response.ok) { + const data = await response.json(); + setNearbyIssues(data); + setShowNearbyModal(true); + } } catch (e) { - console.error("Failed to check nearby issues", e); + console.error("Failed to check nearby issues", e); } finally { - setCheckingNearby(false); + setCheckingNearby(false); } }; @@ -355,10 +359,14 @@ const ReportForm = ({ setView, setLoading, setError, setActionPlan, loading }) = if (formData.image) { payload.append('image', formData.image); } - // Append severity info if available + // Append severity info + payload.append('severity', formData.severity); + if (severity) { - payload.append('severity_level', severity.level); - payload.append('severity_score', severity.confidence); + // We still send detailed AI analysis if key matches what backend expects, or for logging + // But the main field is 'severity' + payload.append('ai_severity_level', severity.level); + payload.append('severity_score', severity.confidence); } try { @@ -399,53 +407,160 @@ const ReportForm = ({ setView, setLoading, setError, setActionPlan, loading }) = return (
-

Report an Issue

-
-
- - setFormData({ ...formData, category: e.target.value })} + > + + + + + + + + {analyzingSmartScan && ( +
+ + AI is analyzing image for category... +
+ )} + {analysisErrors.smartScan && ( +
+ + {analysisErrors.smartScan} +
+ )} + {smartCategory && ( +
setFormData({ ...formData, category: smartCategory.mapped })} + className="mt-2 bg-gradient-to-r from-purple-50 to-indigo-50 border border-purple-100 p-2 rounded-lg cursor-pointer hover:bg-purple-100 transition flex items-center justify-between group" > - - - - - - - - {analyzingSmartScan && ( -
- - AI is analyzing image for category... +
+ +
+

AI Suggestion

+

{smartCategory.original}

- )} - {analysisErrors.smartScan && ( -
- - {analysisErrors.smartScan} -
- )} - {smartCategory && ( -
setFormData({...formData, category: smartCategory.mapped})} - className="mt-2 bg-gradient-to-r from-purple-50 to-indigo-50 border border-purple-100 p-2 rounded-lg cursor-pointer hover:bg-purple-100 transition flex items-center justify-between group" - > -
- -
-

AI Suggestion

-

{smartCategory.original}

-
-
-
- Apply -
-
- )} +
+
+ Apply +
+
+ )} +
+ +
+ + + {severity && ( +
+
+ AI Severity Analysis + {severity.level} +
+

+ Detected {severity.raw_label || severity.level} with {(severity.confidence * 100).toFixed(0)}% confidence. +

+
+ )} +
+ +
+ + +
+ +
+ +
+