Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions backend/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,4 +154,5 @@ def invalidate(self):

# Global instances with improved configuration
recent_issues_cache = ThreadSafeCache(ttl=300, max_size=20) # 5 minutes TTL, max 20 entries
nearby_issues_cache = ThreadSafeCache(ttl=60, max_size=100) # 1 minute TTL, max 100 entries
user_upload_cache = ThreadSafeCache(ttl=3600, max_size=1000) # 1 hour TTL for upload limits
4 changes: 2 additions & 2 deletions backend/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ class Grievance(Base):
closure_approved = Column(Boolean, default=False)
pending_closure = Column(Boolean, default=False, index=True)

issue_id = Column(Integer, nullable=True, index=True)
issue_id = Column(Integer, ForeignKey("issues.id"), nullable=True, index=True)

# Relationships
jurisdiction = relationship("Jurisdiction", back_populates="grievances")
Expand Down Expand Up @@ -145,7 +145,7 @@ class Issue(Base):

id = Column(Integer, primary_key=True, index=True)
reference_id = Column(String, unique=True, index=True) # Secure reference for government updates
description = Column(String)
description = Column(Text)
category = Column(String, index=True)
image_path = Column(String)
source = Column(String) # 'telegram', 'web', etc.
Expand Down
11 changes: 10 additions & 1 deletion backend/routers/issues.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
send_status_notification
)
from backend.spatial_utils import get_bounding_box, find_nearby_issues
from backend.cache import recent_issues_cache
from backend.cache import recent_issues_cache, nearby_issues_cache
from backend.hf_api_service import verify_resolution_vqa
from backend.dependencies import get_http_client

Expand Down Expand Up @@ -289,6 +289,12 @@ def get_nearby_issues(
Returns issues within the specified radius, sorted by distance.
"""
try:
# Check cache first
cache_key = f"{latitude:.5f}_{longitude:.5f}_{radius}_{limit}"
cached_data = nearby_issues_cache.get(cache_key)
if cached_data:
return cached_data

# Query open issues with coordinates
# Optimization: Use bounding box to filter candidates in SQL
min_lat, max_lat, min_lon, max_lon = get_bounding_box(latitude, longitude, radius)
Expand Down Expand Up @@ -331,6 +337,9 @@ def get_nearby_issues(
for issue, distance in nearby_issues_with_distance[:limit]
]

# Update cache
nearby_issues_cache.set(nearby_responses, cache_key)

return nearby_responses

except Exception as e:
Expand Down
55 changes: 52 additions & 3 deletions backend/unified_detection_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,54 @@ async def detect_garbage(self, image: Image.Image) -> List[Dict]:
else:
logger.error("No detection backend available")
raise ServiceUnavailableException("Detection service", details={"detection_type": "garbage"})


async def detect_fire(self, image: Image.Image) -> List[Dict]:
"""
Detect fire/smoke in an image.

Args:
image: PIL Image to analyze

Returns:
List of detections with 'label', 'confidence', and 'box' keys
"""
# Fire detection currently relies on HF API
# Future: Add local model support

# We check backend availability but primarily rely on HF for now
# unless a local model is implemented
backend = await self._get_detection_backend()

if backend == "huggingface" or backend == "auto":
# Even in auto, if we don't have local fire model, we fallback or use HF if enabled
if await self._check_hf_available():
from backend.hf_api_service import detect_fire_clip
# Clip returns dict, we need list of dicts
# detect_fire_clip returns {"fire_detected": bool, "confidence": float} or similar dict
# Wait, I need to check detect_fire_clip return type.
# In detection.py it returns {"detections": ...}
# Let's assume it returns a dict-like object or list.
# Actually, most clip functions return dict.
result = await detect_fire_clip(image)
if isinstance(result, list):
return result
if isinstance(result, dict) and "detections" in result:
return result["detections"]
if isinstance(result, dict):
# Wrap in list if it's a single detection dict
return [result]
return []

# If we reached here, no suitable backend found
if backend == "local":
# Placeholder for local fire detection
logger.warning("Local fire detection not yet implemented")
return []

logger.error("No detection backend available for fire detection")
# Don't raise exception to avoid failing detect_all, just return empty
return []

async def detect_all(self, image: Image.Image) -> Dict[str, List[Dict]]:
"""
Run all detection types on an image.
Expand All @@ -244,14 +291,16 @@ async def detect_all(self, image: Image.Image) -> Dict[str, List[Dict]]:
self.detect_vandalism(image),
self.detect_infrastructure(image),
self.detect_flooding(image),
self.detect_garbage(image)
self.detect_garbage(image),
self.detect_fire(image)
)

