← 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
M components/DesktopBubbe.tsxM components/MobileBubbe.tsxA types/speech.d.ts
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 →