Anish-530 commited on
Commit
ee2facf
·
1 Parent(s): 60d0281

Fixed loader stuck, Added a new feedback mechanism, and fixed sound upon analysis

Browse files
backend/app/ai/meta_classifier.py CHANGED
@@ -15,14 +15,17 @@ def retrain_from_feedback():
15
 
16
  data = db.query(Feedback).all()
17
 
18
- if len(data) < 5:
19
  print("Not enough feedback to retrain")
 
20
  return
21
 
22
  X = []
23
  y = []
24
 
25
- for row in data:
 
 
26
  X.append([row.freq_score, row.cnn_score])
27
  y.append(1 if row.label == "ai" else 0)
28
 
@@ -35,6 +38,11 @@ def retrain_from_feedback():
35
  joblib.dump(model, get_model_path())
36
 
37
  print("Model retrained on real feedback data")
 
 
 
 
 
38
 
39
  def train_meta_model():
40
  X = np.array([
 
15
 
16
  data = db.query(Feedback).all()
17
 
18
+ if len(data) < 10:
19
  print("Not enough feedback to retrain")
20
+ db.close()
21
  return
22
 
23
  X = []
24
  y = []
25
 
26
+ batch = data[:10]
27
+
28
+ for row in batch:
29
  X.append([row.freq_score, row.cnn_score])
30
  y.append(1 if row.label == "ai" else 0)
31
 
 
38
  joblib.dump(model, get_model_path())
39
 
40
  print("Model retrained on real feedback data")
41
+
42
+ for row in batch:
43
+ db.delete(row)
44
+ db.commit()
45
+ db.close()
46
 
47
  def train_meta_model():
48
  X = np.array([
backend/app/ai/model_loader.py CHANGED
@@ -8,13 +8,13 @@ class ModelLoader:
8
  def __init__(self):
9
  self.model_dir = Path(settings.MODEL_DIR)
10
  self.model_dir.mkdir(parents=True, exist_ok=True)
11
- self.current_version = "v1.0 (Fallback)"
12
 
13
  def get_latest_model_version(self) -> str:
14
  try:
15
  files = [f.name for f in self.model_dir.iterdir() if f.is_file()]
16
  if not files:
17
- logger.warning("No model files found in directory. Using Fallback.")
18
  return self.current_version
19
 
20
  latest_file = sorted(files)[-1]
 
8
  def __init__(self):
9
  self.model_dir = Path(settings.MODEL_DIR)
10
  self.model_dir.mkdir(parents=True, exist_ok=True)
11
+ self.current_version = "meta_model.pkl"
12
 
13
  def get_latest_model_version(self) -> str:
14
  try:
15
  files = [f.name for f in self.model_dir.iterdir() if f.is_file()]
16
  if not files:
17
+ logger.warning("No model files found in directory. Using generated meta_model.")
18
  return self.current_version
19
 
20
  latest_file = sorted(files)[-1]
backend/app/api/feedback_routes.py CHANGED
@@ -1,31 +1,38 @@
1
- from fastapi import APIRouter, Depends
2
  from sqlalchemy.orm import Session
3
  from app.db.session import get_db
4
  from app.models.feedback_model import Feedback
5
  from app.core.auth_dependancy import get_current_user
 
 
6
 
7
  router = APIRouter(prefix="/feedback", tags=["Feedback"])
8
 
9
  @router.post("/")
10
  def submit_feedback(
11
- file_id: int,
12
- label: str,
13
- confidence: float,
14
- freq_score: float,
15
- cnn_score: float,
16
  db: Session = Depends(get_db),
17
  user = Depends(get_current_user)
18
  ):
 
 
 
 
 
19
  fb = Feedback(
20
- file_id=file_id,
21
  user_id=user.id,
22
- label=label,
23
- confidence=confidence,
24
- freq_score=freq_score,
25
- cnn_score=cnn_score
26
  )
27
 
28
  db.add(fb)
29
  db.commit()
 
 
 
30
 
31
  return {"message": "Feedback saved"}
 
1
+ from fastapi import APIRouter, Depends, HTTPException, BackgroundTasks
2
  from sqlalchemy.orm import Session
3
  from app.db.session import get_db
4
  from app.models.feedback_model import Feedback
5
  from app.core.auth_dependancy import get_current_user
6
+ from app.schemas.feedback_schema import FeedbackCreate
7
+ from app.ai.meta_classifier import retrain_from_feedback
8
 
9
  router = APIRouter(prefix="/feedback", tags=["Feedback"])
10
 
11
  @router.post("/")
12
  def submit_feedback(
13
+ payload: FeedbackCreate,
14
+ background_tasks: BackgroundTasks,
 
 
 
15
  db: Session = Depends(get_db),
16
  user = Depends(get_current_user)
17
  ):
18
+ # Check if user already submitted feedback for this file
19
+ existing = db.query(Feedback).filter(Feedback.file_id == payload.file_id, Feedback.user_id == user.id).first()
20
+ if existing:
21
+ raise HTTPException(status_code=400, detail="Feedback already submitted for this file.")
22
+
23
  fb = Feedback(
24
+ file_id=payload.file_id,
25
  user_id=user.id,
26
+ label=payload.label,
27
+ confidence=payload.confidence,
28
+ freq_score=payload.freq_score,
29
+ cnn_score=payload.cnn_score
30
  )
31
 
32
  db.add(fb)
33
  db.commit()
34
+
35
+ # Trigger async retraining check
36
+ background_tasks.add_task(retrain_from_feedback)
37
 
38
  return {"message": "Feedback saved"}
backend/app/schemas/feedback_schema.py ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel
2
+ from typing import Optional
3
+
4
+ class FeedbackCreate(BaseModel):
5
+ file_id: int
6
+ label: str
7
+ confidence: float
8
+ freq_score: float
9
+ cnn_score: float
frontend/app/dashboard/page.tsx CHANGED
@@ -137,7 +137,7 @@ export default function DashboardPage() {
137
 
138
  useEffect(() => {
139
  if (!loading) {
140
- window.dispatchEvent(new Event('app-page-loaded'));
141
  }
142
  }, [loading]);
143
 
 
137
 
138
  useEffect(() => {
139
  if (!loading) {
140
+ (window as any).__PAGE_LOADED = true;
141
  }
142
  }, [loading]);
143
 
frontend/app/login/page.tsx CHANGED
@@ -19,6 +19,7 @@ export default function LoginPage() {
19
 
20
  // OAuth Token Intake
21
  useEffect(() => {
 
22
  const token = new URLSearchParams(window.location.search).get('access_token');
23
  if (token) {
24
  localStorage.setItem('access_token', token);
@@ -90,7 +91,12 @@ export default function LoginPage() {
90
  setLoading(false);
91
  }
92
  } catch (err: any) {
93
- setError(err.response?.data?.detail || "Authentication Failed");
 
 
 
 
 
94
  setLoading(false);
95
  }
96
  };
 
19
 
20
  // OAuth Token Intake
21
  useEffect(() => {
22
+ (window as any).__PAGE_LOADED = true;
23
  const token = new URLSearchParams(window.location.search).get('access_token');
24
  if (token) {
25
  localStorage.setItem('access_token', token);
 
91
  setLoading(false);
92
  }
93
  } catch (err: any) {
94
+ const detail = err.response?.data?.detail;
95
+ if (Array.isArray(detail)) {
96
+ setError(detail[0]?.msg || "Validation Error");
97
+ } else {
98
+ setError(detail || "Authentication Failed");
99
+ }
100
  setLoading(false);
101
  }
102
  };
frontend/app/profile/page.tsx CHANGED
@@ -113,7 +113,7 @@ export default function ProfilePage() {
113
 
114
  useEffect(() => {
115
  if (!loading) {
116
- window.dispatchEvent(new Event('app-page-loaded'));
117
  }
118
  }, [loading]);
119
 
 
113
 
114
  useEffect(() => {
115
  if (!loading) {
116
+ (window as any).__PAGE_LOADED = true;
117
  }
118
  }, [loading]);
119
 
frontend/app/result/[id]/page.tsx CHANGED
@@ -2,7 +2,7 @@
2
  import { useEffect, useState } from "react";
3
  import { useParams, useRouter } from "next/navigation";
4
  import axios from "axios";
5
- import { ArrowLeft, ArrowRight, Network, Activity, Layers, Hash, X } from "lucide-react";
6
  import UploadZone from "@/components/upload/UploadZone";
7
  import Navbar from "@/components/shared/Navbar";
8
  import { useAuth } from "@/contexts/AuthContext";
@@ -39,6 +39,12 @@ export default function ResultPage() {
39
  const [isUploadModalOpen, setIsUploadModalOpen] = useState(false);
40
  const [mediaLoaded, setMediaLoaded] = useState(false);
41
  const [heatmapLoaded, setHeatmapLoaded] = useState(false);
 
 
 
 
 
 
42
 
43
  useEffect(() => {
44
  const handleKeyDown = (e: KeyboardEvent) => {
@@ -115,7 +121,7 @@ export default function ResultPage() {
115
 
116
  useEffect(() => {
117
  if (!loading) {
118
- window.dispatchEvent(new Event('app-page-loaded'));
119
  }
120
  }, [loading]);
121
 
@@ -207,6 +213,33 @@ export default function ResultPage() {
207
  const isVideo = fileData?.type?.startsWith("video/");
208
  const allLoaded = isVideo ? mediaLoaded : (mediaLoaded && (!heatmapUrl || heatmapLoaded));
209
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
210
  return (
211
  <main className="min-h-screen bg-[var(--theme-bg)] text-[var(--theme-text)] selection:bg-[var(--theme-text)]/20 selection:text-[var(--theme-text)] pb-32 !cursor-none relative overflow-x-hidden">
212
  <style dangerouslySetInnerHTML={{__html: `
@@ -514,6 +547,94 @@ export default function ResultPage() {
514
  </AnimatePresence>
515
  </div>
516
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
517
  </>
518
  )}
519
  </main>
 
2
  import { useEffect, useState } from "react";
3
  import { useParams, useRouter } from "next/navigation";
4
  import axios from "axios";
5
+ import { ArrowLeft, ArrowRight, Network, Activity, Layers, Hash, X, ThumbsUp, ThumbsDown } from "lucide-react";
6
  import UploadZone from "@/components/upload/UploadZone";
7
  import Navbar from "@/components/shared/Navbar";
8
  import { useAuth } from "@/contexts/AuthContext";
 
39
  const [isUploadModalOpen, setIsUploadModalOpen] = useState(false);
40
  const [mediaLoaded, setMediaLoaded] = useState(false);
41
  const [heatmapLoaded, setHeatmapLoaded] = useState(false);
42
+
43
+ // Feedback States
44
+ const [showFeedbackPopup, setShowFeedbackPopup] = useState(false);
45
+ const [showFeedbackModal, setShowFeedbackModal] = useState(false);
46
+ const [feedbackSubmitted, setFeedbackSubmitted] = useState(false);
47
+ const [feedbackLoading, setFeedbackLoading] = useState(false);
48
 
49
  useEffect(() => {
50
  const handleKeyDown = (e: KeyboardEvent) => {
 
121
 
122
  useEffect(() => {
123
  if (!loading) {
124
+ (window as any).__PAGE_LOADED = true;
125
  }
126
  }, [loading]);
127
 
 
213
  const isVideo = fileData?.type?.startsWith("video/");
214
  const allLoaded = isVideo ? mediaLoaded : (mediaLoaded && (!heatmapUrl || heatmapLoaded));
215
 
216
+ useEffect(() => {
217
+ if (allLoaded && isAuthenticated && !feedbackSubmitted && verdictStatus) {
218
+ const timer = setTimeout(() => setShowFeedbackPopup(true), 2000);
219
+ return () => clearTimeout(timer);
220
+ }
221
+ }, [allLoaded, isAuthenticated, feedbackSubmitted, verdictStatus]);
222
+
223
+ const submitFeedback = async (selectedLabel: string) => {
224
+ setFeedbackLoading(true);
225
+ try {
226
+ await apiLayer.submitFeedback({
227
+ file_id: parseInt(fileId),
228
+ label: selectedLabel.toLowerCase(),
229
+ confidence: confidenceVal || 0,
230
+ freq_score: freqScore || 0,
231
+ cnn_score: cnnScore || 0
232
+ });
233
+ setFeedbackSubmitted(true);
234
+ setShowFeedbackPopup(false);
235
+ setShowFeedbackModal(false);
236
+ } catch (err: any) {
237
+ console.error("Feedback error", err);
238
+ } finally {
239
+ setFeedbackLoading(false);
240
+ }
241
+ };
242
+
243
  return (
244
  <main className="min-h-screen bg-[var(--theme-bg)] text-[var(--theme-text)] selection:bg-[var(--theme-text)]/20 selection:text-[var(--theme-text)] pb-32 !cursor-none relative overflow-x-hidden">
245
  <style dangerouslySetInnerHTML={{__html: `
 
547
  </AnimatePresence>
548
  </div>
549
  </div>
550
+
551
+ {/* Sticky Floating Feedback Popup */}
552
+ <AnimatePresence>
553
+ {showFeedbackPopup && !showFeedbackModal && !feedbackSubmitted && (
554
+ <motion.div
555
+ initial={{ opacity: 0, x: 50, y: 0 }}
556
+ animate={{ opacity: 1, x: 0, y: 0 }}
557
+ exit={{ opacity: 0, x: 50, scale: 0.95 }}
558
+ className="fixed right-6 bottom-6 z-40 bg-[var(--theme-glass)] backdrop-blur-2xl border border-[var(--theme-border)] shadow-[0_10px_40px_rgba(0,0,0,0.8)] rounded-2xl p-4 md:p-6 w-80 max-w-[calc(100vw-32px)] flex flex-col gap-3 !cursor-none group"
559
+ >
560
+ <button onClick={() => setShowFeedbackPopup(false)} className="absolute top-3 right-3 text-theme-text/40 hover:text-[var(--theme-text)] transition !cursor-none">
561
+ <X className="w-4 h-4" />
562
+ </button>
563
+ <h4 className="text-sm font-bold uppercase tracking-widest text-[var(--theme-text)]">Rate Analysis</h4>
564
+ <p className="text-xs text-[#d0c4bb]/70 leading-relaxed">
565
+ Did the Spotix engine analyze this media correctly? Your feedback improves the core model.
566
+ </p>
567
+ <div className="flex gap-3 mt-2">
568
+ <button
569
+ onClick={() => submitFeedback(verdictStatus)}
570
+ disabled={feedbackLoading}
571
+ className="flex-1 bg-emerald-500/10 hover:bg-emerald-500/20 text-emerald-400 border border-emerald-500/20 rounded-lg py-2 flex justify-center items-center gap-2 transition !cursor-none disabled:opacity-50"
572
+ >
573
+ <ThumbsUp className="w-4 h-4" />
574
+ <span className="text-xs font-bold uppercase tracking-wider">Accurate</span>
575
+ </button>
576
+ <button
577
+ onClick={() => { setShowFeedbackPopup(false); setShowFeedbackModal(true); }}
578
+ disabled={feedbackLoading}
579
+ className="flex-1 bg-rose-500/10 hover:bg-rose-500/20 text-rose-400 border border-rose-500/20 rounded-lg py-2 flex justify-center items-center gap-2 transition !cursor-none disabled:opacity-50"
580
+ >
581
+ <ThumbsDown className="w-4 h-4" />
582
+ <span className="text-xs font-bold uppercase tracking-wider">Incorrect</span>
583
+ </button>
584
+ </div>
585
+ </motion.div>
586
+ )}
587
+ </AnimatePresence>
588
+
589
+ {/* Blurred Thumbs Down Modal */}
590
+ <AnimatePresence>
591
+ {showFeedbackModal && (
592
+ <motion.div
593
+ initial={{ opacity: 0, backdropFilter: 'blur(0px)' }}
594
+ animate={{ opacity: 1, backdropFilter: 'blur(20px)' }}
595
+ exit={{ opacity: 0, backdropFilter: 'blur(0px)' }}
596
+ className="fixed inset-0 z-[100] bg-black/60 flex justify-center items-center p-4 !cursor-none"
597
+ onClick={(e) => { if (e.target === e.currentTarget) setShowFeedbackModal(false); }}
598
+ >
599
+ <motion.div
600
+ initial={{ scale: 0.95, y: 20 }}
601
+ animate={{ scale: 1, y: 0 }}
602
+ exit={{ scale: 0.95, y: 20 }}
603
+ className="w-full max-w-md bg-[var(--theme-bg)] border border-[var(--theme-border)] shadow-[0_0_50px_rgba(0,0,0,0.8)] rounded-3xl p-8 relative flex flex-col items-center text-center"
604
+ >
605
+ <button onClick={() => setShowFeedbackModal(false)} className="absolute top-6 right-6 text-theme-text/40 hover:text-[var(--theme-text)] transition group z-10 !cursor-none">
606
+ <X className="w-6 h-6 group-hover:rotate-90 transition-transform" />
607
+ </button>
608
+
609
+ <div className="w-16 h-16 rounded-full bg-rose-500/10 flex items-center justify-center mb-6">
610
+ <ThumbsDown className="w-8 h-8 text-rose-500" />
611
+ </div>
612
+
613
+ <h3 className="text-xl tracking-widest font-bold mb-3 font-sans uppercase">
614
+ Flag Incorrect Result
615
+ </h3>
616
+
617
+ <p className="text-[#d0c4bb]/70 text-sm leading-relaxed mb-8">
618
+ Select the correct classification below. This data will be used to automatically retrain the Spotix Meta-Classifier.
619
+ </p>
620
+
621
+ <div className="flex flex-col gap-3 w-full">
622
+ {['AI', 'Suspicious', 'Real'].map((lbl) => (
623
+ <button
624
+ key={lbl}
625
+ onClick={() => submitFeedback(lbl)}
626
+ disabled={feedbackLoading}
627
+ className={`w-full py-4 rounded-xl relative overflow-hidden group dash-border transition !cursor-none font-bold tracking-widest text-sm uppercase ${lbl === 'AI' ? 'bg-red-500/10 text-red-400 hover:bg-red-500/20 border border-red-500/20' : lbl === 'Suspicious' ? 'bg-amber-500/10 text-amber-400 hover:bg-amber-500/20 border border-amber-500/20' : 'bg-emerald-500/10 text-emerald-400 hover:bg-emerald-500/20 border border-emerald-500/20'}`}
628
+ >
629
+ It was actually {lbl}
630
+ </button>
631
+ ))}
632
+ </div>
633
+ </motion.div>
634
+ </motion.div>
635
+ )}
636
+ </AnimatePresence>
637
+
638
  </>
639
  )}
640
  </main>
frontend/components/shared/PageLoader.tsx CHANGED
@@ -21,17 +21,13 @@ export default function PageLoader({ children }: { children: React.ReactNode })
21
  setLoadProgress(0);
22
 
23
  let progress = 0;
24
- let isPageActuallyLoaded = false;
 
25
 
26
- const handlePageLoaded = () => {
27
- isPageActuallyLoaded = true;
28
- };
29
- window.addEventListener('app-page-loaded', handlePageLoaded);
30
-
31
  const interval = setInterval(() => {
32
  if (progress < 90) {
33
  progress += Math.random() * 15 + 5;
34
- } else if (isPageActuallyLoaded) {
35
  progress = 100;
36
  }
37
 
@@ -46,7 +42,6 @@ export default function PageLoader({ children }: { children: React.ReactNode })
46
 
47
  return () => {
48
  clearInterval(interval);
49
- window.removeEventListener('app-page-loaded', handlePageLoaded);
50
  };
51
  }, [pathname]);
52
 
 
21
  setLoadProgress(0);
22
 
23
  let progress = 0;
24
+ // Reset the flag to false on path change
25
+ (window as any).__PAGE_LOADED = false;
26
 
 
 
 
 
 
27
  const interval = setInterval(() => {
28
  if (progress < 90) {
29
  progress += Math.random() * 15 + 5;
30
+ } else if ((window as any).__PAGE_LOADED) {
31
  progress = 100;
32
  }
33
 
 
42
 
43
  return () => {
44
  clearInterval(interval);
 
45
  };
46
  }, [pathname]);
47
 
frontend/lib/api.ts CHANGED
@@ -37,7 +37,7 @@ export const apiLayer = {
37
  updateUsername: (username: string) => api.put('/profile/username', { username }),
38
 
39
  // Feedback
40
- submitFeedback: (data: { issue_type: string, message: string }) => api.post('/issues/', data),
41
 
42
  // Account deletion
43
  deleteAccount: () => api.delete('/users/delete')
 
37
  updateUsername: (username: string) => api.put('/profile/username', { username }),
38
 
39
  // Feedback
40
+ submitFeedback: (data: { file_id: number, label: string, confidence: number, freq_score: number, cnn_score: number }) => api.post('/feedback/', data),
41
 
42
  // Account deletion
43
  deleteAccount: () => api.delete('/users/delete')