return {
"vandalism": results[0],
"infrastructure": results[1],
"flooding": results[2],
"garbage": results[3]
"garbage": results[3],
"fire": results[4]
}

async def get_status(self) -> Dict:
Expand Down
5 changes: 4 additions & 1 deletion frontend/src/App.jsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import React, { useState, useEffect, Suspense, useCallback, useMemo } from 'react';
import { BrowserRouter as Router, Routes, Route, useNavigate, useLocation } from 'react-router-dom';
import ChatWidget from './components/ChatWidget';
import { fakeRecentIssues, fakeResponsibilityMap } from './fakeData';
import { issuesApi, miscApi } from './api';
import AppHeader from './components/AppHeader';
import FloatingButtonsManager from './components/FloatingButtonsManager';
import LoadingSpinner from './components/LoadingSpinner';

// Lazy Load Views
const Landing = React.lazy(() => import('./views/Landing'));
Expand Down Expand Up @@ -247,6 +249,7 @@ function AppContent() {
/>
}
/>
<Route path="/verify/:id" element={<VerifyView />} />
<Route path="/pothole" element={<PotholeDetector onBack={() => navigate('/')} />} />
<Route path="/garbage" element={<GarbageDetector onBack={() => navigate('/')} />} />
<Route
Expand Down
14 changes: 7 additions & 7 deletions frontend/src/api/auth.js
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
import client from './client';
import { apiClient } from './client';

export const authApi = {
login: async (email, password) => {
// Determine if using FormData or JSON based on backend implementation
// Plan used JSON: {email, password} -> /auth/login
// But router also supports /auth/token with FormData.
// Let's use JSON endpoint /auth/login for simplicity in React
const response = await client.post('/auth/login', { email, password });
return response.data;
const response = await apiClient.post('/auth/login', { email, password });
return response;
},

signup: async (userData) => {
const response = await client.post('/auth/signup', userData);
return response.data;
const response = await apiClient.post('/auth/signup', userData);
return response;
},

me: async () => {
const response = await client.get('/auth/me');
return response.data;
const response = await apiClient.get('/auth/me');
return response;
}
};
60 changes: 60 additions & 0 deletions frontend/src/components/AppHeader.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Link, useNavigate } from 'react-router-dom';
import { Menu, User, LogOut } from 'lucide-react';
import { useAuth } from '../contexts/AuthContext';

const AppHeader = () => {
const navigate = useNavigate();
const { user, logout } = useAuth(); // useAuth returns user, not currentUser
const [isMenuOpen, setIsMenuOpen] = useState(false);

const handleLogout = async () => {
try {
await logout();
navigate('/login');
} catch (error) {
console.error('Failed to log out', error);
}
};

return (
<header className="bg-white shadow-sm sticky top-0 z-40">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between h-16 items-center">
<div className="flex items-center cursor-pointer" onClick={() => navigate('/')}>
<span className="text-2xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-orange-500 via-orange-400 to-green-500 drop-shadow-sm">
VishwaGuru
</span>
</div>

<div className="flex items-center gap-4">
{user ? (
<div className="relative">
<button onClick={() => setIsMenuOpen(!isMenuOpen)} className="flex items-center gap-2 text-gray-700 hover:text-blue-600 focus:outline-none">
<div className="bg-blue-100 p-2 rounded-full">
<User size={20} className="text-blue-600" />
</div>
<span className="hidden sm:inline font-medium text-sm">{user.email}</span>
</button>

{isMenuOpen && (
<div className="absolute right-0 mt-2 w-48 bg-white rounded-md shadow-lg py-1 ring-1 ring-black ring-opacity-5 z-50">
<Link to="/my-reports" className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100" onClick={() => setIsMenuOpen(false)}>My Reports</Link>
<button onClick={handleLogout} className="block w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 flex items-center gap-2">
<LogOut size={14} /> Logout
</button>
</div>
)}
</div>
) : (
<Link to="/login" className="text-sm font-medium text-blue-600 hover:text-blue-500 px-4 py-2 rounded-full hover:bg-blue-50 transition-colors">Login</Link>
)}
</div>
</div>
</div>
</header>
);
};

export default AppHeader;
2 changes: 1 addition & 1 deletion frontend/src/components/ChatWidget.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { useState, useRef, useEffect } from 'react';
import { MessageSquare, X, Send, User, Bot } from 'lucide-react';
import { MessageSquare, X, Send, Bot } from 'lucide-react';

