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
73 changes: 73 additions & 0 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"@monaco-editor/react": "^4.7.0",
"@tailwindcss/vite": "^4.1.12",
"axios": "^1.11.0",
"face-api.js": "^0.22.2",
"framer-motion": "^12.42.2",
"html2pdf.js": "^0.14.0",
"lucide-react": "^0.542.0",
Expand Down
107 changes: 107 additions & 0 deletions frontend/src/components/VideoSentimentAnalyzer.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import React, { useEffect, useRef, useState } from 'react';
import * as faceapi from 'face-api.js';

const VideoSentimentAnalyzer = ({ onAnalysisComplete }) => {
const videoRef = useRef();
const [isModelLoaded, setIsModelLoaded] = useState(false);
const [expressions, setExpressions] = useState({});
const [isAnalyzing, setIsAnalyzing] = useState(false);

useEffect(() => {
const loadModels = async () => {
try {
await faceapi.nets.tinyFaceDetector.loadFromUri('/models');
await faceapi.nets.faceExpressionNet.loadFromUri('/models');
Comment thread
desireddymohithreddy0925 marked this conversation as resolved.
setIsModelLoaded(true);
} catch (err) {
console.error("Error loading face-api models", err);
}
Comment thread
desireddymohithreddy0925 marked this conversation as resolved.
};
loadModels();
}, []);

const startVideo = () => {
navigator.mediaDevices.getUserMedia({ video: true })
.then((stream) => {
videoRef.current.srcObject = stream;
setIsAnalyzing(true);
})
.catch((err) => console.error(err));
Comment thread
desireddymohithreddy0925 marked this conversation as resolved.
};

const stopVideo = () => {
const stream = videoRef.current?.srcObject;
if (stream) {
stream.getTracks().forEach(track => track.stop());
}
setIsAnalyzing(false);
if (onAnalysisComplete) {
onAnalysisComplete(expressions);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Clear expression state between frames and sessions.

expressions is updated only when detections.length > 0, and startVideo never resets it. If the face leaves the frame or a new session has no detection yet, the UI and onAnalysisComplete(expressions) can report the previous session's last expression. Set {} when no face is detected and when a new stream starts.

Proposed state reset
 .then((stream) => {
   videoRef.current.srcObject = stream;
+  setExpressions({});
   setIsAnalyzing(true);
 ...
-        if (detections.length > 0) {
-          setExpressions(detections[0].expressions);
-        }
+        setExpressions(detections[0]?.expressions ?? {});

Also applies to: 51-52

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/components/VideoSentimentAnalyzer.jsx` at line 39, Update the
expression state handling in the video analysis component: set expressions to {}
when detections.length is zero, and reset it at the start of startVideo before
processing the new stream. Ensure onAnalysisComplete receives the cleared state
when no face is detected and cannot retain the previous session’s expression.

}
};

const handleVideoPlay = () => {
setInterval(async () => {
if (videoRef.current && isAnalyzing) {
const detections = await faceapi.detectAllFaces(
videoRef.current,
new faceapi.TinyFaceDetectorOptions()
).withFaceExpressions();

if (detections.length > 0) {
setExpressions(detections[0].expressions);
}
}
}, 1000);
Comment thread
desireddymohithreddy0925 marked this conversation as resolved.
};

return (
<div className="p-4 bg-gray-900 rounded-xl shadow-lg border border-gray-700">
<h2 className="text-xl font-bold text-white mb-4">AI Behavioral Sentiment Analyzer</h2>

{!isModelLoaded ? (
<p className="text-gray-400">Loading AI Models...</p>
) : (
<div className="flex flex-col items-center">
<div className="relative w-full max-w-md aspect-video bg-black rounded-lg overflow-hidden mb-4">
<video
ref={videoRef}
autoPlay
muted
onPlay={handleVideoPlay}
className="w-full h-full object-cover"
/>
</div>

<div className="flex gap-4 mb-4">
{!isAnalyzing ? (
<button onClick={startVideo} className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-500 transition-colors">
Start Mock Interview
</button>
) : (
<button onClick={stopVideo} className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-500 transition-colors">
End Session
</button>
)}
</div>

{Object.keys(expressions).length > 0 && (
<div className="w-full bg-gray-800 p-4 rounded-lg">
<h3 className="text-white font-semibold mb-2">Real-time Analysis</h3>
<div className="grid grid-cols-2 gap-2 text-sm">
{Object.entries(expressions).map(([exp, val]) => (
<div key={exp} className="flex justify-between text-gray-300">
<span className="capitalize">{exp}</span>
<span>{(val * 100).toFixed(1)}%</span>
</div>
))}
</div>
</div>
)}
</div>
)}
</div>
);
};

export default VideoSentimentAnalyzer;