[eric] voice: context extracted to its own module, provider/overlay runtime import cycle broken

This commit is contained in:
ciregenz
2026-07-28 11:06:53 -07:00
parent e498484409
commit 2a9f1e4bf3
6 changed files with 37 additions and 30 deletions
@@ -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';
@@ -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';
@@ -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. */
@@ -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<number>;
}
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<VoiceContextValue>(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
</VoiceContext.Provider>
);
}
// 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);
}
+1 -1
View File
@@ -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 +
+30
View File
@@ -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<number>;
}
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<VoiceContextValue>(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);
}