@@ -33,6 +33,11 @@ export function RecordingBar() {
3333 const [ audioSource , setAudioSource ] = useState < 'mic' | 'system' | 'both' > ( 'mic' ) ;
3434 const [ barHeights , setBarHeights ] = useState ( [ '4px' , '4px' , '4px' ] ) ;
3535 const sttProviderRef = useRef ( 'nvidia' ) ;
36+ // Reactive copy of the saved STT provider for UI gating. Cabin translation
37+ // is Nvidia NMT (cloud) — there is no offline translate model — so when the
38+ // user picks Local (offline) we disable the toggle entirely.
39+ const [ sttProvider , setSttProvider ] = useState ( 'nvidia' ) ;
40+ const cabinDisabled = sttProvider === 'local' ;
3641 const { showToast } = useToast ( ) ;
3742
3843 // AI is OPTIONAL — `aiConfigured` (resolved in App.tsx, shared via store)
@@ -85,6 +90,26 @@ export function RecordingBar() {
8590 setBarHeightsFn . current = setBarHeights ;
8691 } , [ setBarHeightsFn ] ) ;
8792
93+ // Keep the reactive STT provider in sync with saved settings: fetch on
94+ // mount and whenever Settings is saved (SettingsPanel dispatches the event).
95+ useEffect ( ( ) => {
96+ let alive = true ;
97+ const refresh = ( ) => {
98+ getSettings ( )
99+ . then ( ( s ) => { if ( alive ) setSttProvider ( ( s . stt_provider || 'nvidia' ) . toLowerCase ( ) ) ; } )
100+ . catch ( ( ) => { } ) ;
101+ } ;
102+ refresh ( ) ;
103+ window . addEventListener ( 'scribble:settings-saved' , refresh ) ;
104+ return ( ) => { alive = false ; window . removeEventListener ( 'scribble:settings-saved' , refresh ) ; } ;
105+ } , [ ] ) ;
106+
107+ // Local (offline) has no translation model — force cabin translation off so
108+ // no stale TRANSLATE command/lang is sent on the local WS.
109+ useEffect ( ( ) => {
110+ if ( cabinDisabled && translationEnabled ) setTranslationEnabled ( false ) ;
111+ } , [ cabinDisabled , translationEnabled , setTranslationEnabled ] ) ;
112+
88113 // ── Mid-session translation toggle ──
89114 const translationToggleRef = useRef ( { enabled : translationEnabled , lang : translationLang } ) ;
90115 useEffect ( ( ) => {
@@ -806,18 +831,27 @@ export function RecordingBar() {
806831 />
807832 </ div >
808833 < div className = "bar-divider" />
809- < div className = "translation-toggle-wrap" >
834+ < div
835+ className = "translation-toggle-wrap"
836+ title = { cabinDisabled
837+ ? ( lang === 'vi'
838+ ? 'Dịch cabin cần Nvidia (chưa hỗ trợ dịch offline) — không khả dụng ở chế độ Local'
839+ : 'Cabin translation needs Nvidia (no offline translate model) — unavailable in Local mode' )
840+ : undefined }
841+ style = { cabinDisabled ? { opacity : 0.45 } : undefined }
842+ >
810843 < svg width = "16" height = "16" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" strokeWidth = "2" strokeLinecap = "round" strokeLinejoin = "round" >
811844 < path d = "m5 8 6 6" /> < path d = "m4 14 6-6 2-3" /> < path d = "M2 5h12" /> < path d = "M7 2h1" /> < path d = "m22 22-5-10-5 10" /> < path d = "M14 18h6" />
812845 </ svg >
813846 < span > { lang === 'vi' ? 'Dịch cabin' : 'Translate' } </ span >
814847 < label className = "toggle toggle-sm" >
815- < input type = "checkbox" checked = { translationEnabled } onChange = { ( e ) => setTranslationEnabled ( e . target . checked ) } />
848+ < input type = "checkbox" checked = { translationEnabled && ! cabinDisabled } disabled = { cabinDisabled }
849+ onChange = { ( e ) => setTranslationEnabled ( e . target . checked ) } />
816850 < span className = "toggle-slider" />
817851 </ label >
818852 </ div >
819853 < CustomSelect className = "translation-lang-select" value = { translationLang }
820- onChange = { setTranslationLang } disabled = { ! translationEnabled }
854+ onChange = { setTranslationLang } disabled = { ! translationEnabled || cabinDisabled }
821855 options = { [
822856 { value : 'vi' , label : 'Vietnamese' } , { value : 'en' , label : 'English' } ,
823857 { value : 'ja' , label : 'Japanese' } , { value : 'ko' , label : 'Korean' } ,
0 commit comments