From 2a9f1e4bf35149746cf2e85b388f568c6592da81 Mon Sep 17 00:00:00 2001 From: ciregenz Date: Tue, 28 Jul 2026 11:06:53 -0700 Subject: [PATCH] [eric] voice: context extracted to its own module, provider/overlay runtime import cycle broken --- .../ChatInput/toolbar/ToolbarActions.tsx | 2 +- .../Dashboard/desktop/DesktopSpawnPill.tsx | 2 +- .../app/pages/Dashboard/desktop/HelpPill.tsx | 2 +- .../shared/voice/VoiceDictationContext.tsx | 29 ++---------------- frontend/src/shared/voice/VoiceOverlay.tsx | 2 +- frontend/src/shared/voice/voiceContext.ts | 30 +++++++++++++++++++ 6 files changed, 37 insertions(+), 30 deletions(-) create mode 100644 frontend/src/shared/voice/voiceContext.ts diff --git a/frontend/src/app/pages/AgentChat/ChatInput/toolbar/ToolbarActions.tsx b/frontend/src/app/pages/AgentChat/ChatInput/toolbar/ToolbarActions.tsx index cca72991..b50f3cc2 100644 --- a/frontend/src/app/pages/AgentChat/ChatInput/toolbar/ToolbarActions.tsx +++ b/frontend/src/app/pages/AgentChat/ChatInput/toolbar/ToolbarActions.tsx @@ -15,7 +15,7 @@ import DescriptionOutlinedIcon from '@mui/icons-material/DescriptionOutlined'; import { useAppDispatch, useAppSelector } from '@/shared/hooks'; import { fetchSkills } from '@/shared/state/skillsSlice'; import { openSettingsModal } from '@/shared/state/settingsSlice'; -import { useVoice } from '@/shared/voice/VoiceDictationContext'; +import { useVoice } from '@/shared/voice/voiceContext'; import { useElementSelection } from '@/app/components/editor/ElementSelectionContext'; import { ClaudeTokens } from '@/shared/styles/claudeTokens'; import { ComposerPlusMenu, ActiveTogglePills, PlusMenuItem } from './ComposerPlusMenu'; diff --git a/frontend/src/app/pages/Dashboard/desktop/DesktopSpawnPill.tsx b/frontend/src/app/pages/Dashboard/desktop/DesktopSpawnPill.tsx index f6afb222..cac473c4 100644 --- a/frontend/src/app/pages/Dashboard/desktop/DesktopSpawnPill.tsx +++ b/frontend/src/app/pages/Dashboard/desktop/DesktopSpawnPill.tsx @@ -6,7 +6,7 @@ import AddRounded from '@mui/icons-material/AddRounded'; import MicNoneOutlinedIcon from '@mui/icons-material/MicNoneOutlined'; import MicIcon from '@mui/icons-material/Mic'; import CircularProgress from '@mui/material/CircularProgress'; -import { useVoice } from '@/shared/voice/VoiceDictationContext'; +import { useVoice } from '@/shared/voice/voiceContext'; import GridViewRoundedIcon from '@mui/icons-material/GridViewRounded'; import StickyNote2OutlinedIcon from '@mui/icons-material/StickyNote2Outlined'; import HistoryRoundedIcon from '@mui/icons-material/HistoryRounded'; diff --git a/frontend/src/app/pages/Dashboard/desktop/HelpPill.tsx b/frontend/src/app/pages/Dashboard/desktop/HelpPill.tsx index 6d6cbd07..73bacef2 100644 --- a/frontend/src/app/pages/Dashboard/desktop/HelpPill.tsx +++ b/frontend/src/app/pages/Dashboard/desktop/HelpPill.tsx @@ -5,7 +5,7 @@ import Tooltip from '@mui/material/Tooltip'; import CircularProgress from '@mui/material/CircularProgress'; import MicNoneOutlinedIcon from '@mui/icons-material/MicNoneOutlined'; import MicIcon from '@mui/icons-material/Mic'; -import { useVoice } from '@/shared/voice/VoiceDictationContext'; +import { useVoice } from '@/shared/voice/voiceContext'; import HelpPanel from './HelpPanel'; /** Top-right desktop pill: Help opens the help panel (ask, report a bug, docs); the mic dictates (local whisper) into the focused field. */ diff --git a/frontend/src/shared/voice/VoiceDictationContext.tsx b/frontend/src/shared/voice/VoiceDictationContext.tsx index 14d2dd89..95acdc84 100644 --- a/frontend/src/shared/voice/VoiceDictationContext.tsx +++ b/frontend/src/shared/voice/VoiceDictationContext.tsx @@ -1,29 +1,12 @@ -import React, { createContext, useCallback, useContext, useEffect, useRef } from 'react'; +import React, { useCallback, useEffect, useRef } from 'react'; import { useAppSelector } from '@/shared/hooks'; -import { useVoiceDictation, VoiceState, VoiceFeedback } from './useVoiceDictation'; +import { useVoiceDictation } from './useVoiceDictation'; +import { VoiceContext } from './voiceContext'; import VoiceOverlay from './VoiceOverlay'; // One recorder for the whole app. Both mics (the Help pill and the spawn composer) plus the global // hotkey drive the SAME dictation session, so two mics can't fight over the microphone or show // out-of-sync state. Mounted once near the app root. -interface VoiceContextValue { - state: VoiceState; - lastText: string; - error: string | null; - pct: number; - feedback: VoiceFeedback | null; - toggle: () => void; - // Mic-button press semantics that respect the hold/toggle setting: press starts (or toggles), - // release stops only in hold mode. Buttons wire onPointerDown/Up to these and stay mode-agnostic. - pressStart: () => void; - pressEnd: () => void; - holdMode: boolean; - volumeRef: React.MutableRefObject; -} - -const NOOP_REF = { current: 0 }; -const NOOP: VoiceContextValue = { state: 'idle', lastText: '', error: null, pct: 0, feedback: null, toggle: () => {}, pressStart: () => {}, pressEnd: () => {}, holdMode: true, volumeRef: NOOP_REF }; -const VoiceContext = createContext(NOOP); export function VoiceDictationProvider({ children }: { children: React.ReactNode }): React.ReactElement { const { state, lastText, error, pct, feedback, toggle, start, stop, volumeRef } = useVoiceDictation(); @@ -67,9 +50,3 @@ export function VoiceDictationProvider({ children }: { children: React.ReactNode ); } - -// A component rendered outside the provider (or a web build with no Electron bridge) gets the no-op, -// so mics still render and just do nothing rather than crashing. -export function useVoice(): VoiceContextValue { - return useContext(VoiceContext); -} diff --git a/frontend/src/shared/voice/VoiceOverlay.tsx b/frontend/src/shared/voice/VoiceOverlay.tsx index 433162cd..2fef3f29 100644 --- a/frontend/src/shared/voice/VoiceOverlay.tsx +++ b/frontend/src/shared/voice/VoiceOverlay.tsx @@ -6,7 +6,7 @@ import CheckRoundedIcon from '@mui/icons-material/CheckRounded'; import ContentPasteRoundedIcon from '@mui/icons-material/ContentPasteRounded'; import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined'; import { useThemeAccent } from '@/shared/styles/ThemeContext'; -import { useVoice } from './VoiceDictationContext'; +import { useVoice } from './voiceContext'; // WhisperFlow-style presence: while the mic is hot, an accent-tinted aurora breathes up from the // bottom edge, its height and glow riding the live mic level. Imperative rAF writes only (opacity + diff --git a/frontend/src/shared/voice/voiceContext.ts b/frontend/src/shared/voice/voiceContext.ts new file mode 100644 index 00000000..47cc6458 --- /dev/null +++ b/frontend/src/shared/voice/voiceContext.ts @@ -0,0 +1,30 @@ +import React, { createContext, useContext } from 'react'; +import { VoiceState, VoiceFeedback } from './useVoiceDictation'; + +// The context lives below both the provider and the overlay so neither imports the other +// (VoiceDictationContext renders VoiceOverlay; both reach down here instead of sideways). +export interface VoiceContextValue { + state: VoiceState; + lastText: string; + error: string | null; + pct: number; + feedback: VoiceFeedback | null; + toggle: () => void; + // Mic-button press semantics that respect the hold/toggle setting: press starts (or toggles), + // release stops only in hold mode. Buttons wire onPointerDown/Up to these and stay mode-agnostic. + pressStart: () => void; + pressEnd: () => void; + holdMode: boolean; + volumeRef: React.MutableRefObject; +} + +const NOOP_REF = { current: 0 }; +const NOOP: VoiceContextValue = { state: 'idle', lastText: '', error: null, pct: 0, feedback: null, toggle: () => {}, pressStart: () => {}, pressEnd: () => {}, holdMode: true, volumeRef: NOOP_REF }; + +export const VoiceContext = createContext(NOOP); + +// A component rendered outside the provider (or a web build with no Electron bridge) gets the no-op, +// so mics still render and just do nothing rather than crashing. +export function useVoice(): VoiceContextValue { + return useContext(VoiceContext); +}