feat: weather precip/wind, dashboard mic, remove global voice overlay
- WeatherCard: show precipitation (mm) and max wind speed per forecast day - DashboardChatInput: add PTT mic button (transcribe-to-input, voice-gated) - Remove global VoiceOverlay floating button and Space PTT shortcut from App.vue — inline mic buttons in chat/briefing/dashboard are the right UX; global overlay had focus/latency/context issues Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, nextTick } from "vue";
|
||||
import { apiGet } from "@/api/client";
|
||||
import { ref, nextTick, onMounted } from "vue";
|
||||
import { apiGet, getVoiceStatus, transcribeAudio } from "@/api/client";
|
||||
import { useVoiceRecorder } from "@/composables/useVoiceRecorder";
|
||||
import { useChatStore } from "@/stores/chat";
|
||||
import type { Note } from "@/types/note";
|
||||
|
||||
@@ -106,6 +107,39 @@ function focus() {
|
||||
inputEl.value?.focus();
|
||||
}
|
||||
|
||||
// Voice PTT
|
||||
const voiceEnabled = ref(false);
|
||||
const transcribingVoice = ref(false);
|
||||
const recorder = useVoiceRecorder();
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const status = await getVoiceStatus();
|
||||
voiceEnabled.value = status.enabled && status.stt;
|
||||
} catch { /* voice absent */ }
|
||||
});
|
||||
|
||||
async function startPtt() {
|
||||
if (!voiceEnabled.value || recorder.recording.value) return;
|
||||
await recorder.startRecording();
|
||||
}
|
||||
|
||||
async function stopPtt() {
|
||||
if (!recorder.recording.value) return;
|
||||
transcribingVoice.value = true;
|
||||
try {
|
||||
const blob = await recorder.stopRecording();
|
||||
const { transcript } = await transcribeAudio(blob);
|
||||
if (transcript.trim()) {
|
||||
messageInput.value = (messageInput.value ? messageInput.value + " " : "") + transcript.trim();
|
||||
await nextTick();
|
||||
autoResize();
|
||||
}
|
||||
} catch { /* transcription failed silently */ } finally {
|
||||
transcribingVoice.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({ focus });
|
||||
</script>
|
||||
|
||||
@@ -188,6 +222,27 @@ defineExpose({ focus });
|
||||
rows="1"
|
||||
></textarea>
|
||||
|
||||
<button
|
||||
v-if="voiceEnabled && recorder.isSupported"
|
||||
class="btn-attach btn-mic-ptt"
|
||||
:class="{ 'mic-recording': recorder.recording.value, 'mic-transcribing': transcribingVoice }"
|
||||
@mousedown.prevent="startPtt"
|
||||
@mouseup.prevent="stopPtt"
|
||||
@touchstart.prevent="startPtt"
|
||||
@touchend.prevent="stopPtt"
|
||||
:disabled="transcribingVoice || !store.chatReady"
|
||||
:title="recorder.recording.value ? 'Release to send' : 'Hold to speak'"
|
||||
aria-label="Push to talk"
|
||||
>
|
||||
<svg v-if="!transcribingVoice" width="17" height="17" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M12 14c1.66 0 3-1.34 3-3V5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3zm-1-9c0-.55.45-1 1-1s1 .45 1 1v6c0 .55-.45 1-1 1s-1-.45-1-1V5zm6 6c0 2.76-2.24 5-5 5s-5-2.24-5-5H5c0 3.53 2.61 6.43 6 6.92V21h2v-3.08c3.39-.49 6-3.39 6-6.92h-2z"/>
|
||||
</svg>
|
||||
<svg v-else width="17" height="17" viewBox="0 0 24 24" fill="currentColor">
|
||||
<circle cx="12" cy="12" r="8" opacity="0.35"/>
|
||||
<circle cx="12" cy="12" r="4"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="btn-send"
|
||||
@click="onSubmit"
|
||||
@@ -348,4 +403,16 @@ defineExpose({ focus });
|
||||
opacity: 0.35;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.btn-mic-ptt {
|
||||
transition: color 0.15s, opacity 0.15s;
|
||||
}
|
||||
.btn-mic-ptt.mic-recording {
|
||||
color: #ef4444;
|
||||
opacity: 1;
|
||||
}
|
||||
.btn-mic-ptt.mic-transcribing {
|
||||
color: var(--color-primary);
|
||||
opacity: 1;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user