Skip to content
Open
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
10 changes: 4 additions & 6 deletions backend/tests/test_detection_bytes.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,7 @@ def client():
backend.dependencies.SHARED_HTTP_CLIENT = mock_client
yield c

@pytest.mark.asyncio
async def test_detect_vandalism_with_bytes(client):
def test_detect_vandalism_with_bytes(client):
# We need to control the response for specific tests
# Since client is fixture, the http_client is already initialized in app.state
# We can access it via app.state.http_client (which is the mock_client from fixture)
Expand All @@ -92,7 +91,7 @@ async def test_detect_vandalism_with_bytes(client):
patch('backend.pothole_detection.validate_image_for_processing'), \
patch('backend.routers.detection.detect_vandalism_unified', AsyncMock(return_value=[{"label": "graffiti", "score": 0.95}])):
response = client.post(
"/detect-vandalism",
"/api/detect-vandalism",
files={"image": ("test.jpg", img_bytes, "image/jpeg")}
)

Expand All @@ -104,8 +103,7 @@ async def test_detect_vandalism_with_bytes(client):

# Client not invoked because detection is mocked above

@pytest.mark.asyncio
async def test_detect_infrastructure_with_bytes(client):
def test_detect_infrastructure_with_bytes(client):
mock_client = app.state.http_client
mock_client.post.reset_mock()

Expand All @@ -130,7 +128,7 @@ async def test_detect_infrastructure_with_bytes(client):
patch('backend.pothole_detection.validate_image_for_processing'), \
patch('backend.routers.detection.detect_infrastructure_unified', AsyncMock(return_value=[{"label": "fallen tree", "score": 0.8}])):
response = client.post(
"/detect-infrastructure",
"/api/detect-infrastructure",
files={"image": ("test.jpg", img_bytes, "image/jpeg")}
)

Expand Down
9 changes: 3 additions & 6 deletions backend/tests/test_new_detectors.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,7 @@ def create_test_image():
img.save(img_byte_arr, format='JPEG')
return img_byte_arr.getvalue()

@pytest.mark.asyncio
async def test_detect_traffic_sign_damaged(client):
def test_detect_traffic_sign_damaged(client):
# Mock the HF API response at the lower level (_make_request or query_hf_api)
# Since we are mocking the client, we mock the client.post response

Expand Down Expand Up @@ -86,8 +85,7 @@ async def test_detect_traffic_sign_damaged(client):
assert len(data["detections"]) == 1
assert data["detections"][0]["label"] == "damaged traffic sign"

@pytest.mark.asyncio
async def test_detect_traffic_sign_clear(client):
def test_detect_traffic_sign_clear(client):
img_bytes = create_test_image()

with patch('backend.utils.validate_uploaded_file'), \
Expand All @@ -102,8 +100,7 @@ async def test_detect_traffic_sign_clear(client):
# Should be empty because 'clear traffic sign' is not in targets
assert len(data["detections"]) == 0

@pytest.mark.asyncio
async def test_detect_abandoned_vehicle_found(client):
def test_detect_abandoned_vehicle_found(client):
img_bytes = create_test_image()

with patch('backend.utils.validate_uploaded_file'), \
Expand Down
6 changes: 3 additions & 3 deletions backend/tests/test_new_features.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ def test_detect_waste(client_with_mock_http):

with patch('backend.utils.validate_uploaded_file'):
response = client.post(
"/detect-waste",
"/api/detect-waste",
files={"image": ("test.jpg", img_bytes, "image/jpeg")}
)

Expand Down Expand Up @@ -95,7 +95,7 @@ def test_detect_civic_eye(client_with_mock_http):

with patch('backend.utils.validate_uploaded_file'):
response = client.post(
"/detect-civic-eye",
"/api/detect-civic-eye",
files={"image": ("test.jpg", img_bytes, "image/jpeg")}
)

Expand All @@ -114,7 +114,7 @@ def test_transcribe_audio(client_with_mock_http):
with patch('backend.routers.detection.transcribe_audio', new_callable=AsyncMock) as mock_transcribe:
mock_transcribe.return_value = "This is a test transcription."
response = client.post(
"/transcribe-audio",
"/api/transcribe-audio",
files={"file": ("test.wav", audio_content, "audio/wav")}
)

Expand Down
5 changes: 2 additions & 3 deletions backend/tests/test_severity.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,7 @@

from backend.main import app

