diff --git a/src/renderer/src/components/DraggableVoiceCallWindow.tsx b/src/renderer/src/components/DraggableVoiceCallWindow.tsx new file mode 100644 index 0000000000..8e030a1920 --- /dev/null +++ b/src/renderer/src/components/DraggableVoiceCallWindow.tsx @@ -0,0 +1,388 @@ +import React, { useState, useEffect, useRef } from 'react'; +import { Button, Space, Tooltip } from 'antd'; +import { + AudioMutedOutlined, + AudioOutlined, + CloseOutlined, + PauseCircleOutlined, + PlayCircleOutlined, + SoundOutlined, + DragOutlined +} from '@ant-design/icons'; +import styled from 'styled-components'; +import { useTranslation } from 'react-i18next'; +import VoiceVisualizer from './VoiceVisualizer'; +import { VoiceCallService } from '../services/VoiceCallService'; + +interface Props { + visible: boolean; + onClose: () => void; + position?: { x: number, y: number }; + onPositionChange?: (position: { x: number, y: number }) => void; +} + +const DraggableVoiceCallWindow: React.FC = ({ + visible, + onClose, + position = { x: 20, y: 20 }, + onPositionChange +}) => { + const { t } = useTranslation(); + const [isDragging, setIsDragging] = useState(false); + const [currentPosition, setCurrentPosition] = useState(position); + const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 }); + const containerRef = useRef(null); + + // 语音通话状态 + 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: (text) => { + // 这里不设置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' }); + onClose(); + } + }; + + // 添加TTS状态变化事件监听器 + const handleTTSStateChange = (event: CustomEvent) => { + const { isPlaying } = event.detail; + console.log('TTS状态变化事件:', isPlaying); + setIsSpeaking(isPlaying); + }; + + if (visible) { + startVoiceCall(); + // 添加事件监听器 + window.addEventListener('tts-state-change', handleTTSStateChange as EventListener); + } + + return () => { + VoiceCallService.endCall(); + // 移除事件监听器 + window.removeEventListener('tts-state-change', handleTTSStateChange as EventListener); + }; + }, [visible, t]); + + // 拖拽相关处理 + const handleDragStart = (e: React.MouseEvent) => { + if (containerRef.current) { + setIsDragging(true); + const rect = containerRef.current.getBoundingClientRect(); + setDragOffset({ + x: e.clientX - rect.left, + y: e.clientY - rect.top + }); + } + }; + + const handleDrag = (e: MouseEvent) => { + if (isDragging) { + const newPosition = { + x: e.clientX - dragOffset.x, + y: e.clientY - dragOffset.y + }; + setCurrentPosition(newPosition); + onPositionChange?.(newPosition); + } + }; + + const handleDragEnd = () => { + setIsDragging(false); + }; + + useEffect(() => { + if (isDragging) { + document.addEventListener('mousemove', handleDrag); + document.addEventListener('mouseup', handleDragEnd); + } + return () => { + document.removeEventListener('mousemove', handleDrag); + document.removeEventListener('mouseup', handleDragEnd); + }; + }, [isDragging]); + + // 语音通话相关处理 + const toggleMute = () => { + setIsMuted(!isMuted); + VoiceCallService.setMuted(!isMuted); + }; + + const togglePause = () => { + const newPauseState = !isPaused; + setIsPaused(newPauseState); + VoiceCallService.setPaused(newPauseState); + }; + + // 长按说话相关处理 + 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); + } + } + }; + + if (!visible) return null; + + return ( + +
+ + {t('voice_call.title')} + + + +
+ + + + + + + + + {transcript && ( + + {t('voice_call.you')}: {transcript} + + )} + + + + +