From a56e1082e5c712e6beed6212c69fda020cddedcd Mon Sep 17 00:00:00 2001 From: Alvaro Parker Date: Wed, 7 Oct 2026 09:32:53 -0300 Subject: [PATCH] fix(frontend): make overlays accessible and wake word opt-in --- vocero-api/CHANGELOG.md | 4 + vocero-frontend/src/App.tsx | 150 ++++++------------ vocero-frontend/src/components/Bubbles.tsx | 4 +- vocero-frontend/src/components/Dialog.tsx | 8 +- .../src/components/ReportsPanel.tsx | 54 +++++++ .../src/components/WakeWordControl.test.tsx | 49 ++++++ .../src/components/WakeWordControl.tsx | 32 ++++ vocero-frontend/src/hooks/use-media-query.ts | 15 ++ vocero-frontend/src/hooks/use-wake-word.ts | 15 +- vocero-frontend/src/realtime/controller.ts | 9 ++ vocero-frontend/src/styles.css | 73 +++++---- vocero-frontend/src/test/setup.ts | 10 ++ 12 files changed, 285 insertions(+), 138 deletions(-) create mode 100644 vocero-frontend/src/components/ReportsPanel.tsx create mode 100644 vocero-frontend/src/components/WakeWordControl.test.tsx create mode 100644 vocero-frontend/src/components/WakeWordControl.tsx create mode 100644 vocero-frontend/src/hooks/use-media-query.ts diff --git a/vocero-api/CHANGELOG.md b/vocero-api/CHANGELOG.md index bcf6f1f..6f994e3 100644 --- a/vocero-api/CHANGELOG.md +++ b/vocero-api/CHANGELOG.md @@ -9,6 +9,10 @@ the implementation at the recorded date; current operating instructions live in ### Fixed +- Use native dialogs for startup and mobile reports, restore focus on dismissal, + and make duplicate transcript bubbles inert. Require explicit wake-word opt-in, + show when it is listening, and turn it off when the conversation stops. + - Isolate microphone meter subscriptions from the application tree. Apply draft settings only on Save, preserve explicitly live noise calibration, and persist/send only relevant changes. diff --git a/vocero-frontend/src/App.tsx b/vocero-frontend/src/App.tsx index 0b5cc38..4333978 100644 --- a/vocero-frontend/src/App.tsx +++ b/vocero-frontend/src/App.tsx @@ -1,13 +1,15 @@ -import { BookOpen, ChartNoAxesColumn, ChevronRight, Info, PanelLeft, Settings, Wrench, X } from "lucide-react"; +import { BookOpen, Info, PanelLeft, Settings, Wrench, X } from "lucide-react"; import { useEffect, useLayoutEffect, useState, useSyncExternalStore } from "react"; import { Bubbles } from "./components/Bubbles"; import { Conversation } from "./components/Conversation"; +import { Dialog } from "./components/Dialog"; +import { ReportsPanel } from "./components/ReportsPanel"; import { AboutDialog, ProfileDialog, RagDialog, SettingsDialog, ToolsDialog } from "./components/SettingsDialogs"; import { VoiceStage } from "./components/VoiceStage"; -import { useWakeWord } from "./hooks/use-wake-word"; +import { WakeWordControl } from "./components/WakeWordControl"; +import { useMediaQuery } from "./hooks/use-media-query"; import { initials, profileName } from "./lib/context"; -import { reportSuggestions } from "./lib/reports"; import { isDebug } from "./lib/settings"; import { ACTIVE_STATES, VoiceController } from "./realtime/controller"; @@ -17,6 +19,7 @@ export default function App() { const state = useSyncExternalStore(controller.subscribe, controller.getSnapshot); const [modal, setModal] = useState(null); const [reportsOpen, setReportsOpen] = useState(false); + const desktop = useMediaQuery("(min-width: 1024px)"); const debug = isDebug(); const white = state.config.theme === "white"; const name = profileName(state.profile); @@ -51,21 +54,6 @@ export default function App() { clearTimeout(timer); }; }, [ready, controller]); - useWakeWord(ready && !ACTIVE_STATES.has(state.status) && !modal && !state.unlock, controller); - useEffect(() => { - if (!reportsOpen) { - return; - } - const escape = (event: KeyboardEvent) => { - if (event.key === "Escape") { - setReportsOpen(false); - } - }; - window.addEventListener("keydown", escape); - return () => { - window.removeEventListener("keydown", escape); - }; - }, [reportsOpen]); const close = () => { setModal(null); }; @@ -98,20 +86,36 @@ export default function App() { return ( <> {state.unlock && ( - +

+ Activa el micrófono para hablar con Vocero. Puedes detener la conversación en cualquier momento. +

+
+ + +
+ )}
@@ -141,7 +145,7 @@ export default function App() { {initials(name)} )} - {hasReports && ( + {hasReports && !desktop && ( -
- )} {state.error && (
{state.error} @@ -246,60 +229,21 @@ export default function App() {
)} + {ready && !ACTIVE_STATES.has(state.status) && !modal && !state.unlock && !reportsOpen && ( + + )} {!white && ACTIVE_STATES.has(state.status) && } - {hasReports && ( - + {hasReports && state.reports && ( + { + setReportsOpen(false); + }} + /> )} {modal === "settings" && } {modal === "tools" && } diff --git a/vocero-frontend/src/components/Bubbles.tsx b/vocero-frontend/src/components/Bubbles.tsx index a08a7cb..2154406 100644 --- a/vocero-frontend/src/components/Bubbles.tsx +++ b/vocero-frontend/src/components/Bubbles.tsx @@ -15,7 +15,7 @@ function Bubble({ message, userName }: { message: Message; userName: string }) { clearTimeout(timer); }; }, [message.text, message.partial]); - if (expiredText === message.text) { + if (!message.partial && expiredText === message.text) { return null; } return ( @@ -36,7 +36,7 @@ function Bubble({ message, userName }: { message: Message; userName: string }) { } export const Bubbles = memo(function Bubbles({ messages, userName }: { messages: Message[]; userName: string }) { return ( -