feat(chat): add voice playback with auto-play and visual effects (#396)
## Features ### Core Functionality - **Web Speech API Integration**: Add TTS (text-to-speech) playback for assistant messages - **Manual Playback**: Click-to-play button next to each assistant message (🔊 icon) - **Auto-play Mode**: Toggle switch in input bar to auto-play responses - **Playback Controls**: Play/pause/stop with visual feedback ### User Interface - **Playback Button**: Hover-activated button in message meta area (next to copy button) - **Auto-play Switch**: Voice icon toggle in input top bar with state persistence - **Mobile Optimization**: Buttons always visible on mobile (≤768px width) - **Visual Feedback**: - Rainbow glowing border during playback (2px border, 10px/20px glow) - 4-second animation cycle through 6 colors - Play/pause icon toggle ### Voice Customization - **Pitch/Rate Control**: Low-pitched (0.5) fast-speaking (1.2) "male voice" - **Auto Voice Selection**: Attempts to select male voices across platforms (macOS: Yaoyao, Windows: David/Daniel) - **Platform Compatibility**: Works with system-provided voices on macOS, iOS, Android, Windows ### Content Filtering - Smart text extraction: filters code blocks, `<thinking>` tags, HTML - Only assistant messages are eligible for playback - Tool and system messages are excluded ### Internationalization - Added 8 language translations (en, zh, de, es, fr, ja, ko, pt) - New keys: `playSpeech`, `pauseSpeech`, `resumeSpeech`, `stopSpeech`, `autoPlaySpeech`, `speechNotSupported` ## Technical Details ### New Files - `packages/client/src/composables/useSpeech.ts`: Core speech synthesis composable - Voice loading and selection logic - Single-instance global speech manager - Event handling (onstart, onend, onerror, onboundary) - State management (isPlaying, isPaused, currentMessageId) ### Modified Components - **ChatInput.vue**: Auto-play switch with localStorage persistence - **MessageItem.vue**: - Playback button integration - Event listener for auto-play triggers - Mobile-first button visibility - Rainbow border animation during playback ### Store Changes - `chat.ts`: - Added `autoPlaySpeechEnabled` state - `setAutoPlaySpeech()` method - `playMessageSpeech()` method for event-based playback - Auto-play trigger on `run.completed` event ## Browser Support - Requires Web Speech API support (all modern browsers) - Graceful degradation: button hidden if API not supported - Voice availability varies by platform and OS Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -4,7 +4,7 @@ import { useChatStore } from '@/stores/hermes/chat'
|
||||
import { useAppStore } from '@/stores/hermes/app'
|
||||
import { useProfilesStore } from '@/stores/hermes/profiles'
|
||||
import { fetchContextLength } from '@/api/hermes/sessions'
|
||||
import { NButton, NTooltip } from 'naive-ui'
|
||||
import { NButton, NTooltip, NSwitch } from 'naive-ui'
|
||||
import { computed, ref, onMounted, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
@@ -18,6 +18,26 @@ const isDragging = ref(false)
|
||||
const dragCounter = ref(0)
|
||||
const isComposing = ref(false)
|
||||
|
||||
// 自动播放语音开关
|
||||
const autoPlaySpeech = ref(false)
|
||||
|
||||
// 从 localStorage 读取设置
|
||||
onMounted(() => {
|
||||
const saved = localStorage.getItem('autoPlaySpeech')
|
||||
if (saved !== null) {
|
||||
autoPlaySpeech.value = saved === 'true'
|
||||
// 同步到 chat store
|
||||
chatStore.setAutoPlaySpeech(autoPlaySpeech.value)
|
||||
}
|
||||
})
|
||||
|
||||
// 监听变化并保存
|
||||
watch(autoPlaySpeech, (value) => {
|
||||
localStorage.setItem('autoPlaySpeech', String(value))
|
||||
// 通知 chat store
|
||||
chatStore.setAutoPlaySpeech(value)
|
||||
})
|
||||
|
||||
const canSend = computed(() => inputText.value.trim() || attachments.value.length > 0)
|
||||
|
||||
// --- Context info ---
|
||||
@@ -195,7 +215,7 @@ function isImage(type: string): boolean {
|
||||
|
||||
<template>
|
||||
<div class="chat-input-area">
|
||||
<!-- Top bar: attach + context info -->
|
||||
<!-- Top bar: attach + auto play speech + context info -->
|
||||
<div class="input-top-bar">
|
||||
<NTooltip trigger="hover">
|
||||
<template #trigger>
|
||||
@@ -207,6 +227,25 @@ function isImage(type: string): boolean {
|
||||
</template>
|
||||
{{ t('chat.attachFiles') }}
|
||||
</NTooltip>
|
||||
|
||||
<div class="auto-play-speech-switch">
|
||||
<NTooltip trigger="hover">
|
||||
<template #trigger>
|
||||
<div class="switch-label">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polygon points="5 3 19 12 5 21 5 3"/>
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
{{ t('chat.autoPlaySpeech') }}
|
||||
</NTooltip>
|
||||
<NSwitch
|
||||
size="small"
|
||||
v-model:value="autoPlaySpeech"
|
||||
:round="false"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<span v-if="totalTokens > 0" class="context-info" :class="{ 'context-warning': usagePercent > 80 }">
|
||||
{{ formatTokens(totalTokens) }} / {{ formatTokens(contextLength) }} · {{ t('chat.contextRemaining') }} {{ formatTokens(remainingTokens) }}
|
||||
</span>
|
||||
@@ -314,6 +353,26 @@ function isImage(type: string): boolean {
|
||||
padding: 0 0 6px;
|
||||
}
|
||||
|
||||
.auto-play-speech-switch {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 0 8px;
|
||||
border-left: 1px solid $border-light;
|
||||
margin-left: 4px;
|
||||
|
||||
.switch-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: $text-muted;
|
||||
font-size: 12px;
|
||||
|
||||
svg {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.context-info {
|
||||
font-size: 11px;
|
||||
color: $text-muted;
|
||||
|
||||
Reference in New Issue
Block a user