Spaces:
Running
Running
Anish commited on
Commit ·
5603f49
1
Parent(s): a16341c
[UI/UX] Final touches before deployment.
Browse files- backend/app/models/file_model.py +1 -1
- backend/app/models/user_model.py +1 -1
- backend/requirements.txt +0 -0
- frontend/app/dashboard/page.tsx +19 -1
- frontend/app/learn-more/page.tsx +1 -1
- frontend/app/page.tsx +17 -0
- frontend/app/profile/page.tsx +11 -0
- frontend/components/shared/Navbar.tsx +71 -9
- frontend/components/upload/UploadZone.tsx +7 -0
backend/app/models/file_model.py
CHANGED
|
@@ -15,7 +15,7 @@ class File(Base):
|
|
| 15 |
ip_address = Column(String, nullable=True)
|
| 16 |
|
| 17 |
owner_id = Column(Integer, ForeignKey("users.id"))
|
| 18 |
-
created_at = Column(DateTime, default=datetime.now(UTC))
|
| 19 |
|
| 20 |
status = Column(String, default="uploaded")
|
| 21 |
result = Column(String, nullable=True)
|
|
|
|
| 15 |
ip_address = Column(String, nullable=True)
|
| 16 |
|
| 17 |
owner_id = Column(Integer, ForeignKey("users.id"))
|
| 18 |
+
created_at = Column(DateTime, default=lambda: datetime.now(UTC))
|
| 19 |
|
| 20 |
status = Column(String, default="uploaded")
|
| 21 |
result = Column(String, nullable=True)
|
backend/app/models/user_model.py
CHANGED
|
@@ -12,7 +12,7 @@ class User(Base):
|
|
| 12 |
password = Column(String, nullable=True)
|
| 13 |
oauth_provider = Column(String, nullable=True)
|
| 14 |
oauth_sub_id = Column(String, unique=True, nullable=True)
|
| 15 |
-
created_at = Column(DateTime, default=datetime.now(UTC))
|
| 16 |
reset_token_hash = Column(String, nullable=True)
|
| 17 |
reset_token_expire_at = Column(DateTime, nullable=True)
|
| 18 |
is_verified = Column(Boolean, default=False)
|
|
|
|
| 12 |
password = Column(String, nullable=True)
|
| 13 |
oauth_provider = Column(String, nullable=True)
|
| 14 |
oauth_sub_id = Column(String, unique=True, nullable=True)
|
| 15 |
+
created_at = Column(DateTime, default=lambda: datetime.now(UTC))
|
| 16 |
reset_token_hash = Column(String, nullable=True)
|
| 17 |
reset_token_expire_at = Column(DateTime, nullable=True)
|
| 18 |
is_verified = Column(Boolean, default=False)
|
backend/requirements.txt
CHANGED
|
Binary files a/backend/requirements.txt and b/backend/requirements.txt differ
|
|
|
frontend/app/dashboard/page.tsx
CHANGED
|
@@ -96,10 +96,28 @@ export default function DashboardPage() {
|
|
| 96 |
return confB - confA;
|
| 97 |
}
|
| 98 |
});
|
| 99 |
-
|
| 100 |
return result;
|
| 101 |
}, [files, searchQuery, sortBy, resultFilter]);
|
| 102 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
// Cursor handled globally (CustomCursor.tsx)
|
| 104 |
// Local Parallax for Dash Grid
|
| 105 |
useEffect(() => {
|
|
|
|
| 96 |
return confB - confA;
|
| 97 |
}
|
| 98 |
});
|
|
|
|
| 99 |
return result;
|
| 100 |
}, [files, searchQuery, sortBy, resultFilter]);
|
| 101 |
|
| 102 |
+
useEffect(() => {
|
| 103 |
+
const handleDragOver = (e: DragEvent) => {
|
| 104 |
+
e.preventDefault();
|
| 105 |
+
if (!isUploadModalOpen) setIsUploadModalOpen(true);
|
| 106 |
+
};
|
| 107 |
+
const handleEsc = (event: KeyboardEvent) => {
|
| 108 |
+
if (event.key === 'Escape' && isUploadModalOpen) {
|
| 109 |
+
setIsUploadModalOpen(false);
|
| 110 |
+
}
|
| 111 |
+
};
|
| 112 |
+
|
| 113 |
+
window.addEventListener('dragover', handleDragOver);
|
| 114 |
+
document.addEventListener('keydown', handleEsc);
|
| 115 |
+
return () => {
|
| 116 |
+
window.removeEventListener('dragover', handleDragOver);
|
| 117 |
+
document.removeEventListener('keydown', handleEsc);
|
| 118 |
+
}
|
| 119 |
+
}, [isUploadModalOpen]);
|
| 120 |
+
|
| 121 |
// Cursor handled globally (CustomCursor.tsx)
|
| 122 |
// Local Parallax for Dash Grid
|
| 123 |
useEffect(() => {
|
frontend/app/learn-more/page.tsx
CHANGED
|
@@ -75,7 +75,7 @@ export default function LearnMorePage() {
|
|
| 75 |
<div>
|
| 76 |
<h2 className="text-2xl font-bold mb-4">1. Uploading Media</h2>
|
| 77 |
<p className="text-[var(--theme-text)]/70 leading-relaxed text-lg">
|
| 78 |
-
|
| 79 |
</p>
|
| 80 |
</div>
|
| 81 |
</div>
|
|
|
|
| 75 |
<div>
|
| 76 |
<h2 className="text-2xl font-bold mb-4">1. Uploading Media</h2>
|
| 77 |
<p className="text-[var(--theme-text)]/70 leading-relaxed text-lg">
|
| 78 |
+
<strong className="text-[var(--theme-text)]">Drag and Drop</strong> your <strong className="text-[var(--theme-text)]">Media</strong> anywhere, <strong className="text-[var(--theme-text)]">OR</strong> click the <strong className="text-[var(--theme-text)]">Analyze Media</strong> button on the top right of the navigation bar, or from the Landing Page. An upload zone box will appear. You can drag and drop your images or videos (MP4, JPG, PNG) directly into this zone.
|
| 79 |
</p>
|
| 80 |
</div>
|
| 81 |
</div>
|
frontend/app/page.tsx
CHANGED
|
@@ -26,6 +26,23 @@ export default function LandingPage() {
|
|
| 26 |
}
|
| 27 |
}, [searchParams]);
|
| 28 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
// --- Footer Copyright and Dynamic Date ---
|
| 30 |
const [year, setYear] = useState(new Date().getFullYear());
|
| 31 |
|
|
|
|
| 26 |
}
|
| 27 |
}, [searchParams]);
|
| 28 |
|
| 29 |
+
useEffect(() => {
|
| 30 |
+
const handleDragOver = (e: DragEvent) => {
|
| 31 |
+
e.preventDefault();
|
| 32 |
+
if (!isUploadModalOpen) setIsUploadModalOpen(true);
|
| 33 |
+
};
|
| 34 |
+
const handleEsc = (e: KeyboardEvent) => {
|
| 35 |
+
if (e.key === 'Escape' && isUploadModalOpen) setIsUploadModalOpen(false);
|
| 36 |
+
};
|
| 37 |
+
|
| 38 |
+
window.addEventListener('dragover', handleDragOver);
|
| 39 |
+
window.addEventListener('keydown', handleEsc);
|
| 40 |
+
return () => {
|
| 41 |
+
window.removeEventListener('dragover', handleDragOver);
|
| 42 |
+
window.removeEventListener('keydown', handleEsc);
|
| 43 |
+
};
|
| 44 |
+
}, [isUploadModalOpen]);
|
| 45 |
+
|
| 46 |
// --- Footer Copyright and Dynamic Date ---
|
| 47 |
const [year, setYear] = useState(new Date().getFullYear());
|
| 48 |
|
frontend/app/profile/page.tsx
CHANGED
|
@@ -100,6 +100,17 @@ export default function ProfilePage() {
|
|
| 100 |
}
|
| 101 |
};
|
| 102 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
if (loading) return null;
|
| 104 |
|
| 105 |
return (
|
|
|
|
| 100 |
}
|
| 101 |
};
|
| 102 |
|
| 103 |
+
useEffect(() => {
|
| 104 |
+
const handleEsc = (event: KeyboardEvent) => {
|
| 105 |
+
if (event.key === 'Escape' && isDeleteModalOpen) {
|
| 106 |
+
setIsDeleteModalOpen(false);
|
| 107 |
+
setDeleteInput("");
|
| 108 |
+
}
|
| 109 |
+
};
|
| 110 |
+
document.addEventListener('keydown', handleEsc);
|
| 111 |
+
return () => document.removeEventListener('keydown', handleEsc);
|
| 112 |
+
}, [isDeleteModalOpen]);
|
| 113 |
+
|
| 114 |
if (loading) return null;
|
| 115 |
|
| 116 |
return (
|
frontend/components/shared/Navbar.tsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
import React, { useState, useRef, useEffect } from 'react';
|
| 2 |
import { useRouter, usePathname } from 'next/navigation';
|
| 3 |
import { useAuth } from '@/contexts/AuthContext';
|
| 4 |
-
import { Menu, X, LogOut, Camera, Upload, Trash2, Moon, Sun, MessageSquare, UserCircle, LayoutDashboard } from 'lucide-react';
|
| 5 |
import { apiLayer } from '@/lib/api';
|
| 6 |
import FeedbackModal from '@/components/shared/FeedbackModal';
|
| 7 |
|
|
@@ -15,6 +15,40 @@ export default function Navbar({ onAnalyzeClick }: NavbarProps) {
|
|
| 15 |
const { isAuthenticated, user, logout, updateUser } = useAuth();
|
| 16 |
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
| 17 |
const [dropdownOpen, setDropdownOpen] = useState(false);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
|
| 19 |
// Avatar logic
|
| 20 |
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
@@ -34,20 +68,26 @@ export default function Navbar({ onAnalyzeClick }: NavbarProps) {
|
|
| 34 |
}
|
| 35 |
}, [user?.avatar_url, user?.google_avatar_url]);
|
| 36 |
|
| 37 |
-
// Close dropdown on outside click or ESC
|
| 38 |
useEffect(() => {
|
| 39 |
const handleClickOutside = (event: MouseEvent) => {
|
| 40 |
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
| 41 |
setDropdownOpen(false);
|
|
|
|
| 42 |
}
|
| 43 |
};
|
|
|
|
| 44 |
const handleEsc = (event: KeyboardEvent) => {
|
| 45 |
-
if (event.key === 'Escape')
|
|
|
|
|
|
|
|
|
|
| 46 |
};
|
|
|
|
| 47 |
if (dropdownOpen) {
|
| 48 |
document.addEventListener('mousedown', handleClickOutside);
|
| 49 |
document.addEventListener('keydown', handleEsc);
|
| 50 |
}
|
|
|
|
| 51 |
return () => {
|
| 52 |
document.removeEventListener('mousedown', handleClickOutside);
|
| 53 |
document.removeEventListener('keydown', handleEsc);
|
|
@@ -121,10 +161,19 @@ export default function Navbar({ onAnalyzeClick }: NavbarProps) {
|
|
| 121 |
</button>
|
| 122 |
</>
|
| 123 |
) : (
|
| 124 |
-
<div
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 125 |
<div
|
| 126 |
className="flex items-center gap-3 cursor-pointer hover:opacity-80 transition-opacity"
|
| 127 |
-
onClick={() =>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 128 |
>
|
| 129 |
<span className="hidden sm:block text-[var(--theme-text)] opacity-80 text-sm font-mono tracking-tight hover-scale">{user?.name || "User"}</span>
|
| 130 |
<div className="w-10 h-10 rounded-full border border-[var(--theme-border)] overflow-hidden bg-[var(--theme-text)]/5 flex items-center justify-center dash-border hover-scale">
|
|
@@ -137,7 +186,8 @@ export default function Navbar({ onAnalyzeClick }: NavbarProps) {
|
|
| 137 |
</div>
|
| 138 |
|
| 139 |
{dropdownOpen && (
|
| 140 |
-
<div className="absolute right-0 top-full
|
|
|
|
| 141 |
|
| 142 |
<button onClick={() => { setDropdownOpen(false); router.push('/dashboard'); }} className="px-4 py-3 flex items-center gap-3 text-sm text-[var(--theme-text)]/70 hover:text-[var(--theme-text)] hover:bg-[var(--theme-text)]/5 transition-colors text-left !cursor-none">
|
| 143 |
<LayoutDashboard className="w-4 h-4 !cursor-none" /> Dashboard
|
|
@@ -147,9 +197,6 @@ export default function Navbar({ onAnalyzeClick }: NavbarProps) {
|
|
| 147 |
<UserCircle className="w-4 h-4 !cursor-none" /> My Profile
|
| 148 |
</button>
|
| 149 |
|
| 150 |
-
<div className="h-px w-full bg-[var(--theme-border)] my-1"></div>
|
| 151 |
-
|
| 152 |
-
|
| 153 |
{localAvatar && (
|
| 154 |
<button
|
| 155 |
className="px-4 py-3 flex items-center gap-3 text-sm text-yellow-500/80 hover:text-yellow-500 hover:bg-[var(--theme-text)]/5 transition-colors text-left !cursor-none"
|
|
@@ -161,6 +208,20 @@ export default function Navbar({ onAnalyzeClick }: NavbarProps) {
|
|
| 161 |
|
| 162 |
<div className="h-px w-full bg-[var(--theme-border)] my-1"></div>
|
| 163 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 164 |
|
| 165 |
<button
|
| 166 |
className="px-4 py-3 flex items-center gap-3 text-sm text-[var(--theme-text)]/70 hover:text-[var(--theme-text)] hover:bg-[var(--theme-text)]/5 transition-colors text-left !cursor-none"
|
|
@@ -183,6 +244,7 @@ export default function Navbar({ onAnalyzeClick }: NavbarProps) {
|
|
| 183 |
>
|
| 184 |
<LogOut className="w-4 h-4 !cursor-none" /> Logout
|
| 185 |
</button>
|
|
|
|
| 186 |
</div>
|
| 187 |
)}
|
| 188 |
</div>
|
|
|
|
| 1 |
import React, { useState, useRef, useEffect } from 'react';
|
| 2 |
import { useRouter, usePathname } from 'next/navigation';
|
| 3 |
import { useAuth } from '@/contexts/AuthContext';
|
| 4 |
+
import { Menu, X, LogOut, Camera, Upload, Trash2, Moon, Sun, MessageSquare, UserCircle, LayoutDashboard, Volume2, VolumeX } from 'lucide-react';
|
| 5 |
import { apiLayer } from '@/lib/api';
|
| 6 |
import FeedbackModal from '@/components/shared/FeedbackModal';
|
| 7 |
|
|
|
|
| 15 |
const { isAuthenticated, user, logout, updateUser } = useAuth();
|
| 16 |
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
| 17 |
const [dropdownOpen, setDropdownOpen] = useState(false);
|
| 18 |
+
const [dropdownPinned, setDropdownPinned] = useState(false);
|
| 19 |
+
const [soundEnabled, setSoundEnabled] = useState(true);
|
| 20 |
+
const closeTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
| 21 |
+
|
| 22 |
+
const handleMouseEnter = () => {
|
| 23 |
+
if (!dropdownPinned) {
|
| 24 |
+
if (closeTimeoutRef.current) clearTimeout(closeTimeoutRef.current);
|
| 25 |
+
setDropdownOpen(true);
|
| 26 |
+
}
|
| 27 |
+
};
|
| 28 |
+
|
| 29 |
+
const handleMouseLeave = () => {
|
| 30 |
+
if (!dropdownPinned) {
|
| 31 |
+
closeTimeoutRef.current = setTimeout(() => {
|
| 32 |
+
setDropdownOpen(false);
|
| 33 |
+
}, 300); // 300ms grace period
|
| 34 |
+
}
|
| 35 |
+
};
|
| 36 |
+
|
| 37 |
+
useEffect(() => {
|
| 38 |
+
if (typeof window !== 'undefined') {
|
| 39 |
+
const saved = localStorage.getItem('spotix_sound');
|
| 40 |
+
if (saved !== null) {
|
| 41 |
+
setSoundEnabled(saved === 'true');
|
| 42 |
+
}
|
| 43 |
+
}
|
| 44 |
+
}, []);
|
| 45 |
+
|
| 46 |
+
const toggleSound = (e: React.MouseEvent) => {
|
| 47 |
+
e.stopPropagation();
|
| 48 |
+
const newState = !soundEnabled;
|
| 49 |
+
setSoundEnabled(newState);
|
| 50 |
+
localStorage.setItem('spotix_sound', String(newState));
|
| 51 |
+
};
|
| 52 |
|
| 53 |
// Avatar logic
|
| 54 |
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
|
|
| 68 |
}
|
| 69 |
}, [user?.avatar_url, user?.google_avatar_url]);
|
| 70 |
|
|
|
|
| 71 |
useEffect(() => {
|
| 72 |
const handleClickOutside = (event: MouseEvent) => {
|
| 73 |
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
| 74 |
setDropdownOpen(false);
|
| 75 |
+
setDropdownPinned(false);
|
| 76 |
}
|
| 77 |
};
|
| 78 |
+
|
| 79 |
const handleEsc = (event: KeyboardEvent) => {
|
| 80 |
+
if (event.key === 'Escape') {
|
| 81 |
+
setDropdownOpen(false);
|
| 82 |
+
setDropdownPinned(false);
|
| 83 |
+
}
|
| 84 |
};
|
| 85 |
+
|
| 86 |
if (dropdownOpen) {
|
| 87 |
document.addEventListener('mousedown', handleClickOutside);
|
| 88 |
document.addEventListener('keydown', handleEsc);
|
| 89 |
}
|
| 90 |
+
|
| 91 |
return () => {
|
| 92 |
document.removeEventListener('mousedown', handleClickOutside);
|
| 93 |
document.removeEventListener('keydown', handleEsc);
|
|
|
|
| 161 |
</button>
|
| 162 |
</>
|
| 163 |
) : (
|
| 164 |
+
<div
|
| 165 |
+
className="flex items-center gap-3 relative group"
|
| 166 |
+
ref={dropdownRef}
|
| 167 |
+
onMouseEnter={handleMouseEnter}
|
| 168 |
+
onMouseLeave={handleMouseLeave}
|
| 169 |
+
>
|
| 170 |
<div
|
| 171 |
className="flex items-center gap-3 cursor-pointer hover:opacity-80 transition-opacity"
|
| 172 |
+
onClick={() => {
|
| 173 |
+
const newPinned = !dropdownPinned;
|
| 174 |
+
setDropdownPinned(newPinned);
|
| 175 |
+
setDropdownOpen(newPinned);
|
| 176 |
+
}}
|
| 177 |
>
|
| 178 |
<span className="hidden sm:block text-[var(--theme-text)] opacity-80 text-sm font-mono tracking-tight hover-scale">{user?.name || "User"}</span>
|
| 179 |
<div className="w-10 h-10 rounded-full border border-[var(--theme-border)] overflow-hidden bg-[var(--theme-text)]/5 flex items-center justify-center dash-border hover-scale">
|
|
|
|
| 186 |
</div>
|
| 187 |
|
| 188 |
{dropdownOpen && (
|
| 189 |
+
<div className="absolute right-0 top-full pt-3 w-56 flex flex-col z-[100] !cursor-none">
|
| 190 |
+
<div className="bg-[var(--theme-bg)] border border-[var(--theme-border)] rounded-xl py-2 shadow-2xl flex flex-col w-full h-full">
|
| 191 |
|
| 192 |
<button onClick={() => { setDropdownOpen(false); router.push('/dashboard'); }} className="px-4 py-3 flex items-center gap-3 text-sm text-[var(--theme-text)]/70 hover:text-[var(--theme-text)] hover:bg-[var(--theme-text)]/5 transition-colors text-left !cursor-none">
|
| 193 |
<LayoutDashboard className="w-4 h-4 !cursor-none" /> Dashboard
|
|
|
|
| 197 |
<UserCircle className="w-4 h-4 !cursor-none" /> My Profile
|
| 198 |
</button>
|
| 199 |
|
|
|
|
|
|
|
|
|
|
| 200 |
{localAvatar && (
|
| 201 |
<button
|
| 202 |
className="px-4 py-3 flex items-center gap-3 text-sm text-yellow-500/80 hover:text-yellow-500 hover:bg-[var(--theme-text)]/5 transition-colors text-left !cursor-none"
|
|
|
|
| 208 |
|
| 209 |
<div className="h-px w-full bg-[var(--theme-border)] my-1"></div>
|
| 210 |
|
| 211 |
+
<button
|
| 212 |
+
onClick={toggleSound}
|
| 213 |
+
className="px-4 py-3 flex items-center gap-3 text-sm text-[var(--theme-text)]/70 hover:text-[var(--theme-text)] hover:bg-[var(--theme-text)]/5 transition-colors text-left justify-between !cursor-none"
|
| 214 |
+
>
|
| 215 |
+
<div className="flex items-center gap-3">
|
| 216 |
+
{soundEnabled ? <Volume2 className="w-4 h-4 !cursor-none text-green-400" /> : <VolumeX className="w-4 h-4 !cursor-none text-red-400" />}
|
| 217 |
+
Alerts Sound
|
| 218 |
+
</div>
|
| 219 |
+
<span className={`text-[10px] font-bold px-2 py-0.5 rounded-full uppercase tracking-wider ${soundEnabled ? 'bg-green-500/20 text-green-400' : 'bg-red-500/20 text-red-400'}`}>
|
| 220 |
+
{soundEnabled ? 'ON' : 'OFF'}
|
| 221 |
+
</span>
|
| 222 |
+
</button>
|
| 223 |
+
|
| 224 |
+
<div className="h-px w-full bg-[var(--theme-border)] my-1"></div>
|
| 225 |
|
| 226 |
<button
|
| 227 |
className="px-4 py-3 flex items-center gap-3 text-sm text-[var(--theme-text)]/70 hover:text-[var(--theme-text)] hover:bg-[var(--theme-text)]/5 transition-colors text-left !cursor-none"
|
|
|
|
| 244 |
>
|
| 245 |
<LogOut className="w-4 h-4 !cursor-none" /> Logout
|
| 246 |
</button>
|
| 247 |
+
</div>
|
| 248 |
</div>
|
| 249 |
)}
|
| 250 |
</div>
|
frontend/components/upload/UploadZone.tsx
CHANGED
|
@@ -99,6 +99,13 @@ export default function UploadZone({ autoAnalyze = false }: { autoAnalyze?: bool
|
|
| 99 |
conf: fileData.confidence ?? null
|
| 100 |
});
|
| 101 |
setInteractionState('result');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 102 |
}, 400);
|
| 103 |
} else if (fileData.status === 'Failed' || fileData.status === 'error' || fileData.status === 'FAILED') {
|
| 104 |
polling = false;
|
|
|
|
| 99 |
conf: fileData.confidence ?? null
|
| 100 |
});
|
| 101 |
setInteractionState('result');
|
| 102 |
+
|
| 103 |
+
const soundSetting = localStorage.getItem('spotix_sound');
|
| 104 |
+
if (soundSetting !== 'false') {
|
| 105 |
+
const audio = new Audio('/sounds/notif.wav');
|
| 106 |
+
audio.volume = 1.0;
|
| 107 |
+
audio.play().catch(e => console.error("Audio play failed:", e));
|
| 108 |
+
}
|
| 109 |
}, 400);
|
| 110 |
} else if (fileData.status === 'Failed' || fileData.status === 'error' || fileData.status === 'FAILED') {
|
| 111 |
polling = false;
|