[object Object]

← back to Dear Bubbe Nextjs

refactor(bubbe): typed SpeechRecognition shim + drop dead window flag

f3aff3500bbb72ac27ac8e84697afd8f14dab572 · 2026-07-16 18:01:26 -0700 · Steve Abrams

- new types/speech.d.ts: WebSpeechRecognition interface + Window augmentation
  (distinct name to avoid clobbering lib.dom); kills the (window as any) casts
  and the untyped recognitionRef in both components
- removed MobileBubbe's vestigial __bubbeAutoStarted window flag (gated nothing;
  mobile uses the welcome-screen buttons, not auto-start)
- kept DesktopBubbe's __bubbeDesktopAutoStarted (legit StrictMode remount guard)
- DTD verdict B (incremental-safe); deferred the speech-setup extraction — mic
  lifecycle can't be verified headless, needs a real-device test

Verified: build clean, homepage 200, chat + console clean (WebKit).

Files touched

Diff

commit f3aff3500bbb72ac27ac8e84697afd8f14dab572
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu Jul 16 18:01:26 2026 -0700

    refactor(bubbe): typed SpeechRecognition shim + drop dead window flag
    
    - new types/speech.d.ts: WebSpeechRecognition interface + Window augmentation
      (distinct name to avoid clobbering lib.dom); kills the (window as any) casts
      and the untyped recognitionRef in both components
    - removed MobileBubbe's vestigial __bubbeAutoStarted window flag (gated nothing;
      mobile uses the welcome-screen buttons, not auto-start)
    - kept DesktopBubbe's __bubbeDesktopAutoStarted (legit StrictMode remount guard)
    - DTD verdict B (incremental-safe); deferred the speech-setup extraction — mic
      lifecycle can't be verified headless, needs a real-device test
    
    Verified: build clean, homepage 200, chat + console clean (WebKit).
---
 components/DesktopBubbe.tsx |  5 +++--
 components/MobileBubbe.tsx  | 11 +++--------
 types/speech.d.ts           | 24 ++++++++++++++++++++++++
 3 files changed, 30 insertions(+), 10 deletions(-)

diff --git a/components/DesktopBubbe.tsx b/components/DesktopBubbe.tsx
index 33b0195..5d7f616 100644
--- a/components/DesktopBubbe.tsx
+++ b/components/DesktopBubbe.tsx
@@ -14,7 +14,7 @@ export default function DesktopBubbe() {
   const [hasStarted, setHasStarted] = useState(false)
   const [voiceEnabled, setVoiceEnabled] = useState(true)
   const messagesEndRef = useRef<HTMLDivElement>(null)
-  const recognitionRef = useRef<any>(null)
+  const recognitionRef = useRef<WebSpeechRecognition | null>(null)
   const audioRef = useRef<HTMLAudioElement | null>(null)
   const isSpeakingRef = useRef(false) // synchronous lock so voices never overlap
   const isAutoStartedRef = useRef(false)
@@ -26,7 +26,8 @@ export default function DesktopBubbe() {
   useEffect(() => {
     // Initialize speech recognition
     if (typeof window !== 'undefined' && ('webkitSpeechRecognition' in window || 'SpeechRecognition' in window)) {
-      const SpeechRecognition = (window as any).webkitSpeechRecognition || (window as any).SpeechRecognition
+      const SpeechRecognition = window.webkitSpeechRecognition || window.SpeechRecognition
+      if (!SpeechRecognition) return
       try {
         recognitionRef.current = new SpeechRecognition()
         recognitionRef.current.continuous = false
diff --git a/components/MobileBubbe.tsx b/components/MobileBubbe.tsx
index 7d541cc..cdbecf4 100644
--- a/components/MobileBubbe.tsx
+++ b/components/MobileBubbe.tsx
@@ -17,7 +17,7 @@ export default function MobileBubbe() {
   const [showListeningUI, setShowListeningUI] = useState(false)
   const [transitionTimer, setTransitionTimer] = useState<NodeJS.Timeout | null>(null)
   const messagesEndRef = useRef<HTMLDivElement>(null)
-  const recognitionRef = useRef<any>(null)
+  const recognitionRef = useRef<WebSpeechRecognition | null>(null)
   const audioRef = useRef<HTMLAudioElement | null>(null)
   const isSpeakingRef = useRef(false) // synchronous lock so voices never overlap
   const isAutoStartedRef = useRef(false)
@@ -29,7 +29,8 @@ export default function MobileBubbe() {
   useEffect(() => {
     // Initialize speech recognition
     if (typeof window !== 'undefined' && ('webkitSpeechRecognition' in window || 'SpeechRecognition' in window)) {
-      const SpeechRecognition = (window as any).webkitSpeechRecognition || (window as any).SpeechRecognition
+      const SpeechRecognition = window.webkitSpeechRecognition || window.SpeechRecognition
+      if (!SpeechRecognition) return
       try {
         recognitionRef.current = new SpeechRecognition()
         recognitionRef.current.continuous = false
@@ -83,12 +84,6 @@ export default function MobileBubbe() {
   }, [mode, hasStarted, isSpeaking, isLoading])
 
   // Don't auto-start - wait for user interaction
-  useEffect(() => {
-    // Reset global flag on component mount
-    if ((window as any).__bubbeAutoStarted) {
-      (window as any).__bubbeAutoStarted = false
-    }
-  }, [])
 
   // Voice playback (stop-before-play + overlap lock) lives in lib/bubbeChat.
   const voicePlayer = makeVoicePlayer({ audioRef, isSpeakingRef, setIsSpeaking })
diff --git a/types/speech.d.ts b/types/speech.d.ts
new file mode 100644
index 0000000..b453b1f
--- /dev/null
+++ b/types/speech.d.ts
@@ -0,0 +1,24 @@
+// Minimal Web Speech API typings. lib.dom doesn't ship the webkit-prefixed
+// constructor, and it's the reason both chat components fell back to
+// `(window as any)`. A distinct type name (WebSpeechRecognition) avoids
+// clobbering any lib.dom `SpeechRecognition` declaration; the Window fields
+// are additive (interface merge).
+
+interface WebSpeechRecognition extends EventTarget {
+  lang: string
+  continuous: boolean
+  interimResults: boolean
+  maxAlternatives: number
+  start(): void
+  stop(): void
+  abort(): void
+  onresult: ((e: { resultIndex: number; results: { length: number; [i: number]: { isFinal: boolean; [i: number]: { transcript: string } } } }) => void) | null
+  onerror: ((e: { error: string; message?: string }) => void) | null
+  onend: (() => void) | null
+  onstart: (() => void) | null
+}
+
+interface Window {
+  webkitSpeechRecognition?: { new (): WebSpeechRecognition }
+  SpeechRecognition?: { new (): WebSpeechRecognition }
+}

← 6ac4af5 debug+refactor(bubbe): fix social-post ENOENT, extract share  ·  back to Dear Bubbe Nextjs  ·  refactor(bubbe): delete dead ExpandableChat + VoiceManager 3899565 →