From 736b7f3d15ec5ce3e7e9d5cd4f953547bf711121 Mon Sep 17 00:00:00 2001 From: ParikhShreya Date: Fri, 21 Nov 2025 20:58:46 +0530 Subject: [PATCH 1/4] Enhance Navbar with error dialog and voice search Added error handling and voice search functionality to the Navbar component. --- .../components/Navigation/Navbar/Navbar.tsx | 290 ++++++++++++++---- 1 file changed, 232 insertions(+), 58 deletions(-) diff --git a/frontend/src/components/Navigation/Navbar/Navbar.tsx b/frontend/src/components/Navigation/Navbar/Navbar.tsx index c565b7f6d..b817d2eb1 100644 --- a/frontend/src/components/Navigation/Navbar/Navbar.tsx +++ b/frontend/src/components/Navigation/Navbar/Navbar.tsx @@ -1,52 +1,201 @@ -import { Input } from '@/components/ui/input'; -import { ThemeSelector } from '@/components/ThemeToggle'; -import { Search } from 'lucide-react'; -import { useDispatch, useSelector } from 'react-redux'; -import { selectAvatar, selectName } from '@/features/onboardingSelectors'; -import { clearSearch } from '@/features/searchSlice'; -import { convertFileSrc } from '@tauri-apps/api/core'; -import { FaceSearchDialog } from '@/components/Dialog/FaceSearchDialog'; +import React, { useState, useEffect, useRef } from "react"; +import { Input } from "@/components/ui/input"; +import { ThemeSelector } from "@/components/ThemeToggle"; +import { Search, Mic } from "lucide-react"; +import { useDispatch, useSelector } from "react-redux"; +import { selectAvatar, selectName } from "@/features/onboardingSelectors"; +import { clearSearch } from "@/features/searchSlice"; +import { convertFileSrc } from "@tauri-apps/api/core"; +import { FaceSearchDialog } from "@/components/Dialog/FaceSearchDialog"; +/* ------------------------------------------------------- + ERROR DIALOG +------------------------------------------------------- */ +const ErrorDialog = ({ message, onClose }: { message: string; onClose: () => void }) => ( +
+
+

Error

+

{message}

+ +
+
+); + +/* ------------------------------------------------------- + NAVBAR +------------------------------------------------------- */ export function Navbar() { const userName = useSelector(selectName); const userAvatar = useSelector(selectAvatar); - const searchState = useSelector((state: any) => state.search); + const dispatch = useDispatch(); + const isSearchActive = searchState.active; const queryImage = searchState.queryImage; - const dispatch = useDispatch(); + const recognitionRef = useRef(null); + + /* ROUTES */ + const routeMap: Record = { + home: "/", + albums: "/albums", + videos: "/videos", + settings: "/settings", + "ai-tagging": "/ai-tagging", + memories: "/memories", + favourites: "/favourites", + }; + + const suggestionKeys = Object.keys(routeMap); + + /* STATES */ + const [query, setQuery] = useState(""); + const [activeIndex, setActiveIndex] = useState(-1); + const [voiceOpen, setVoiceOpen] = useState(false); + const [voiceText, setVoiceText] = useState("Listening..."); + const [error, setError] = useState(null); + + const filtered = + query.length > 0 + ? suggestionKeys.filter((s) => + s.toLowerCase().includes(query.toLowerCase().replace(/\s+/g, "-")) + ) + : []; + + /* NAVIGATION */ + const goToPage = (label: string) => { + dispatch(clearSearch()); // Prevent face search lock + const key = label.trim().toLowerCase().replace(/\s+/g, "-"); + if (routeMap[key]) { + window.location.href = routeMap[key]; + } else { + setError(`The page "${label}" does not exist.`); + } + }; + + /* KEYBOARD SUPPORT */ + const handleKeyDown = (e: React.KeyboardEvent) => { + if (!filtered.length) return; + if (e.key === "ArrowDown") { + e.preventDefault(); + setActiveIndex((prev) => (prev + 1) % filtered.length); + } else if (e.key === "ArrowUp") { + e.preventDefault(); + setActiveIndex((prev) => (prev <= 0 ? filtered.length - 1 : prev - 1)); + } else if (e.key === "Enter") { + if (activeIndex >= 0) { + goToPage(filtered[activeIndex]); + setActiveIndex(-1); + } + } + }; + + useEffect(() => { + setActiveIndex(-1); + }, [query]); + + /* ------------------------------------------------------- + VOICE SEARCH +------------------------------------------------------- */ + const startListening = () => { + const SR = (window as any).SpeechRecognition || (window as any).webkitSpeechRecognition; + if (!SR) { + setError("Speech recognition is not supported in your browser."); + return; + } + + const recog = new SR(); + recognitionRef.current = recog; + + recog.lang = "en-US"; + recog.interimResults = false; + + recog.onstart = () => { + setVoiceOpen(true); + setVoiceText("Listening..."); + }; + + recog.onresult = (e: any) => { + const spoken = e.results[0][0].transcript.toLowerCase().trim(); + setVoiceText(spoken); + + const synonyms: Record = { + favourite: "favourites", + favorites: "favourites", + favorite: "favourites", + favourites: "favourites", + fav: "favourites", + playlist: "favourites", + memory: "memories", + pics: "albums", + photos: "albums", + pictures: "albums", + photo: "albums", + }; + + let cleaned = spoken.replace(/\s+/g, "-"); + Object.keys(synonyms).forEach((k) => { + if (cleaned.includes(k)) cleaned = synonyms[k]; + }); + + const found = suggestionKeys.find((k) => cleaned.includes(k)); + + if (found) { + goToPage(found); + } else { + setError(`No matching page found for "${spoken}".`); + } + + setTimeout(() => setVoiceOpen(false), 1000); + }; + + recog.onerror = () => { + setVoiceText("Try again"); + setTimeout(() => setVoiceOpen(false), 900); + }; + + recog.start(); + }; + + /* UI */ return ( -
- {/* Logo */} -
- - PictoPy Logo - PictoPy - +
+ {/* LEFT */} +
+ PictoPy Logo + PictoPy
- {/* Search Bar */} -
-
- {/* Query Image */} + {/* CENTER */} +
+
+ + setQuery(e.target.value)} + onKeyDown={handleKeyDown} + className="flex-1 border-0 bg-transparent" + /> + + {/* Query image preview */} {queryImage && ( -
+
Query {isSearchActive && ( @@ -54,43 +203,68 @@ export function Navbar() {
)} - {/* Input */} - - - {/* FaceSearch Dialog */} - + {/* MIC */} + + {/* DROPDOWN */} + {filtered.length > 0 && ( +
+ {filtered.map((key, idx) => ( +
goToPage(key)} + > + {key} +
+ ))} +
+ )}
- {/* Right Side */} -
+ {/* RIGHT */} +
-
- - Welcome {userName} - - - User avatar - -
+ + Welcome {userName} + + + User avatar +
+ + {/* error */} + {error && setError(null)} />} + + {/* Voice Modal */} + {voiceOpen && ( +
+
+ +

{voiceText}

+

Listening...

+
+
+ )}
); } + +export default Navbar; From 7af185c9e873e38edf3283f97671edbeb805d945 Mon Sep 17 00:00:00 2001 From: ParikhShreya Date: Sat, 22 Nov 2025 10:09:36 +0530 Subject: [PATCH 2/4] Enhance Navbar with voice command and error handling Added voice command functionality and improved error handling. --- .../components/Navigation/Navbar/Navbar.tsx | 92 ++++++++++--------- 1 file changed, 51 insertions(+), 41 deletions(-) diff --git a/frontend/src/components/Navigation/Navbar/Navbar.tsx b/frontend/src/components/Navigation/Navbar/Navbar.tsx index b817d2eb1..f79a6a402 100644 --- a/frontend/src/components/Navigation/Navbar/Navbar.tsx +++ b/frontend/src/components/Navigation/Navbar/Navbar.tsx @@ -7,7 +7,7 @@ import { selectAvatar, selectName } from "@/features/onboardingSelectors"; import { clearSearch } from "@/features/searchSlice"; import { convertFileSrc } from "@tauri-apps/api/core"; import { FaceSearchDialog } from "@/components/Dialog/FaceSearchDialog"; - +import { VoiceCommand } from "@/components/Dialog/VoiceCommand"; /* ------------------------------------------------------- ERROR DIALOG ------------------------------------------------------- */ @@ -67,10 +67,14 @@ export function Navbar() { ) : []; - /* NAVIGATION */ + /* ------------------------------------------------------- + FIXED: ALWAYS CLEAR SEARCH BEFORE NAVIGATION +------------------------------------------------------- */ const goToPage = (label: string) => { - dispatch(clearSearch()); // Prevent face search lock + dispatch(clearSearch()); // ⭐ CRITICAL FIX ⭐ + const key = label.trim().toLowerCase().replace(/\s+/g, "-"); + if (routeMap[key]) { window.location.href = routeMap[key]; } else { @@ -78,9 +82,10 @@ export function Navbar() { } }; - /* KEYBOARD SUPPORT */ + /* KEYBOARD HANDLING */ const handleKeyDown = (e: React.KeyboardEvent) => { if (!filtered.length) return; + if (e.key === "ArrowDown") { e.preventDefault(); setActiveIndex((prev) => (prev + 1) % filtered.length); @@ -103,7 +108,10 @@ export function Navbar() { VOICE SEARCH ------------------------------------------------------- */ const startListening = () => { - const SR = (window as any).SpeechRecognition || (window as any).webkitSpeechRecognition; + const SR = + (window as any).webkitSpeechRecognition || + (window as any).SpeechRecognition; + if (!SR) { setError("Speech recognition is not supported in your browser."); return; @@ -121,29 +129,24 @@ export function Navbar() { }; recog.onresult = (e: any) => { - const spoken = e.results[0][0].transcript.toLowerCase().trim(); + const spoken = e.results[0][0].transcript.toLowerCase(); setVoiceText(spoken); - const synonyms: Record = { - favourite: "favourites", - favorites: "favourites", - favorite: "favourites", - favourites: "favourites", - fav: "favourites", - playlist: "favourites", - memory: "memories", - pics: "albums", - photos: "albums", - pictures: "albums", - photo: "albums", - }; - - let cleaned = spoken.replace(/\s+/g, "-"); - Object.keys(synonyms).forEach((k) => { - if (cleaned.includes(k)) cleaned = synonyms[k]; - }); - - const found = suggestionKeys.find((k) => cleaned.includes(k)); + // PRIORITY: favourites + if ( + spoken.includes("favourite") || + spoken.includes("favorite") || + spoken.includes("favorites") || + spoken.includes("favourites") + ) { + goToPage("favourites"); + setTimeout(() => setVoiceOpen(false), 900); + return; + } + + const found = suggestionKeys.find((k) => + spoken.replace(/\s+/g, "-").includes(k) + ); if (found) { goToPage(found); @@ -151,18 +154,20 @@ export function Navbar() { setError(`No matching page found for "${spoken}".`); } - setTimeout(() => setVoiceOpen(false), 1000); + setTimeout(() => setVoiceOpen(false), 1200); }; recog.onerror = () => { - setVoiceText("Try again"); - setTimeout(() => setVoiceOpen(false), 900); + setVoiceText("Couldn't understand. Try again."); + setTimeout(() => setVoiceOpen(false), 1200); }; recog.start(); }; - /* UI */ + /* ------------------------------------------------------- + UI +------------------------------------------------------- */ return (
{/* LEFT */} @@ -173,8 +178,9 @@ export function Navbar() { {/* CENTER */}
-
+
+ - {/* Query image preview */} + {/* Query Image Preview */} {queryImage && (
Query {isSearchActive && ( @@ -216,7 +226,7 @@ export function Navbar() { {/* DROPDOWN */} {filtered.length > 0 && ( -
+
{filtered.map((key, idx) => (
- + Welcome {userName} User
- {/* error */} + {/* ERROR */} {error && setError(null)} />} - {/* Voice Modal */} + {/* VOICE MODAL */} {voiceOpen && ( -
-
+
+

{voiceText}

Listening...

From 1b37ae3d9b800d18b9fa058a1af326d3cc7ba72b Mon Sep 17 00:00:00 2001 From: ParikhShreya Date: Sat, 22 Nov 2025 10:10:22 +0530 Subject: [PATCH 3/4] Implement VoiceCommand component for speech recognition --- .../src/components/Dialog/VoiceCommand.tsx | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 frontend/src/components/Dialog/VoiceCommand.tsx diff --git a/frontend/src/components/Dialog/VoiceCommand.tsx b/frontend/src/components/Dialog/VoiceCommand.tsx new file mode 100644 index 000000000..82b9da6cd --- /dev/null +++ b/frontend/src/components/Dialog/VoiceCommand.tsx @@ -0,0 +1,56 @@ +import React, { useState } from "react"; +import { Mic } from "lucide-react"; + +interface Props { + onCommand: (cmd: string) => void; +} + +export function VoiceCommand({ onCommand }: Props) { + const [listening, setListening] = useState(false); + + const startListening = () => { + const SpeechRecognition = + (window as any).SpeechRecognition || + (window as any).webkitSpeechRecognition; + + if (!SpeechRecognition) { + alert("Speech recognition not supported in this browser."); + return; + } + + const recognition = new SpeechRecognition(); + recognition.continuous = false; + recognition.interimResults = false; + recognition.lang = "en-US"; + + recognition.start(); + setListening(true); + + recognition.onresult = (event: any) => { + const spokenText = event.results[0][0].transcript.toLowerCase().trim(); + setListening(false); + + console.log("Recognized:", spokenText); + onCommand(spokenText); // send recognized speech to navbar + }; + + recognition.onerror = () => { + setListening(false); + alert("Try again..."); + }; + }; + + return ( + + ); +} From 7bc639df4ed95b9070797319027403d903d46941 Mon Sep 17 00:00:00 2001 From: ParikhShreya Date: Sat, 22 Nov 2025 10:44:10 +0530 Subject: [PATCH 4/4] Refactor Navbar and ErrorDialog components Refactor ErrorDialog and Navbar components for improved styling and functionality. Adjust voice search handling and cleanup code structure. --- .../components/Navigation/Navbar/Navbar.tsx | 173 ++++++------------ 1 file changed, 57 insertions(+), 116 deletions(-) diff --git a/frontend/src/components/Navigation/Navbar/Navbar.tsx b/frontend/src/components/Navigation/Navbar/Navbar.tsx index f79a6a402..bc97d539c 100644 --- a/frontend/src/components/Navigation/Navbar/Navbar.tsx +++ b/frontend/src/components/Navigation/Navbar/Navbar.tsx @@ -12,16 +12,11 @@ import { VoiceCommand } from "@/components/Dialog/VoiceCommand"; ERROR DIALOG ------------------------------------------------------- */ const ErrorDialog = ({ message, onClose }: { message: string; onClose: () => void }) => ( -
-
+
+
+

Error

{message}

-
); @@ -40,7 +35,6 @@ export function Navbar() { const recognitionRef = useRef(null); - /* ROUTES */ const routeMap: Record = { home: "/", albums: "/albums", @@ -53,7 +47,6 @@ export function Navbar() { const suggestionKeys = Object.keys(routeMap); - /* STATES */ const [query, setQuery] = useState(""); const [activeIndex, setActiveIndex] = useState(-1); const [voiceOpen, setVoiceOpen] = useState(false); @@ -67,12 +60,8 @@ export function Navbar() { ) : []; - /* ------------------------------------------------------- - FIXED: ALWAYS CLEAR SEARCH BEFORE NAVIGATION -------------------------------------------------------- */ const goToPage = (label: string) => { - dispatch(clearSearch()); // ⭐ CRITICAL FIX ⭐ - + dispatch(clearSearch()); const key = label.trim().toLowerCase().replace(/\s+/g, "-"); if (routeMap[key]) { @@ -82,7 +71,6 @@ export function Navbar() { } }; - /* KEYBOARD HANDLING */ const handleKeyDown = (e: React.KeyboardEvent) => { if (!filtered.length) return; @@ -92,55 +80,46 @@ export function Navbar() { } else if (e.key === "ArrowUp") { e.preventDefault(); setActiveIndex((prev) => (prev <= 0 ? filtered.length - 1 : prev - 1)); - } else if (e.key === "Enter") { - if (activeIndex >= 0) { - goToPage(filtered[activeIndex]); - setActiveIndex(-1); - } + } else if (e.key === "Enter" && activeIndex >= 0) { + goToPage(filtered[activeIndex]); + setActiveIndex(-1); } }; - useEffect(() => { - setActiveIndex(-1); - }, [query]); + useEffect(() => setActiveIndex(-1), [query]); - /* ------------------------------------------------------- - VOICE SEARCH -------------------------------------------------------- */ const startListening = () => { + setVoiceOpen(true); + setQuery(""); // HIDE suggestions + setActiveIndex(-1); // RESET dropdown selection + const SR = (window as any).webkitSpeechRecognition || (window as any).SpeechRecognition; if (!SR) { setError("Speech recognition is not supported in your browser."); + setVoiceOpen(false); return; } const recog = new SR(); recognitionRef.current = recog; - recog.lang = "en-US"; recog.interimResults = false; - recog.onstart = () => { - setVoiceOpen(true); - setVoiceText("Listening..."); - }; - recog.onresult = (e: any) => { const spoken = e.results[0][0].transcript.toLowerCase(); setVoiceText(spoken); - // PRIORITY: favourites if ( - spoken.includes("favourite") || spoken.includes("favorite") || + spoken.includes("favourite") || spoken.includes("favorites") || spoken.includes("favourites") ) { goToPage("favourites"); - setTimeout(() => setVoiceOpen(false), 900); + setTimeout(() => setVoiceOpen(false), 800); return; } @@ -148,128 +127,90 @@ export function Navbar() { spoken.replace(/\s+/g, "-").includes(k) ); - if (found) { - goToPage(found); - } else { - setError(`No matching page found for "${spoken}".`); - } - - setTimeout(() => setVoiceOpen(false), 1200); + found ? goToPage(found) : setError(`No matching page found for "${spoken}".`); + setTimeout(() => setVoiceOpen(false), 1000); }; recog.onerror = () => { - setVoiceText("Couldn't understand. Try again."); - setTimeout(() => setVoiceOpen(false), 1200); + setVoiceText("Try again"); + setTimeout(() => setVoiceOpen(false), 800); }; recog.start(); }; - /* ------------------------------------------------------- - UI -------------------------------------------------------- */ return ( -
- {/* LEFT */} -
- PictoPy Logo + + - {/* CENTER */} -
-
+
+
setQuery(e.target.value)} onKeyDown={handleKeyDown} - className="flex-1 border-0 bg-transparent" + className="flex-1 border-0 bg-transparent focus-visible:ring-0" /> - {/* Query Image Preview */} - {queryImage && ( -
- Query - {isSearchActive && ( - - )} -
- )} - - {/* MIC */} - - {/* DROPDOWN */} - {filtered.length > 0 && ( -
- {filtered.map((key, idx) => ( -
goToPage(key)} - > - {key} -
- ))} -
- )}
+ + {/* Suggestions appear ONLY if voice dialog is closed */} + {!voiceOpen && filtered.length > 0 && ( +
+ {filtered.map((key, idx) => ( +
goToPage(key)} + className={`px-4 py-2 cursor-pointer capitalize ${ + idx === activeIndex + ? "bg-purple-200 dark:bg-purple-700" + : "hover:bg-neutral-200 dark:hover:bg-neutral-700" + }`} + > + {key} +
+ ))} +
+ )}
- {/* RIGHT */} -
+
- + Welcome {userName} User
- {/* ERROR */} {error && setError(null)} />} - {/* VOICE MODAL */} {voiceOpen && ( -
-
+
+
+ -

{voiceText}

-

Listening...

+

{voiceText}

)}