From db3293bbb45a5ba2e715f5e68ba2f6bb0727b15c Mon Sep 17 00:00:00 2001 From: 1600822305 <1600822305@qq.com> Date: Fri, 11 Apr 2025 21:03:18 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E4=BA=86=20TTS=20=E7=9B=B8?= =?UTF-8?q?=E5=85=B3=E6=9C=8D=E5=8A=A1=E5=B9=B6=E6=9B=B4=E6=96=B0=E4=BA=86?= =?UTF-8?q?=E8=AE=BE=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/DraggableVoiceCallWindow.tsx | 218 ++++------- .../src/components/VoiceCallButton.tsx | 61 ++- .../src/components/VoiceCallModal.tsx | 213 ++++++----- src/renderer/src/i18n/locales/en-us.json | 1 - src/renderer/src/i18n/locales/zh-cn.json | 1 - src/renderer/src/pages/VoiceCallWindow.tsx | 355 ------------------ .../src/pages/home/Inputbar/Inputbar.tsx | 174 +++++---- .../src/pages/home/Messages/Message.tsx | 83 +++- .../settings/TTSSettings/TTSSettings.tsx | 8 - src/renderer/src/services/ASRService.ts | 54 +-- src/renderer/src/services/VoiceCallService.ts | 10 +- .../src/services/tts/TTSTextFilter.ts | 25 -- src/renderer/src/store/settings.ts | 3 - 13 files changed, 395 insertions(+), 811 deletions(-) delete mode 100644 src/renderer/src/pages/VoiceCallWindow.tsx diff --git a/src/renderer/src/components/DraggableVoiceCallWindow.tsx b/src/renderer/src/components/DraggableVoiceCallWindow.tsx index ac802d6e57..0194f31a1b 100644 --- a/src/renderer/src/components/DraggableVoiceCallWindow.tsx +++ b/src/renderer/src/components/DraggableVoiceCallWindow.tsx @@ -8,29 +8,15 @@ import { SoundOutlined } from '@ant-design/icons' import { Button, Space, Tooltip } from 'antd' -import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import React, { useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { useDispatch } from 'react-redux' -import { Action } from 'redux' import styled from 'styled-components' import { VoiceCallService } from '../services/VoiceCallService' import { setIsVoiceCallActive, setLastPlayedMessageId, setSkipNextAutoTTS } from '../store/settings' import VoiceVisualizer from './VoiceVisualizer' -// 节流函数,限制函数调用频率 - -function throttle any>(func: T, delay: number): (...args: Parameters) => void { - let lastCall = 0 - return (...args: Parameters) => { - const now = Date.now() - if (now - lastCall >= delay) { - lastCall = now - func(...args) - } - } -} - interface Props { visible: boolean onClose: () => void @@ -60,17 +46,56 @@ const DraggableVoiceCallWindow: React.FC = ({ const [isPaused, setIsPaused] = useState(false) const [isMuted, setIsMuted] = useState(false) - // eslint-disable-next-line react-hooks/exhaustive-deps - const memoizedOnClose = useCallback(() => { - onClose(); - }, []); + // 使用useRef跟踪窗口是否已经初始化,避免重复初始化 + const isInitializedRef = useRef(false) - // eslint-disable-next-line react-hooks/exhaustive-deps - const memoizedDispatch = useCallback((action: Action) => { - dispatch(action); - }, []); + // 使用useRef跟踪是否已经设置了状态,避免重复设置 + const stateSetRef = useRef(false) + // 单独处理状态设置,只在visible变化时执行一次 useEffect(() => { + if (visible) { + // 只有在状态没有设置过的情况下,才设置其他状态 + if (!stateSetRef.current) { + // 设置状态标记为已完成 + stateSetRef.current = true + + // 更新语音通话窗口状态 + dispatch(setIsVoiceCallActive(true)) + // 重置最后播放的消息ID + dispatch(setLastPlayedMessageId(null)) + } + } else if (!visible) { + // 当窗口关闭时重置状态标记 + stateSetRef.current = false + isInitializedRef.current = false + + // 更新语音通话窗口状态 + dispatch(setIsVoiceCallActive(false)) + } + }, [visible, dispatch]) + + // 单独的 useEffect 来设置 skipNextAutoTTS,确保每次窗口可见时都设置 + useEffect(() => { + if (visible) { + // 每次窗口可见时,都设置跳过下一次自动TTS + console.log('设置 skipNextAutoTTS 为 true,确保打开窗口时不会自动播放最后一条消息') + // 使用 setTimeout 确保在所有消息组件渲染后设置 + setTimeout(() => { + dispatch(setSkipNextAutoTTS(true)) + }, 100) + } + }, [visible, dispatch]) + + // 处理语音通话初始化和清理 + useEffect(() => { + // 添加TTS状态变化事件监听器 + const handleTTSStateChange = (event: CustomEvent) => { + const { isPlaying } = event.detail + console.log('TTS状态变化事件:', isPlaying) + setIsSpeaking(isPlaying) + } + const startVoiceCall = async () => { try { // 显示加载中提示 @@ -99,140 +124,55 @@ const DraggableVoiceCallWindow: React.FC = ({ } catch (error) { console.error('Voice call error:', error) window.message.error({ content: t('voice_call.error'), key: 'voice-call-init' }) - memoizedOnClose() + onClose() } } - // 添加TTS状态变化事件监听器 - const handleTTSStateChange = (event: CustomEvent) => { - const { isPlaying } = event.detail - console.log('TTS状态变化事件:', isPlaying) - setIsSpeaking(isPlaying) - } + if (visible && !isInitializedRef.current) { + // 设置初始化标记为已完成 + isInitializedRef.current = true - if (visible) { - // 更新语音通话窗口状态 - memoizedDispatch(setIsVoiceCallActive(true)) - // 重置最后播放的消息ID,确保不会自动播放已有消息 - memoizedDispatch(setLastPlayedMessageId(null)) - // 设置跳过下一次自动TTS,确保打开窗口时不会自动播放最后一条消息 - memoizedDispatch(setSkipNextAutoTTS(true)) + // 启动语音通话 startVoiceCall() // 添加事件监听器 window.addEventListener('tts-state-change', handleTTSStateChange as EventListener) } return () => { - // 更新语音通话窗口状态 - memoizedDispatch(setIsVoiceCallActive(false)) - VoiceCallService.endCall() + if (!visible) { + VoiceCallService.endCall() + } // 移除事件监听器 window.removeEventListener('tts-state-change', handleTTSStateChange as EventListener) } - // 使用 memoizedOnClose 和 memoizedDispatch 代替原始的 onClose 和 dispatch - }, [visible, t, memoizedDispatch, memoizedOnClose]) + }, [visible, t, onClose]) // 拖拽相关处理 const handleDragStart = (e: React.MouseEvent) => { if (containerRef.current) { - e.preventDefault() // 防止默认行为 - e.stopPropagation() // 阻止事件冒泡 - - // 直接使用鼠标相对于屏幕的位置和窗口相对于屏幕的位置计算偏移 setIsDragging(true) - - // 记录鼠标相对于窗口左上角的偏移量 + const rect = containerRef.current.getBoundingClientRect() setDragOffset({ - x: e.clientX - currentPosition.x, - y: e.clientY - currentPosition.y + x: e.clientX - rect.left, + y: e.clientY - rect.top }) - - // 在开发环境下只输出一次关键日志 - if (process.env.NODE_ENV === 'development') { - console.log('开始拖拽 - 偏移量:', { x: e.clientX - currentPosition.x, y: e.clientY - currentPosition.y }) - } } } - // 使用useCallback包装并优化handleDrag函数 - const handleDragBase = useCallback( - (e: MouseEvent) => { - if (isDragging && containerRef.current) { - // 限制拖拽范围,防止窗口被拖出屏幕 - const windowWidth = window.innerWidth - const windowHeight = window.innerHeight - const containerWidth = containerRef.current.offsetWidth - const containerHeight = containerRef.current.offsetHeight - - // 计算新位置,确保窗口不会被拖出屏幕 - // 使用鼠标当前位置减去偏移量,得到窗口应该在的位置 - const newX = Math.min(Math.max(0, e.clientX - dragOffset.x), windowWidth - containerWidth) - const newY = Math.min(Math.max(0, e.clientY - dragOffset.y), windowHeight - containerHeight) - - const newPosition = { x: newX, y: newY } - - // 完全关闭拖拽中的日志输出 - // 如果需要调试,可以在这里添加日志代码 - - // 立即更新窗口位置,提高响应速度 - containerRef.current.style.left = `${newX}px` - containerRef.current.style.top = `${newY}px` - - // 更新状态 - setCurrentPosition(newPosition) - onPositionChange?.(newPosition) + const handleDrag = (e: MouseEvent) => { + if (isDragging) { + const newPosition = { + x: e.clientX - dragOffset.x, + y: e.clientY - dragOffset.y } - }, - [isDragging, dragOffset, onPositionChange] - ) + setCurrentPosition(newPosition) + onPositionChange?.(newPosition) + } + } - // 使用useMemo包装节流函数,避免重复创建 - const handleDrag = useMemo(() => throttle(handleDragBase, 16), [handleDragBase]) // 16ms 大约相当于 60fps - - const handleDragEnd = useCallback(() => { + const handleDragEnd = () => { setIsDragging(false) - // 完全关闭拖拽结束的日志输出 - }, []) - - // 添加键盘快捷键支持 - const handleKeyDown = useCallback( - (e: KeyboardEvent) => { - if (e.key === 'Escape') { - onClose() - } else if (e.key === 'ArrowUp' || e.key === 'ArrowDown' || e.key === 'ArrowLeft' || e.key === 'ArrowRight') { - // 防止箭头键滚动页面 - e.preventDefault() - - // 移动步长 - const step = e.shiftKey ? 10 : 5 - - // 根据按键移动窗口 - let newX = currentPosition.x - let newY = currentPosition.y - - if (e.key === 'ArrowUp') newY -= step - if (e.key === 'ArrowDown') newY += step - if (e.key === 'ArrowLeft') newX -= step - if (e.key === 'ArrowRight') newX += step - - // 限制范围 - if (containerRef.current) { - const windowWidth = window.innerWidth - const windowHeight = window.innerHeight - const containerWidth = containerRef.current.offsetWidth - const containerHeight = containerRef.current.offsetHeight - - newX = Math.min(Math.max(0, newX), windowWidth - containerWidth) - newY = Math.min(Math.max(0, newY), windowHeight - containerHeight) - } - - const newPosition = { x: newX, y: newY } - setCurrentPosition(newPosition) - onPositionChange?.(newPosition) - } - }, - [currentPosition, onClose, onPositionChange] - ) + } useEffect(() => { if (isDragging) { @@ -243,17 +183,7 @@ const DraggableVoiceCallWindow: React.FC = ({ document.removeEventListener('mousemove', handleDrag) document.removeEventListener('mouseup', handleDragEnd) } - }, [isDragging, handleDrag, handleDragEnd]) - - // 添加键盘事件监听 - useEffect(() => { - if (visible) { - document.addEventListener('keydown', handleKeyDown) - } - return () => { - document.removeEventListener('keydown', handleKeyDown) - } - }, [visible, handleKeyDown]) + }, [isDragging, handleDrag]) // 语音通话相关处理 const toggleMute = () => { @@ -374,9 +304,7 @@ const DraggableVoiceCallWindow: React.FC = ({ left: `${currentPosition.x}px`, top: `${currentPosition.y}px`, position: 'fixed', - zIndex: 1000, - transform: 'translate3d(0,0,0)', // 启用GPU加速,提高拖拽流畅度 - willChange: 'left, top' // 提示浏览器这些属性将会变化,优化渲染 + zIndex: 1000 }}>
diff --git a/src/renderer/src/components/VoiceCallButton.tsx b/src/renderer/src/components/VoiceCallButton.tsx index c150fde280..1e9c44e31c 100644 --- a/src/renderer/src/components/VoiceCallButton.tsx +++ b/src/renderer/src/components/VoiceCallButton.tsx @@ -1,44 +1,39 @@ -import React, { useState } from 'react'; -import { Button, Tooltip } from 'antd'; -import { PhoneOutlined, LoadingOutlined } from '@ant-design/icons'; -import { useTranslation } from 'react-i18next'; -import { useDispatch } from 'react-redux'; -import { VoiceCallService } from '../services/VoiceCallService'; -import DraggableVoiceCallWindow from './DraggableVoiceCallWindow'; -import { setIsVoiceCallActive, setLastPlayedMessageId, setSkipNextAutoTTS } from '../store/settings'; +import { LoadingOutlined, PhoneOutlined } from '@ant-design/icons' +import { Button, Tooltip } from 'antd' +import React, { useState } from 'react' +import { useTranslation } from 'react-i18next' + +import { VoiceCallService } from '../services/VoiceCallService' +import DraggableVoiceCallWindow from './DraggableVoiceCallWindow' interface Props { - disabled?: boolean; - style?: React.CSSProperties; + disabled?: boolean + style?: React.CSSProperties } const VoiceCallButton: React.FC = ({ disabled = false, style }) => { - const { t } = useTranslation(); - const dispatch = useDispatch(); - const [isWindowVisible, setIsWindowVisible] = useState(false); - const [isLoading, setIsLoading] = useState(false); - const [windowPosition, setWindowPosition] = useState({ x: 20, y: 20 }); + const { t } = useTranslation() + const [isWindowVisible, setIsWindowVisible] = useState(false) + const [isLoading, setIsLoading] = useState(false) + const [windowPosition, setWindowPosition] = useState({ x: 20, y: 20 }) const handleClick = async () => { - if (disabled || isLoading) return; + if (disabled || isLoading) return - setIsLoading(true); + setIsLoading(true) try { // 初始化语音服务 - await VoiceCallService.initialize(); - setIsWindowVisible(true); - dispatch(setIsVoiceCallActive(true)); - // 重置最后播放的消息ID,确保不会自动播放已有消息 - dispatch(setLastPlayedMessageId(null)); - // 设置跳过下一次自动TTS,确保打开窗口时不会自动播放最后一条消息 - dispatch(setSkipNextAutoTTS(true)); + await VoiceCallService.initialize() + // 先设置窗口可见,然后在DraggableVoiceCallWindow组件中处理状态更新 + setIsWindowVisible(true) + // 注意:不在这里调用dispatch,而是在DraggableVoiceCallWindow组件中处理 } catch (error) { - console.error('Failed to initialize voice call:', error); - window.message.error(t('voice_call.initialization_failed')); + console.error('Failed to initialize voice call:', error) + window.message.error(t('voice_call.initialization_failed')) } finally { - setIsLoading(false); + setIsLoading(false) } - }; + } return ( <> @@ -54,14 +49,14 @@ const VoiceCallButton: React.FC = ({ disabled = false, style }) => { { - setIsWindowVisible(false); - dispatch(setIsVoiceCallActive(false)); + setIsWindowVisible(false) + // 注意:不在这里调用dispatch,而是在DraggableVoiceCallWindow组件中处理 }} position={windowPosition} onPositionChange={setWindowPosition} /> - ); -}; + ) +} -export default VoiceCallButton; +export default VoiceCallButton diff --git a/src/renderer/src/components/VoiceCallModal.tsx b/src/renderer/src/components/VoiceCallModal.tsx index 3e1310f7f1..d688e9a9b3 100644 --- a/src/renderer/src/components/VoiceCallModal.tsx +++ b/src/renderer/src/components/VoiceCallModal.tsx @@ -1,5 +1,3 @@ -import React, { useEffect, useState } from 'react'; -import { Modal, Button, Space, Tooltip } from 'antd'; import { AudioMutedOutlined, AudioOutlined, @@ -7,39 +5,47 @@ import { PauseCircleOutlined, PlayCircleOutlined, SoundOutlined -} from '@ant-design/icons'; -import styled from 'styled-components'; -import { useTranslation } from 'react-i18next'; -import VoiceVisualizer from './VoiceVisualizer'; -import { VoiceCallService } from '../services/VoiceCallService'; +} from '@ant-design/icons' +import { Button, Modal, Space, Tooltip } from 'antd' +import React, { useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' +import styled from 'styled-components' + +import { VoiceCallService } from '../services/VoiceCallService' +import VoiceVisualizer from './VoiceVisualizer' interface Props { - visible: boolean; - onClose: () => void; + visible: boolean + onClose: () => void } const VoiceCallModal: React.FC = ({ visible, onClose }) => { - const { t } = useTranslation(); - const [isMuted, setIsMuted] = useState(false); - const [isPaused, setIsPaused] = useState(false); - const [transcript, setTranscript] = useState(''); - const [response, setResponse] = useState(''); - const [isListening, setIsListening] = useState(false); - const [isSpeaking, setIsSpeaking] = useState(false); - const [isRecording, setIsRecording] = useState(false); - const [isProcessing, setIsProcessing] = useState(false); + const { t } = useTranslation() + const [isMuted, setIsMuted] = useState(false) + const [isPaused, setIsPaused] = useState(false) + const [transcript, setTranscript] = useState('') + const [response, setResponse] = useState('') + const [isListening, setIsListening] = useState(false) + const [isSpeaking, setIsSpeaking] = useState(false) + const [isRecording, setIsRecording] = useState(false) + const [isProcessing, setIsProcessing] = useState(false) + + const handleClose = () => { + VoiceCallService.endCall() + onClose() + } useEffect(() => { const startVoiceCall = async () => { try { // 显示加载中提示 - window.message.loading({ content: t('voice_call.initializing'), key: 'voice-call-init' }); + window.message.loading({ content: t('voice_call.initializing'), key: 'voice-call-init' }) // 预先初始化语音识别服务 try { - await VoiceCallService.initialize(); + await VoiceCallService.initialize() } catch (initError) { - console.warn('语音识别服务初始化警告:', initError); + console.warn('语音识别服务初始化警告:', initError) // 不抛出异常,允许程序继续运行 } @@ -48,142 +54,137 @@ const VoiceCallModal: React.FC = ({ visible, onClose }) => { onTranscript: (text) => setTranscript(text), onResponse: (text) => setResponse(text), onListeningStateChange: setIsListening, - onSpeakingStateChange: setIsSpeaking, - }); + onSpeakingStateChange: setIsSpeaking + }) // 关闭加载中提示 - window.message.success({ content: t('voice_call.ready'), key: 'voice-call-init' }); + window.message.success({ content: t('voice_call.ready'), key: 'voice-call-init' }) } catch (error) { - console.error('Voice call error:', error); - window.message.error({ content: t('voice_call.error'), key: 'voice-call-init' }); - handleClose(); + console.error('Voice call error:', error) + window.message.error({ content: t('voice_call.error'), key: 'voice-call-init' }) + handleClose() } - }; + } // 添加TTS状态变化事件监听器 const handleTTSStateChange = (event: CustomEvent) => { - const { isPlaying } = event.detail; - console.log('TTS状态变化事件:', isPlaying); - setIsSpeaking(isPlaying); - }; + const { isPlaying } = event.detail + console.log('TTS状态变化事件:', isPlaying) + setIsSpeaking(isPlaying) + } if (visible) { - startVoiceCall(); + startVoiceCall() // 添加事件监听器 - window.addEventListener('tts-state-change', handleTTSStateChange as EventListener); + window.addEventListener('tts-state-change', handleTTSStateChange as EventListener) } return () => { - VoiceCallService.endCall(); + VoiceCallService.endCall() // 移除事件监听器 - window.removeEventListener('tts-state-change', handleTTSStateChange as EventListener); - }; - }, [visible, t]); - - const handleClose = () => { - VoiceCallService.endCall(); - onClose(); - }; + window.removeEventListener('tts-state-change', handleTTSStateChange as EventListener) + } + }, [visible, t, handleClose]) const toggleMute = () => { - const newMuteState = !isMuted; - setIsMuted(newMuteState); - VoiceCallService.setMuted(newMuteState); - }; + const newMuteState = !isMuted + setIsMuted(newMuteState) + VoiceCallService.setMuted(newMuteState) + } const togglePause = () => { - const newPauseState = !isPaused; - setIsPaused(newPauseState); - VoiceCallService.setPaused(newPauseState); - }; + const newPauseState = !isPaused + setIsPaused(newPauseState) + VoiceCallService.setPaused(newPauseState) + } // 长按说话相关处理 const handleRecordStart = async (e: React.MouseEvent | React.TouchEvent) => { - e.preventDefault(); // 防止触摸事件的默认行为 + e.preventDefault() // 防止触摸事件的默认行为 - if (isProcessing || isPaused) return; + if (isProcessing || isPaused) return // 先清除之前的语音识别结果 - setTranscript(''); + setTranscript('') // 无论是否正在播放,都强制停止TTS - VoiceCallService.stopTTS(); - setIsSpeaking(false); + VoiceCallService.stopTTS() + setIsSpeaking(false) // 更新UI状态 - setIsRecording(true); - setIsProcessing(true); // 设置处理状态,防止重复点击 + setIsRecording(true) + setIsProcessing(true) // 设置处理状态,防止重复点击 // 开始录音 try { - await VoiceCallService.startRecording(); - console.log('开始录音'); - setIsProcessing(false); // 录音开始后取消处理状态 + await VoiceCallService.startRecording() + console.log('开始录音') + setIsProcessing(false) // 录音开始后取消处理状态 } catch (error) { - console.error('开始录音出错:', error); - window.message.error({ content: '启动语音识别失败,请确保语音识别服务已启动', key: 'voice-call-error' }); - setIsRecording(false); - setIsProcessing(false); + console.error('开始录音出错:', error) + window.message.error({ content: '启动语音识别失败,请确保语音识别服务已启动', key: 'voice-call-error' }) + setIsRecording(false) + setIsProcessing(false) } - }; + } const handleRecordEnd = async (e: React.MouseEvent | React.TouchEvent) => { - e.preventDefault(); // 防止触摸事件的默认行为 + e.preventDefault() // 防止触摸事件的默认行为 - if (!isRecording) return; + if (!isRecording) return // 立即更新UI状态 - setIsRecording(false); - setIsProcessing(true); + setIsRecording(false) + setIsProcessing(true) // 无论是否正在播放,都强制停止TTS - VoiceCallService.stopTTS(); - setIsSpeaking(false); + VoiceCallService.stopTTS() + setIsSpeaking(false) // 确保录音完全停止 try { - await VoiceCallService.stopRecording(); - console.log('录音已停止'); + await VoiceCallService.stopRecording() + console.log('录音已停止') } catch (error) { - console.error('停止录音出错:', error); + console.error('停止录音出错:', error) } finally { // 无论成功与否,都确保在一定时间后重置处理状态 setTimeout(() => { - setIsProcessing(false); - }, 1000); // 增加延迟时间,确保有足够时间处理结果 + setIsProcessing(false) + }, 1000) // 增加延迟时间,确保有足够时间处理结果 } - }; + } // 处理鼠标/触摸离开按钮的情况 const handleRecordCancel = async (e: React.MouseEvent | React.TouchEvent) => { - e.preventDefault(); + e.preventDefault() if (isRecording) { // 立即更新UI状态 - setIsRecording(false); - setIsProcessing(true); + setIsRecording(false) + setIsProcessing(true) // 无论是否正在播放,都强制停止TTS - VoiceCallService.stopTTS(); - setIsSpeaking(false); + VoiceCallService.stopTTS() + setIsSpeaking(false) // 取消录音,不发送给AI try { - await VoiceCallService.cancelRecording(); - console.log('录音已取消'); + await VoiceCallService.cancelRecording() + console.log('录音已取消') // 清除输入文本 - setTranscript(''); + setTranscript('') } catch (error) { - console.error('取消录音出错:', error); + console.error('取消录音出错:', error) } finally { // 无论成功与否,都确保在一定时间后重置处理状态 setTimeout(() => { - setIsProcessing(false); - }, 1000); + setIsProcessing(false) + }, 1000) } } - }; + } return ( = ({ visible, onClose }) => { footer={null} width={500} centered - maskClosable={false} - > + maskClosable={false}> @@ -232,7 +232,7 @@ const VoiceCallModal: React.FC = ({ visible, onClose }) => { /> } onMouseDown={handleRecordStart} onMouseUp={handleRecordEnd} @@ -241,8 +241,7 @@ const VoiceCallModal: React.FC = ({ visible, onClose }) => { onTouchEnd={handleRecordEnd} onTouchCancel={handleRecordCancel} size="large" - disabled={isProcessing || isPaused} - > + disabled={isProcessing || isPaused}> {isRecording ? t('voice_call.release_to_send') : t('voice_call.press_to_talk')} @@ -258,21 +257,21 @@ const VoiceCallModal: React.FC = ({ visible, onClose }) => { - ); -}; + ) +} const Container = styled.div` display: flex; flex-direction: column; gap: 20px; height: 400px; -`; +` const VisualizerContainer = styled.div` display: flex; justify-content: space-between; height: 100px; -`; +` const TranscriptContainer = styled.div` flex: 1; @@ -281,33 +280,33 @@ const TranscriptContainer = styled.div` border-radius: 8px; padding: 16px; background-color: var(--color-background-2); -`; +` const TranscriptText = styled.p` margin-bottom: 8px; color: var(--color-text-1); -`; +` const ResponseText = styled.p` margin-bottom: 8px; color: var(--color-primary); -`; +` const UserLabel = styled.span` font-weight: bold; color: var(--color-text-1); -`; +` const AILabel = styled.span` font-weight: bold; color: var(--color-primary); -`; +` const ControlsContainer = styled.div` display: flex; justify-content: center; padding: 10px 0; -`; +` const RecordButton = styled(Button)` min-width: 150px; @@ -316,6 +315,6 @@ const RecordButton = styled(Button)` &:active { transform: scale(0.95); } -`; +` -export default VoiceCallModal; +export default VoiceCallModal diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index fb03bb0fbd..bcf2d4194a 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -1413,7 +1413,6 @@ "filter.markdown": "Filter Markdown", "filter.code_blocks": "Filter code blocks", "filter.html_tags": "Filter HTML tags", - "filter.emojis": "Filter emojis", "max_text_length": "Maximum text length", "test": "Test Speech", "help": "Text-to-speech functionality supports converting text to natural-sounding speech.", diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index 75d5e7f851..516b237a7c 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -1427,7 +1427,6 @@ "filter.markdown": "过滤Markdown标记", "filter.code_blocks": "过滤代码块", "filter.html_tags": "过滤HTML标签", - "filter.emojis": "过滤表情符号", "max_text_length": "最大文本长度", "test": "测试语音", "help": "语音合成功能支持将文本转换为自然语音。", diff --git a/src/renderer/src/pages/VoiceCallWindow.tsx b/src/renderer/src/pages/VoiceCallWindow.tsx deleted file mode 100644 index a55f50c315..0000000000 --- a/src/renderer/src/pages/VoiceCallWindow.tsx +++ /dev/null @@ -1,355 +0,0 @@ -import React, { useCallback, useEffect, useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { useDispatch } from 'react-redux'; -import { Action } from 'redux'; -import styled from 'styled-components'; -import { Button, Space, Tooltip } from 'antd'; -import { - AudioMutedOutlined, - AudioOutlined, - CloseOutlined, - PauseCircleOutlined, - PlayCircleOutlined, - SoundOutlined -} from '@ant-design/icons'; -import { VoiceCallService } from '../services/VoiceCallService'; -import VoiceVisualizer from '../components/VoiceVisualizer'; -import { setIsVoiceCallActive, setLastPlayedMessageId, setSkipNextAutoTTS } from '../store/settings'; - -/** - * 独立窗口的语音通话组件 - * 这个组件用于在独立窗口中显示语音通话界面 - */ -const VoiceCallWindow: React.FC = () => { - const { t } = useTranslation(); - const dispatch = useDispatch(); - - // 使用 useCallback 包装 dispatch 和 handleClose 函数,避免无限循环 - // eslint-disable-next-line react-hooks/exhaustive-deps - const memoizedDispatch = useCallback((action: Action) => { - dispatch(action); - }, []); - - // eslint-disable-next-line react-hooks/exhaustive-deps - const memoizedHandleClose = useCallback(() => { - window.close(); - }, []); - - // 语音通话状态 - const [transcript, setTranscript] = useState(''); - const [isListening, setIsListening] = useState(false); - const [isSpeaking, setIsSpeaking] = useState(false); - const [isRecording, setIsRecording] = useState(false); - const [isProcessing, setIsProcessing] = useState(false); - const [isPaused, setIsPaused] = useState(false); - const [isMuted, setIsMuted] = useState(false); - - useEffect(() => { - const startVoiceCall = async () => { - try { - // 显示加载中提示 - window.message.loading({ content: t('voice_call.initializing'), key: 'voice-call-init' }); - - // 预先初始化语音识别服务 - try { - await VoiceCallService.initialize(); - } catch (initError) { - console.warn('语音识别服务初始化警告:', initError); - // 不抛出异常,允许程序继续运行 - } - - // 启动语音通话 - await VoiceCallService.startCall({ - onTranscript: (text) => setTranscript(text), - onResponse: () => { - // 这里不设置response,因为响应会显示在聊天界面中 - }, - onListeningStateChange: setIsListening, - onSpeakingStateChange: setIsSpeaking, - }); - - // 关闭加载中提示 - window.message.success({ content: t('voice_call.ready'), key: 'voice-call-init' }); - } catch (error) { - console.error('Voice call error:', error); - window.message.error({ content: t('voice_call.error'), key: 'voice-call-init' }); - memoizedHandleClose(); - } - }; - - // 添加TTS状态变化事件监听器 - const handleTTSStateChange = (event: CustomEvent) => { - const { isPlaying } = event.detail; - console.log('TTS状态变化事件:', isPlaying); - setIsSpeaking(isPlaying); - }; - - // 更新语音通话窗口状态 - memoizedDispatch(setIsVoiceCallActive(true)); - // 重置最后播放的消息ID,确保不会自动播放已有消息 - memoizedDispatch(setLastPlayedMessageId(null)); - // 设置跳过下一次自动TTS,确保打开窗口时不会自动播放最后一条消息 - memoizedDispatch(setSkipNextAutoTTS(true)); - - startVoiceCall(); - // 添加事件监听器 - window.addEventListener('tts-state-change', handleTTSStateChange as EventListener); - - return () => { - // 更新语音通话窗口状态 - memoizedDispatch(setIsVoiceCallActive(false)); - VoiceCallService.endCall(); - // 移除事件监听器 - window.removeEventListener('tts-state-change', handleTTSStateChange as EventListener); - }; - }, [t, memoizedDispatch, memoizedHandleClose]); - - // 语音通话相关处理 - const toggleMute = () => { - setIsMuted(!isMuted); - VoiceCallService.setMuted(!isMuted); - }; - - const togglePause = () => { - const newPauseState = !isPaused; - setIsPaused(newPauseState); - VoiceCallService.setPaused(newPauseState); - }; - - // 关闭窗口 - const handleClose = () => { - memoizedHandleClose(); - }; - - // 长按说话相关处理 - const handleRecordStart = async (e: React.MouseEvent | React.TouchEvent) => { - e.preventDefault(); // 防止触摸事件的默认行为 - - if (isProcessing || isPaused) return; - - // 先清除之前的语音识别结果 - setTranscript(''); - - // 无论是否正在播放,都强制停止TTS - VoiceCallService.stopTTS(); - setIsSpeaking(false); - - // 更新UI状态 - setIsRecording(true); - setIsProcessing(true); // 设置处理状态,防止重复点击 - - // 开始录音 - try { - await VoiceCallService.startRecording(); - console.log('开始录音'); - setIsProcessing(false); // 录音开始后取消处理状态 - } catch (error) { - console.error('开始录音出错:', error); - window.message.error({ content: '启动语音识别失败,请确保语音识别服务已启动', key: 'voice-call-error' }); - setIsRecording(false); - setIsProcessing(false); - } - }; - - const handleRecordEnd = async (e: React.MouseEvent | React.TouchEvent) => { - e.preventDefault(); // 防止触摸事件的默认行为 - - if (!isRecording) return; - - // 立即更新UI状态 - setIsRecording(false); - setIsProcessing(true); - - // 无论是否正在播放,都强制停止TTS - VoiceCallService.stopTTS(); - setIsSpeaking(false); - - // 确保录音完全停止 - try { - // 传递 true 参数,表示将结果发送到聊天界面 - const success = await VoiceCallService.stopRecordingAndSendToChat(); - console.log('录音已停止,结果已发送到聊天界面', success ? '成功' : '失败'); - - if (success) { - // 显示成功消息 - window.message.success({ content: '语音识别已完成,正在发送消息...', key: 'voice-call-send' }); - } else { - // 显示失败消息 - window.message.error({ content: '发送语音识别结果失败', key: 'voice-call-error' }); - } - } catch (error) { - console.error('停止录音出错:', error); - window.message.error({ content: '停止录音出错', key: 'voice-call-error' }); - } finally { - // 无论成功与否,都确保在一定时间后重置处理状态 - setTimeout(() => { - setIsProcessing(false); - }, 1000); // 增加延迟时间,确保有足够时间处理结果 - } - }; - - // 处理鼠标/触摸离开按钮的情况 - const handleRecordCancel = async (e: React.MouseEvent | React.TouchEvent) => { - e.preventDefault(); - - if (isRecording) { - // 立即更新UI状态 - setIsRecording(false); - setIsProcessing(true); - - // 无论是否正在播放,都强制停止TTS - VoiceCallService.stopTTS(); - setIsSpeaking(false); - - // 取消录音,不发送给AI - try { - await VoiceCallService.cancelRecording(); - console.log('录音已取消'); - - // 清除输入文本 - setTranscript(''); - } catch (error) { - console.error('取消录音出错:', error); - } finally { - // 无论成功与否,都确保在一定时间后重置处理状态 - setTimeout(() => { - setIsProcessing(false); - }, 1000); - } - } - }; - - return ( - -
- {t('voice_call.title')} - - - -
- - - - - - - - - {transcript && ( - - {t('voice_call.you')}: {transcript} - - )} - - - - -