feat: Migrate voice capture to composable with floating push-to-talk
Extract voice recording logic from FloatingVoice.vue into useVoiceCapture composable. TranscriptCard now does real recording instead of mock typing. InputSettings allows voice mode toggle (WebSpeech/Whisper GPU), mic selection, and debug audio playback. ChatInput gets a settings gear button. Long-press on FloatBubble shows a floating TranscriptCard (push-to-talk) instead of opening the full PromptBar. Release stops recording after a 500ms buffer. Click still opens PromptBar normally. Parallel MediaRecorder captures raw audio in WebSpeech mode for DB save and debug playback. Transient errors (no-speech) no longer kill sessions. Touch selection prevention on FloatBubble for tablets.
This commit is contained in:
@@ -11,9 +11,10 @@ const props = defineProps<{
|
||||
const emit = defineEmits<{
|
||||
click: [event: MouseEvent]
|
||||
hold: [el: HTMLElement]
|
||||
holdrelease: []
|
||||
}>()
|
||||
|
||||
const HOLD_MS = 400
|
||||
const HOLD_MS = 200
|
||||
let holdTimer: number | null = null
|
||||
let didHold = false
|
||||
let holdTarget: HTMLElement | null = null
|
||||
@@ -30,13 +31,18 @@ function onPointerDown(e: PointerEvent) {
|
||||
|
||||
function onPointerUp(e: PointerEvent) {
|
||||
clearHold()
|
||||
if (!didHold) {
|
||||
if (didHold) {
|
||||
emit('holdrelease')
|
||||
} else {
|
||||
emit('click', e as unknown as MouseEvent)
|
||||
}
|
||||
}
|
||||
|
||||
function onPointerCancel() {
|
||||
clearHold()
|
||||
if (didHold) {
|
||||
emit('holdrelease')
|
||||
}
|
||||
}
|
||||
|
||||
function clearHold() {
|
||||
@@ -124,7 +130,10 @@ function bubbleTitle() {
|
||||
@pointerdown.prevent="onPointerDown"
|
||||
@pointerup="onPointerUp"
|
||||
@pointercancel="onPointerCancel"
|
||||
@touchstart.prevent
|
||||
@contextmenu.prevent
|
||||
@selectstart.prevent
|
||||
@dragstart.prevent
|
||||
@mouseenter="!isAnimating() && (($event.currentTarget as HTMLElement).style.boxShadow = bubbleHoverShadow())"
|
||||
@mouseleave="!isAnimating() && (($event.currentTarget as HTMLElement).style.boxShadow = bubbleStyle().boxShadow || '')"
|
||||
>
|
||||
@@ -194,7 +203,8 @@ function bubbleTitle() {
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
-webkit-touch-callout: none;
|
||||
touch-action: manipulation;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
touch-action: none;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@@ -543,6 +553,14 @@ function bubbleTitle() {
|
||||
50% { opacity: 0.7; }
|
||||
}
|
||||
|
||||
/* Prevent touch selection on all children */
|
||||
.agent-bubble * {
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
-webkit-touch-callout: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* ====================== LABELS ====================== */
|
||||
|
||||
.bubble-label {
|
||||
|
||||
Reference in New Issue
Block a user