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 (
+
+ );
+}
diff --git a/frontend/src/components/Navigation/Navbar/Navbar.tsx b/frontend/src/components/Navigation/Navbar/Navbar.tsx
index c565b7f6d..bc97d539c 100644
--- a/frontend/src/components/Navigation/Navbar/Navbar.tsx
+++ b/frontend/src/components/Navigation/Navbar/Navbar.tsx
@@ -1,96 +1,221 @@
-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";
+import { VoiceCommand } from "@/components/Dialog/VoiceCommand";
+/* -------------------------------------------------------
+ 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);
+
+ const routeMap: Record = {
+ home: "/",
+ albums: "/albums",
+ videos: "/videos",
+ settings: "/settings",
+ "ai-tagging": "/ai-tagging",
+ memories: "/memories",
+ favourites: "/favourites",
+ };
+
+ const suggestionKeys = Object.keys(routeMap);
+
+ 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, "-"))
+ )
+ : [];
+
+ const goToPage = (label: string) => {
+ dispatch(clearSearch());
+ const key = label.trim().toLowerCase().replace(/\s+/g, "-");
+
+ if (routeMap[key]) {
+ window.location.href = routeMap[key];
+ } else {
+ setError(`The page "${label}" does not exist.`);
+ }
+ };
+
+ 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" && activeIndex >= 0) {
+ goToPage(filtered[activeIndex]);
+ setActiveIndex(-1);
+ }
+ };
+
+ useEffect(() => setActiveIndex(-1), [query]);
+
+ 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.onresult = (e: any) => {
+ const spoken = e.results[0][0].transcript.toLowerCase();
+ setVoiceText(spoken);
+
+ if (
+ spoken.includes("favorite") ||
+ spoken.includes("favourite") ||
+ spoken.includes("favorites") ||
+ spoken.includes("favourites")
+ ) {
+ goToPage("favourites");
+ setTimeout(() => setVoiceOpen(false), 800);
+ return;
+ }
+
+ const found = suggestionKeys.find((k) =>
+ spoken.replace(/\s+/g, "-").includes(k)
+ );
+
+ found ? goToPage(found) : setError(`No matching page found for "${spoken}".`);
+ setTimeout(() => setVoiceOpen(false), 1000);
+ };
+
+ recog.onerror = () => {
+ setVoiceText("Try again");
+ setTimeout(() => setVoiceOpen(false), 800);
+ };
+
+ recog.start();
+ };
+
return (
-
- {/* Logo */}
-
+
+
+
+ PictoPy
+
+
+
+
+
- {/* Search Bar */}
-
-
- {/* Query Image */}
- {queryImage && (
-
-

- {isSearchActive && (
-
- )}
-
- )}
-
- {/* Input */}
setQuery(e.target.value)}
+ onKeyDown={handleKeyDown}
+ className="flex-1 border-0 bg-transparent focus-visible:ring-0"
/>
- {/* FaceSearch Dialog */}
-
+
+ {/* 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 Side */}
-
+
-
-
- Welcome {userName}
-
-
-
-
-
+
+ Welcome {userName}
+
+
+
+
+
+ {error &&
setError(null)} />}
+
+ {voiceOpen && (
+
+
+
+
+
{voiceText}
+
+
+ )}
);
}
+
+export default Navbar;