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 (