Spaces:
Running
Running
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 +10 -2
- backend/app/ai/model_loader.py +2 -2
- backend/app/api/feedback_routes.py +18 -11
- backend/app/schemas/feedback_schema.py +9 -0
- frontend/app/dashboard/page.tsx +1 -1
- frontend/app/login/page.tsx +7 -1
- frontend/app/profile/page.tsx +1 -1
- frontend/app/result/[id]/page.tsx +123 -2
- frontend/components/shared/PageLoader.tsx +3 -8
- frontend/lib/api.ts +1 -1
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) <
|
| 19 |
print("Not enough feedback to retrain")
|
|
|
|
| 20 |
return
|
| 21 |
|
| 22 |
X = []
|
| 23 |
y = []
|
| 24 |
|
| 25 |
-
|
|
|
|
|
|
|
| 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 = "
|
| 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
|
| 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 |
-
|
| 12 |
-
|
| 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 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
|
|
|
| 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 (
|
| 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: {
|
| 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')
|