mirror of
https://github.com/jamiepine/voicebox.git
synced 2026-10-03 17:15:19 -07:00
When the user disables auto_refine (LLM polish) in Settings, the Qwen refinement model is no longer required for dictation to arm. Previously, canRecord checked llmReady unconditionally, so useChordSync called disable_hotkey whenever Qwen was not downloaded -- even if the user never intended to use refinement. Changes: - useDictationReadiness: gate llmReady behind autoRefine in canRecord, missing, and the polling predicates so hotkeys arm with just Whisper STT when refinement is off. - DictationReadinessChecklist: hide the LLM row when autoRefine is false -- the checklist now shows only Whisper STT. Closes #753
118 lines
4.3 KiB
TypeScript
118 lines
4.3 KiB
TypeScript
import { useQuery } from '@tanstack/react-query';
|
|
import { useAccessibilityPermission } from '@/components/AccessibilityGate/AccessibilityGate';
|
|
import { useInputMonitoringPermission } from '@/components/InputMonitoringGate/InputMonitoringGate';
|
|
import { apiClient } from '@/lib/api/client';
|
|
import type { ModelReadiness } from '@/lib/api/types';
|
|
import { useCaptureSettings } from '@/lib/hooks/useSettings';
|
|
import { usePlatform } from '@/platform/PlatformContext';
|
|
|
|
const READINESS_POLL_INTERVAL_MS = 5_000;
|
|
|
|
export type ReadinessGate = 'stt' | 'llm' | 'input_monitoring' | 'accessibility';
|
|
|
|
export interface DictationReadiness {
|
|
isLoading: boolean;
|
|
canRecord: boolean;
|
|
allReady: boolean;
|
|
/** Subset of gates that are NOT yet satisfied — what the checklist renders. */
|
|
missing: ReadinessGate[];
|
|
stt: ModelReadiness | undefined;
|
|
llm: ModelReadiness | undefined;
|
|
/** Whether the user has auto-refine (LLM polish) enabled. */
|
|
autoRefine: boolean;
|
|
inputMonitoring: boolean;
|
|
accessibility: boolean;
|
|
refetch: () => void;
|
|
openInputMonitoringSettings: () => Promise<void>;
|
|
openAccessibilitySettings: () => Promise<void>;
|
|
recheckInputMonitoring: () => Promise<boolean>;
|
|
recheckAccessibility: () => Promise<boolean>;
|
|
}
|
|
|
|
/**
|
|
* Single source of truth for dictation readiness.
|
|
*
|
|
* ``canRecord`` covers the gates that must be green before the chord can
|
|
* start recording. ``allReady`` also includes Accessibility, which only gates
|
|
* synthetic paste — dictation still records and lands in Captures without it.
|
|
*
|
|
* Gates:
|
|
* - stt / llm: backend ``/capture/readiness`` (polled, since downloads
|
|
* finish out-of-band — e.g. user kicks off a download in another tab and
|
|
* expects the toggle to auto-unlock when it lands)
|
|
* - input_monitoring / accessibility: macOS TCC checks via Tauri commands
|
|
* (rechecked on window focus by the underlying hooks)
|
|
*
|
|
* Hotkey-enabled is the user's intent toggle and is intentionally *not*
|
|
* a gate here — that's `useChordSync`'s concern.
|
|
*/
|
|
export function useDictationReadiness(): DictationReadiness {
|
|
const platform = usePlatform();
|
|
const isTauri = platform.metadata.isTauri;
|
|
const { settings } = useCaptureSettings();
|
|
const autoRefine = settings?.auto_refine ?? true;
|
|
|
|
const {
|
|
needsPermission: inputMonNeeds,
|
|
recheck: recheckInputMon,
|
|
openSettings: openInputMon,
|
|
} = useInputMonitoringPermission();
|
|
const {
|
|
needsPermission: a11yNeeds,
|
|
recheck: recheckA11y,
|
|
openSettings: openA11y,
|
|
} = useAccessibilityPermission();
|
|
|
|
const { data, isLoading, refetch } = useQuery({
|
|
queryKey: ['capture-readiness'],
|
|
queryFn: () => apiClient.getCaptureReadiness(),
|
|
// Poll only while a required model is still missing/downloading. Once
|
|
// all required models are green the endpoint's answer can't change
|
|
// until the user swaps models in settings, and that path invalidates
|
|
// the query explicitly from useSettings. refetchOnWindowFocus stays
|
|
// gated to the same condition.
|
|
refetchInterval: (query) => {
|
|
const d = query.state.data;
|
|
const allGreen = d && d.stt.ready && (!autoRefine || d.llm.ready);
|
|
return allGreen ? false : READINESS_POLL_INTERVAL_MS;
|
|
},
|
|
refetchOnWindowFocus: (query) => {
|
|
const d = query.state.data;
|
|
return !(d && d.stt.ready && (!autoRefine || d.llm.ready));
|
|
},
|
|
});
|
|
|
|
// On the web build there's no TCC layer — treat both as granted so the
|
|
// checklist doesn't block users who can't even open System Settings.
|
|
const inputMonitoring = isTauri ? !inputMonNeeds : true;
|
|
const accessibility = isTauri ? !a11yNeeds : true;
|
|
const sttReady = data?.stt.ready ?? false;
|
|
const llmReady = data?.llm.ready ?? false;
|
|
|
|
const missing: ReadinessGate[] = [];
|
|
if (!sttReady) missing.push('stt');
|
|
if (autoRefine && !llmReady) missing.push('llm');
|
|
if (!inputMonitoring) missing.push('input_monitoring');
|
|
if (!accessibility) missing.push('accessibility');
|
|
const canRecord = sttReady && (!autoRefine || llmReady) && inputMonitoring;
|
|
|
|
return {
|
|
isLoading,
|
|
canRecord,
|
|
allReady: missing.length === 0,
|
|
missing,
|
|
stt: data?.stt,
|
|
llm: data?.llm,
|
|
autoRefine,
|
|
inputMonitoring,
|
|
accessibility,
|
|
refetch: () => {
|
|
refetch();
|
|
},
|
|
openInputMonitoringSettings: openInputMon,
|
|
openAccessibilitySettings: openA11y,
|
|
recheckInputMonitoring: recheckInputMon,
|
|
recheckAccessibility: recheckA11y,
|
|
};
|
|
}
|