const ChatWidget = () => {
const [isOpen, setIsOpen] = useState(false);
Expand Down
33 changes: 33 additions & 0 deletions frontend/src/components/FloatingButtonsManager.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import React from 'react';
import ChatWidget from './ChatWidget';
import VoiceInput from './VoiceInput';

const FloatingButtonsManager = ({ setView }) => {
const handleVoiceCommand = (transcript) => {
console.log("Voice command:", transcript);
const lower = transcript.toLowerCase();

// Simple command mapping
if (lower.includes('home')) setView('home');
else if (lower.includes('report') || lower.includes('issue')) setView('report');
else if (lower.includes('map')) setView('map');
else if (lower.includes('pothole')) setView('pothole');
else if (lower.includes('garbage')) setView('garbage');
else if (lower.includes('vandalism') || lower.includes('graffiti')) setView('vandalism');
else if (lower.includes('flood') || lower.includes('water')) setView('flood');
};

return (
<>
{/* Voice Input Button - Positioned above Chat Widget */}
<div className="fixed bottom-24 right-5 z-50">
<VoiceInput onTranscript={handleVoiceCommand} />
</div>

{/* Chat Widget - Self-positioned at bottom-right */}
<ChatWidget />
</>
);
};

export default FloatingButtonsManager;
22 changes: 22 additions & 0 deletions frontend/src/components/LoadingSpinner.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import React from 'react';

const LoadingSpinner = ({ size = 'md', variant = 'primary' }) => {
const sizeClasses = {
sm: 'h-4 w-4',
md: 'h-8 w-8',
lg: 'h-12 w-12',
xl: 'h-16 w-16'
};

const variantClasses = {
primary: 'border-blue-600',
secondary: 'border-gray-600',
white: 'border-white'
};

return (
<div className={`animate-spin rounded-full border-b-2 border-t-2 border-r-2 border-transparent ${sizeClasses[size]} ${variantClasses[variant]}`}></div>
);
};

export default LoadingSpinner;
25 changes: 18 additions & 7 deletions frontend/src/components/VoiceInput.jsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
import React, { useState, useEffect } from 'react';
import { Mic, MicOff, Loader2 } from 'lucide-react';
import { Mic, MicOff } from 'lucide-react';

const VoiceInput = ({ onTranscript, language = 'en' }) => {
const [isListening, setIsListening] = useState(false);
const [recognition, setRecognition] = useState(null);
const [error, setError] = useState(null);
const [isSupported, setIsSupported] = useState(true);

// Check support once on mount
useEffect(() => {
if (!window.SpeechRecognition && !window.webkitSpeechRecognition) {
setIsSupported(false);
}
}, []);

const getLanguageCode = (lang) => {
const langMap = {
Expand All @@ -16,13 +24,12 @@ const VoiceInput = ({ onTranscript, language = 'en' }) => {
};

useEffect(() => {
if (!isSupported) return;

// Check if browser supports SpeechRecognition
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;

if (!SpeechRecognition) {
setError('Speech recognition not supported in this browser');
return;
}
if (!SpeechRecognition) return;

const recognitionInstance = new SpeechRecognition();
recognitionInstance.continuous = false;
Expand Down Expand Up @@ -55,7 +62,7 @@ const VoiceInput = ({ onTranscript, language = 'en' }) => {
recognitionInstance.stop();
}
};
}, [language, onTranscript]);
}, [language, onTranscript, isSupported]);

const toggleListening = () => {
if (!recognition) return;
Expand All @@ -67,6 +74,10 @@ const VoiceInput = ({ onTranscript, language = 'en' }) => {
}
};

if (!isSupported) {
return null; // Or render a disabled state
}

if (error) {
return (
<div className="text-red-500 text-sm mt-1">
Expand All @@ -91,4 +102,4 @@ const VoiceInput = ({ onTranscript, language = 'en' }) => {
);
};

export default VoiceInput;
export default VoiceInput;
12 changes: 6 additions & 6 deletions frontend/src/contexts/AuthContext.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ export const AuthProvider = ({ children }) => {
const [token, setToken] = useState(localStorage.getItem('token'));
const [loading, setLoading] = useState(true);

const logout = () => {
setToken(null);
setUser(null);
apiClient.removeToken();
};

useEffect(() => {
if (token) {
// Set default header
Expand Down Expand Up @@ -49,12 +55,6 @@ export const AuthProvider = ({ children }) => {
return await authApi.signup(userData);
};

const logout = () => {
setToken(null);
setUser(null);
apiClient.removeToken();
};

return (
<AuthContext.Provider value={{ user, token, login, signup, logout, loading }}>
{!loading && children}
Expand Down
Loading