@pytest.mark.asyncio
async def test_detect_severity_endpoint():
def test_detect_severity_endpoint():
# Mock AI services initialization to prevent startup failure
with patch('backend.main.create_all_ai_services') as mock_create_services, \
patch('backend.main.initialize_ai_services') as mock_init_services, \
Expand Down Expand Up @@ -66,7 +65,7 @@ async def test_detect_severity_endpoint():
with TestClient(app) as client:
# Call the endpoint
with patch('backend.utils.validate_uploaded_file'):
response = client.post("/detect-severity", files=files)
response = client.post("/api/detect-severity", files=files)

# Assertions
assert response.status_code == 200
Expand Down
4 changes: 3 additions & 1 deletion frontend/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import { AuthProvider, useAuth } from './contexts/AuthContext';
import Login from './views/Login';
import ProtectedRoute from './components/ProtectedRoute';
import AdminDashboard from './views/AdminDashboard';
import EmotionDetector from './views/EmotionDetector';
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

EmotionDetector is imported eagerly, while most views/detectors are lazy-loaded in this file. Since this view pulls in heavier dependencies (e.g., react-webcam, framer-motion), consider switching it to React.lazy like the other routes to avoid increasing the initial bundle size.

Copilot uses AI. Check for mistakes.

// Create a wrapper component to handle state management
function AppContent() {
Expand All @@ -69,7 +70,7 @@ function AppContent() {

// Safe navigation helper
const navigateToView = useCallback((view) => {
const validViews = ['home', 'map', 'report', 'action', 'mh-rep', 'pothole', 'garbage', 'vandalism', 'flood', 'infrastructure', 'parking', 'streetlight', 'fire', 'animal', 'blocked', 'tree', 'pest', 'smart-scan', 'grievance-analysis', 'noise', 'safety-check', 'insight', 'my-reports', 'grievance', 'login', 'signup'];
const validViews = ['home', 'map', 'report', 'action', 'mh-rep', 'pothole', 'garbage', 'vandalism', 'flood', 'infrastructure', 'parking', 'streetlight', 'fire', 'animal', 'blocked', 'tree', 'pest', 'smart-scan', 'grievance-analysis', 'noise', 'safety-check', 'insight', 'my-reports', 'grievance', 'login', 'signup', 'emotion'];
if (validViews.includes(view)) {
navigate(view === 'home' ? '/' : `/${view}`);
} else {
Expand Down Expand Up @@ -334,6 +335,7 @@ function AppContent() {
<Route path="/smart-scan" element={<SmartScanner onBack={() => navigate('/')} />} />
<Route path="/grievance-analysis" element={<GrievanceAnalysis onBack={() => navigate('/')} />} />
<Route path="/noise" element={<NoiseDetector onBack={() => navigate('/')} />} />
<Route path="/emotion" element={<EmotionDetector onBack={() => navigate('/')} />} />
<Route path="/safety-check" element={
<div className="flex flex-col h-full p-4">
<button onClick={() => navigate('/')} className="self-start text-blue-600 mb-2 font-bold">
Expand Down
256 changes: 256 additions & 0 deletions frontend/src/views/EmotionDetector.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
import React, { useState, useRef, useCallback } from 'react';
import { Camera, Image as ImageIcon, Loader2, ArrowLeft, RefreshCw, Smile } from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion';
import Webcam from 'react-webcam';
import { detectorsApi } from '../api';

const EmotionDetector = ({ onBack }) => {
const [image, setImage] = useState(null);
const [previewUrl, setPreviewUrl] = useState(null);
const [isAnalyzing, setIsAnalyzing] = useState(false);
const [result, setResult] = useState(null);
const [error, setError] = useState(null);
const [showWebcam, setShowWebcam] = useState(false);
const webcamRef = useRef(null);

const captureWebcam = useCallback(() => {
const imageSrc = webcamRef.current.getScreenshot();
if (imageSrc) {
fetch(imageSrc)
.then(res => res.blob())
.then(blob => {
const file = new File([blob], "emotion_capture.jpg", { type: "image/jpeg" });
setImage(file);
setPreviewUrl(imageSrc);
setShowWebcam(false);
setResult(null);
setError(null);
});
}
}, [webcamRef]);
Comment on lines +16 to +30
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add null guard before accessing webcamRef.current.

If captureWebcam is invoked before the webcam component mounts (e.g., rapid click or race condition), webcamRef.current will be null and calling getScreenshot() will throw a TypeError.

Proposed fix
   const captureWebcam = useCallback(() => {
+    if (!webcamRef.current) return;
     const imageSrc = webcamRef.current.getScreenshot();
     if (imageSrc) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/views/EmotionDetector.jsx` around lines 16 - 30, The
captureWebcam function should guard against webcamRef.current being null before
calling getScreenshot to avoid TypeError; update the captureWebcam callback to
check that webcamRef && webcamRef.current exist (and that getScreenshot is a
function) before calling webcamRef.current.getScreenshot(), and bail out early
(or setError) if missing; keep the existing logic that converts the screenshot
to a blob, creates the File, and calls setImage, setPreviewUrl, setShowWebcam,
setResult, and setError when a valid imageSrc is obtained.


Comment on lines +17 to +31
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

captureWebcam calls webcamRef.current.getScreenshot() without checking that webcamRef.current is set. If the user clicks Capture before the ref is ready (or if Webcam fails to mount), this will throw. Add a null guard (and consider surfacing an error to the UI).

Suggested change
const imageSrc = webcamRef.current.getScreenshot();
if (imageSrc) {
fetch(imageSrc)
.then(res => res.blob())
.then(blob => {
const file = new File([blob], "emotion_capture.jpg", { type: "image/jpeg" });
setImage(file);
setPreviewUrl(imageSrc);
setShowWebcam(false);
setResult(null);
setError(null);
});
}
}, [webcamRef]);
if (!webcamRef.current) {
setError('Unable to access the webcam. Please ensure it is connected and allowed, then try again.');
return;
}
const imageSrc = webcamRef.current.getScreenshot();
if (!imageSrc) {
setError('Could not capture an image from the webcam. Please try again.');
return;
}
fetch(imageSrc)
.then(res => res.blob())
.then(blob => {
const file = new File([blob], "emotion_capture.jpg", { type: "image/jpeg" });
setImage(file);
setPreviewUrl(imageSrc);
setShowWebcam(false);
setResult(null);
setError(null);
});
}, [webcamRef, setError]);

Copilot uses AI. Check for mistakes.
const handleImageUpload = (e) => {
const file = e.target.files[0];
if (file) {
setImage(file);
setPreviewUrl(URL.createObjectURL(file));
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Object URLs created for uploaded images are never revoked, which causes avoidable memory growth on repeated uploads.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At frontend/src/views/EmotionDetector.jsx, line 36:

<comment>Object URLs created for uploaded images are never revoked, which causes avoidable memory growth on repeated uploads.</comment>

<file context>
@@ -0,0 +1,256 @@
+    const file = e.target.files[0];
+    if (file) {
+      setImage(file);
+      setPreviewUrl(URL.createObjectURL(file));
+      setResult(null);
+      setError(null);
</file context>
Fix with Cubic

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Revoke blob preview URLs to avoid memory leaks.

URL.createObjectURL(file) on Line 36 is never revoked during reset/replacement/unmount, so repeated uploads can leak memory.

💡 Suggested fix
-import React, { useState, useRef, useCallback } from 'react';
+import React, { useState, useRef, useCallback, useEffect } from 'react';
...
 const EmotionDetector = ({ onBack }) => {
 ...
+  useEffect(() => {
+    return () => {
+      if (previewUrl?.startsWith('blob:')) {
+        URL.revokeObjectURL(previewUrl);
+      }
+    };
+  }, [previewUrl]);
...
   const handleImageUpload = (e) => {
     const file = e.target.files[0];
     if (file) {
+      if (previewUrl?.startsWith('blob:')) {
+        URL.revokeObjectURL(previewUrl);
+      }
       setImage(file);
       setPreviewUrl(URL.createObjectURL(file));
       setResult(null);
       setError(null);
     }
   };

   const resetDetector = () => {
+    if (previewUrl?.startsWith('blob:')) {
+      URL.revokeObjectURL(previewUrl);
+    }
     setImage(null);
     setPreviewUrl(null);
     setResult(null);
     setError(null);
   };

Also applies to: 42-45

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/views/EmotionDetector.jsx` at line 36, The blob URL created with
URL.createObjectURL(file) in EmotionDetector.jsx (when calling setPreviewUrl) is
never revoked; update the component to revoke previous blob URLs whenever you
replace or clear the preview and on unmount: when setting a new preview inside
the handler that calls setPreviewUrl, call URL.revokeObjectURL for the existing
previewUrl state first (or ensure you store and revoke the old value), and add a
useEffect cleanup that revokes the current previewUrl on component unmount; also
revoke the preview in any reset/clear logic referenced around the reset handler
(lines ~42-45) so no blob URLs leak.

setResult(null);
setError(null);
}
};

const resetDetector = () => {
setImage(null);
setPreviewUrl(null);
setResult(null);
setError(null);
};
Comment on lines +36 to +47
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

URL.createObjectURL(file) is created for previews but never revoked. This can leak memory if the user uploads multiple images or navigates away. Revoke the previous object URL when replacing/resetting the preview, and revoke on unmount if needed.

Copilot uses AI. Check for mistakes.

const analyzeEmotion = async () => {
if (!image) return;

setIsAnalyzing(true);
setError(null);
setResult(null);

const formData = new FormData();
formData.append('image', image);

try {
const data = await detectorsApi.emotion(formData);
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

detectorsApi.emotion(formData) needs to send multipart/form-data to the backend /api/detect-emotion endpoint. With the current API client patterns in this repo (API base is already /api and file uploads use postForm), a JSON post will not correctly transmit the FormData and can also lead to a doubled /api/api/... path if the detectors API uses a prefixed endpoint. Align the emotion call with the other detectors (use the same upload helper and a non-prefixed endpoint string).

Suggested change
const data = await detectorsApi.emotion(formData);
const data = await detectorsApi.postForm('detect-emotion', formData);

Copilot uses AI. Check for mistakes.
if (data && data.emotions) {
Comment on lines +56 to +61
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Checking emotion endpoint client call:"
rg -n -C3 "emotion\\s*:\\s*async|detect-emotion|postForm\\(|post\\(" frontend/src/api/detectors.js

echo
echo "Checking POST helper behavior:"
rg -n -C4 "post:\\s*async|postForm:\\s*async|JSON.stringify\\(data\\)" frontend/src/api/client.js

Repository: RohanExploit/VishwaGuru

Length of output: 2772


🏁 Script executed:

cd frontend/src/views && head -70 EmotionDetector.jsx | tail -20

Repository: RohanExploit/VishwaGuru

Length of output: 607


Emotion upload path is broken with current API helper.

The emotion endpoint in frontend/src/api/detectors.js (lines 61-65) uses apiClient.post() with FormData, but post() calls JSON.stringify(data) which converts FormData to {}, losing the image file. The fix is to use apiClient.postForm() like other file upload endpoints.

Fix
# frontend/src/api/detectors.js
-    emotion: async (formData) => {
-        return apiClient.post('/api/detect-emotion', formData, {
-            headers: {
-                'Content-Type': 'multipart/form-data',
-            },
-        });
-    },
+    emotion: async (formData) => {
+        return apiClient.postForm('/api/detect-emotion', formData);
+    },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/views/EmotionDetector.jsx` around lines 56 - 61, The emotion
upload is broken because detectorsApi.emotion currently calls
apiClient.post(data) which JSON.stringify's the payload and strips FormData;
update the detectorsApi.emotion implementation to use apiClient.postForm(...)
(the same helper used by other file uploads) instead of apiClient.post, and
ensure you do not force a JSON Content-Type so the FormData boundary is
preserved when sending the image file.

setResult(data);
} else {
setError('Could not detect emotions in the image.');
}
} catch (err) {
console.error("Emotion detection failed:", err);
setError('Failed to analyze the image. Please try again.');
} finally {
setIsAnalyzing(false);
}
};

return (
<div className="max-w-4xl mx-auto px-4 py-8 relative z-10">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="bg-white/80 dark:bg-gray-900/80 backdrop-blur-xl rounded-[2.5rem] p-8 border border-white/20 dark:border-gray-800/50 shadow-2xl relative overflow-hidden"
>
<div className="absolute top-0 right-0 w-64 h-64 bg-pink-500/10 rounded-full blur-[80px] -translate-y-1/2 translate-x-1/2"></div>
<div className="absolute bottom-0 left-0 w-64 h-64 bg-blue-500/10 rounded-full blur-[80px] translate-y-1/2 -translate-x-1/2"></div>

{/* Header */}
<div className="flex items-center gap-4 mb-8 relative z-10">
<button
onClick={onBack}
className="p-3 bg-white dark:bg-gray-800 rounded-2xl shadow-sm hover:scale-105 transition-transform text-gray-600 dark:text-gray-300"
>
<ArrowLeft size={24} />
</button>
Comment on lines +86 to +91
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n -C2 '<button|aria-label|ArrowLeft|RefreshCw' frontend/src/views/EmotionDetector.jsx

Repository: RohanExploit/VishwaGuru

Length of output: 2598


Add accessible names to icon-only controls.

The back button (line 86) and reset button (line 154) are icon-only and lack aria-label attributes for screen reader identification. While the reset button has a title attribute, this is insufficient for accessibility—aria-label is required for proper screen reader support.

Suggested fix
           <button
             onClick={onBack}
+            aria-label="Go back"
             className="p-3 bg-white dark:bg-gray-800 rounded-2xl shadow-sm hover:scale-105 transition-transform text-gray-600 dark:text-gray-300"
           >
...
                   <button
                     onClick={resetDetector}
+                    aria-label="Choose different image"
                     className="absolute top-4 right-4 p-2 bg-black/50 hover:bg-black/70 text-white rounded-full backdrop-blur-sm transition-all"
                     title="Choose different image"
                   >
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<button
onClick={onBack}
className="p-3 bg-white dark:bg-gray-800 rounded-2xl shadow-sm hover:scale-105 transition-transform text-gray-600 dark:text-gray-300"
>
<ArrowLeft size={24} />
</button>
<button
onClick={onBack}
aria-label="Go back"
className="p-3 bg-white dark:bg-gray-800 rounded-2xl shadow-sm hover:scale-105 transition-transform text-gray-600 dark:text-gray-300"
>
<ArrowLeft size={24} />
</button>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/views/EmotionDetector.jsx` around lines 86 - 91, The icon-only
back and reset buttons lack accessible names; add aria-label attributes to both
button elements (the back button with onClick={onBack} containing <ArrowLeft />
and the reset button tied to its reset handler) to provide descriptive labels
(e.g., "Back" and "Reset" or localized equivalents), keep existing title if
desired, and ensure screen readers can identify the controls; update the JSX for
those button elements to include aria-label="Back" and aria-label="Reset" (or
appropriate text).

<div>
<h2 className="text-3xl font-black text-gray-900 dark:text-white flex items-center gap-3">
<span className="p-2 bg-pink-100 dark:bg-pink-900/30 text-pink-600 rounded-xl">
<Smile size={28} />
</span>
Emotion Detector
</h2>
<p className="text-gray-500 dark:text-gray-400 font-medium">Analyze facial expressions to determine current emotional state</p>
</div>
</div>

<div className="grid grid-cols-1 md:grid-cols-2 gap-8 relative z-10">
{/* Left Column: Image Input */}
<div className="space-y-6">
{!previewUrl && !showWebcam ? (
<div className="grid grid-cols-2 gap-4 h-64">
<label className="group cursor-pointer bg-gray-50 dark:bg-gray-800/50 rounded-3xl border-2 border-dashed border-gray-200 dark:border-gray-700 p-6 flex flex-col items-center justify-center gap-4 hover:border-pink-500 dark:hover:border-pink-400 transition-all">
<div className="p-4 bg-white dark:bg-gray-800 rounded-2xl shadow-sm text-gray-400 group-hover:text-pink-500 group-hover:scale-110 transition-all">
<ImageIcon size={32} />
</div>
<span className="font-bold text-gray-700 dark:text-gray-300">Upload Photo</span>
<input type="file" accept="image/*" className="hidden" onChange={handleImageUpload} />
</label>

<button
onClick={() => setShowWebcam(true)}
className="group bg-pink-50 dark:bg-pink-900/20 rounded-3xl border-2 border-dashed border-pink-200 dark:border-pink-800/50 p-6 flex flex-col items-center justify-center gap-4 hover:border-pink-500 transition-all"
>
<div className="p-4 bg-white dark:bg-gray-800 rounded-2xl shadow-sm text-pink-500 group-hover:scale-110 transition-all">
<Camera size={32} />
</div>
<span className="font-bold text-pink-700 dark:text-pink-300">Take Photo</span>
</button>
</div>
) : showWebcam ? (
<div className="relative rounded-3xl overflow-hidden shadow-xl border-4 border-white dark:border-gray-800 bg-black aspect-square md:aspect-auto md:h-80 flex flex-col">
<Webcam
audio={false}
ref={webcamRef}
screenshotFormat="image/jpeg"
videoConstraints={{ facingMode: "user" }}
className="w-full h-full object-cover"
/>
<div className="absolute bottom-0 left-0 right-0 p-4 bg-gradient-to-t from-black/80 to-transparent flex justify-center gap-4">
<button
onClick={() => setShowWebcam(false)}
className="px-6 py-2 bg-gray-800 text-white rounded-full font-bold hover:bg-gray-700 transition"
>
Cancel
</button>
<button
onClick={captureWebcam}
className="px-8 py-2 bg-pink-600 text-white rounded-full font-bold hover:bg-pink-500 transition flex items-center gap-2 shadow-lg shadow-pink-600/30"
>
<Camera size={20} /> Capture
</button>
</div>
</div>
) : (
<div className="space-y-4">
<div className="relative rounded-3xl overflow-hidden shadow-xl border-4 border-white dark:border-gray-800 aspect-square md:aspect-auto md:h-80 bg-gray-100">
{previewUrl && <img src={previewUrl} alt="Preview" className="w-full h-full object-cover" />}

Check failure

Code scanning / CodeQL

DOM text reinterpreted as HTML High

DOM text
is reinterpreted as HTML without escaping meta-characters.

Copilot Autofix

AI 26 days ago

Copilot could not generate an autofix suggestion

Copilot could not generate an autofix suggestion for this alert. Try pushing a new commit or if the problem persists contact support.

<button
onClick={resetDetector}
className="absolute top-4 right-4 p-2 bg-black/50 hover:bg-black/70 text-white rounded-full backdrop-blur-sm transition-all"
title="Choose different image"
>
<RefreshCw size={20} />
</button>
</div>
<button
onClick={analyzeEmotion}
disabled={isAnalyzing}
className="w-full py-4 rounded-2xl bg-gradient-to-r from-pink-600 to-purple-600 text-white font-black text-lg shadow-xl shadow-pink-600/20 hover:scale-[1.02] transition-transform disabled:opacity-50 disabled:hover:scale-100 flex items-center justify-center gap-3"
>
{isAnalyzing ? (
<>
<Loader2 size={24} className="animate-spin" />
Analyzing Face...
</>
) : (
<>
<Smile size={24} />
Detect Emotion
</>
)}
</button>
</div>
)}
</div>

{/* Right Column: Results */}
<div className="bg-gray-50 dark:bg-gray-800/50 rounded-3xl p-6 border border-gray-100 dark:border-gray-700">
<h3 className="text-xl font-black mb-6 flex items-center gap-2">
Analysis Results
</h3>

<AnimatePresence mode="wait">
{isAnalyzing ? (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="flex flex-col items-center justify-center h-48 gap-4 text-pink-600"
>
<Loader2 size={40} className="animate-spin" />
<p className="font-bold animate-pulse">Running neural network...</p>
</motion.div>
) : error ? (
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
className="bg-red-50 text-red-700 p-4 rounded-2xl border border-red-200"
>
<p className="font-bold">{error ? error.toString() : ''}</p>
</motion.div>
) : result ? (
<motion.div
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
className="space-y-6"
>
{result.emotions && result.emotions.length > 0 ? (
<div className="space-y-4">
{result.emotions.map((emotion, index) => (
<div key={index} className="bg-white dark:bg-gray-800 p-4 rounded-2xl shadow-sm border border-gray-100 dark:border-gray-700">
<div className="flex justify-between items-center mb-2">
<span className="font-black text-gray-800 dark:text-gray-200 uppercase tracking-widest text-sm flex items-center gap-2">
{index === 0 && <span className="w-2 h-2 rounded-full bg-pink-500 animate-pulse"></span>}
{emotion.label}
</span>
<span className="font-bold text-pink-600">{(emotion.score * 100).toFixed(1)}%</span>
</div>
<div className="w-full h-2 bg-gray-100 dark:bg-gray-700 rounded-full overflow-hidden">
<motion.div
initial={{ width: 0 }}
animate={{ width: `${emotion.score * 100}%` }}
transition={{ duration: 1, delay: index * 0.2 }}
className={`h-full ${index === 0 ? 'bg-pink-500' : 'bg-pink-300 dark:bg-pink-700'}`}
/>
</div>
</div>
))}
</div>
) : (
<div className="text-center p-8 bg-white dark:bg-gray-800 rounded-2xl border border-gray-100 dark:border-gray-700">
<p className="font-bold text-gray-500">No clear emotions detected</p>
</div>
)}
</motion.div>
) : (
<div className="flex flex-col items-center justify-center h-48 text-gray-400">
<Smile size={48} className="mb-4 opacity-50" />
<p className="font-medium">Upload or take a photo to see results</p>
</div>
)}
</AnimatePresence>
</div>
</div>
</motion.div>
</div>
);
};

export default EmotionDetector;
Loading
Loading