From 5f24fd4db7f409c48ce1b1e0363e1516e31f7388 Mon Sep 17 00:00:00 2001 From: Shantur Rathore Date: Tue, 24 Mar 2026 22:42:27 +0000 Subject: [PATCH] feat(speech): make prompt input push to talk --- packages/ui/src/components/prompt-input.tsx | 62 ++++++++++++++++--- .../prompt-input/usePromptVoiceInput.ts | 60 +++++++++--------- .../ui/src/styles/messaging/prompt-input.css | 8 ++- 3 files changed, 90 insertions(+), 40 deletions(-) diff --git a/packages/ui/src/components/prompt-input.tsx b/packages/ui/src/components/prompt-input.tsx index f128791b..3b56c468 100644 --- a/packages/ui/src/components/prompt-input.tsx +++ b/packages/ui/src/components/prompt-input.tsx @@ -1,5 +1,5 @@ import { Suspense, createEffect, createSignal, lazy, on, onCleanup, Show } from "solid-js" -import { ArrowBigUp, ArrowBigDown, Loader2, Mic, Square } from "lucide-solid" +import { ArrowBigUp, ArrowBigDown, Loader2, Mic } from "lucide-solid" import ExpandButton from "./expand-button" import { clearAttachments, removeAttachment } from "../stores/attachments" import { resolvePastedPlaceholders } from "../lib/prompt-placeholders" @@ -464,6 +464,32 @@ export default function PromptInput(props: PromptInputProps) { const instance = () => getActiveInstance() + let voiceButtonPressed = false + + const beginVoicePress = (event?: PointerEvent | KeyboardEvent) => { + if (voiceButtonPressed || props.disabled || voiceInput.isTranscribing() || !voiceInput.canUseVoiceInput()) return + voiceButtonPressed = true + + if (event instanceof PointerEvent) { + const target = event.currentTarget + if (target instanceof HTMLElement) { + try { + target.setPointerCapture(event.pointerId) + } catch { + // no-op + } + } + } + + void voiceInput.startRecording() + } + + const endVoicePress = () => { + if (!voiceButtonPressed) return + voiceButtonPressed = false + voiceInput.stopRecording() + } + return (
void voiceInput.toggleRecording()} + onPointerDown={(event) => { + event.preventDefault() + beginVoicePress(event) + }} + onPointerUp={(event) => { + event.preventDefault() + endVoicePress() + }} + onPointerCancel={() => endVoicePress()} + onLostPointerCapture={() => endVoicePress()} + onKeyDown={(event) => { + if (event.repeat) return + if (event.key !== " " && event.key !== "Enter") return + event.preventDefault() + beginVoicePress(event) + }} + onKeyUp={(event) => { + if (event.key !== " " && event.key !== "Enter") return + event.preventDefault() + endVoicePress() + }} + onBlur={() => endVoicePress()} disabled={!voiceInput.isRecording() && (props.disabled || voiceInput.isTranscribing() || !voiceInput.canUseVoiceInput())} aria-label={voiceInput.buttonTitle()} title={voiceInput.buttonTitle()} > - - {formatVoiceTimer(voiceInput.elapsedMs())} -