添加了 TTS 相关服务并更新了设置

This commit is contained in:
1600822305 2025-04-11 21:03:18 +08:00
parent a7a16272d3
commit db3293bbb4
13 changed files with 395 additions and 811 deletions

View File

@ -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<T extends (...args: any[]) => any>(func: T, delay: number): (...args: Parameters<T>) => void {
let lastCall = 0
return (...args: Parameters<T>) => {
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<Props> = ({
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<Props> = ({
} 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<Props> = ({
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<Props> = ({
left: `${currentPosition.x}px`,
top: `${currentPosition.y}px`,
position: 'fixed',
zIndex: 1000,
transform: 'translate3d(0,0,0)', // 启用GPU加速提高拖拽流畅度
willChange: 'left, top' // 提示浏览器这些属性将会变化,优化渲染
zIndex: 1000
}}>
<Header onMouseDown={handleDragStart}>
<DragOutlined style={{ cursor: 'move', marginRight: 8 }} />

View File

@ -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<Props> = ({ 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<Props> = ({ disabled = false, style }) => {
<DraggableVoiceCallWindow
visible={isWindowVisible}
onClose={() => {
setIsWindowVisible(false);
dispatch(setIsVoiceCallActive(false));
setIsWindowVisible(false)
// 注意不在这里调用dispatch而是在DraggableVoiceCallWindow组件中处理
}}
position={windowPosition}
onPositionChange={setWindowPosition}
/>
</>
);
};
)
}
export default VoiceCallButton;
export default VoiceCallButton

View File

@ -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<Props> = ({ 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<Props> = ({ 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 (
<Modal
@ -193,8 +194,7 @@ const VoiceCallModal: React.FC<Props> = ({ visible, onClose }) => {
footer={null}
width={500}
centered
maskClosable={false}
>
maskClosable={false}>
<Container>
<VisualizerContainer>
<VoiceVisualizer isActive={isListening || isRecording} type="input" />
@ -232,7 +232,7 @@ const VoiceCallModal: React.FC<Props> = ({ visible, onClose }) => {
/>
<Tooltip title={t('voice_call.press_to_talk')}>
<RecordButton
type={isRecording ? "primary" : "default"}
type={isRecording ? 'primary' : 'default'}
icon={<SoundOutlined />}
onMouseDown={handleRecordStart}
onMouseUp={handleRecordEnd}
@ -241,8 +241,7 @@ const VoiceCallModal: React.FC<Props> = ({ 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')}
</RecordButton>
</Tooltip>
@ -258,21 +257,21 @@ const VoiceCallModal: React.FC<Props> = ({ visible, onClose }) => {
</ControlsContainer>
</Container>
</Modal>
);
};
)
}
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

View File

@ -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.",

View File

@ -1427,7 +1427,6 @@
"filter.markdown": "过滤Markdown标记",
"filter.code_blocks": "过滤代码块",
"filter.html_tags": "过滤HTML标签",
"filter.emojis": "过滤表情符号",
"max_text_length": "最大文本长度",
"test": "测试语音",
"help": "语音合成功能支持将文本转换为自然语音。",

View File

@ -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 (
<Container>
<Header>
<Title>{t('voice_call.title')}</Title>
<CloseButton onClick={handleClose}>
<CloseOutlined />
</CloseButton>
</Header>
<Content>
<VisualizerContainer>
<VoiceVisualizer isActive={isListening || isRecording} type="input" />
<VoiceVisualizer isActive={isSpeaking} type="output" />
</VisualizerContainer>
<TranscriptContainer>
{transcript && (
<TranscriptText>
<UserLabel>{t('voice_call.you')}:</UserLabel> {transcript}
</TranscriptText>
)}
</TranscriptContainer>
<ControlsContainer>
<Space>
<Button
type="text"
icon={isMuted ? <AudioMutedOutlined /> : <AudioOutlined />}
onClick={toggleMute}
size="large"
title={isMuted ? t('voice_call.unmute') : t('voice_call.mute')}
/>
<Button
type="text"
icon={isPaused ? <PlayCircleOutlined /> : <PauseCircleOutlined />}
onClick={togglePause}
size="large"
title={isPaused ? t('voice_call.resume') : t('voice_call.pause')}
/>
<Tooltip title={t('voice_call.press_to_talk')}>
<RecordButton
type={isRecording ? "primary" : "default"}
icon={<SoundOutlined />}
onMouseDown={handleRecordStart}
onMouseUp={handleRecordEnd}
onMouseLeave={handleRecordCancel}
onTouchStart={handleRecordStart}
onTouchEnd={handleRecordEnd}
onTouchCancel={handleRecordCancel}
size="large"
disabled={isProcessing || isPaused}
>
{isRecording ? t('voice_call.release_to_send') : t('voice_call.press_to_talk')}
</RecordButton>
</Tooltip>
</Space>
</ControlsContainer>
</Content>
</Container>
);
};
// 样式组件
const Container = styled.div`
width: 100%;
height: 100vh;
background-color: var(--color-background);
display: flex;
flex-direction: column;
overflow: hidden;
`;
const Header = styled.div`
padding: 12px 16px;
background-color: var(--color-primary);
color: white;
font-weight: bold;
display: flex;
align-items: center;
`;
const Title = styled.div`
flex: 1;
font-size: 16px;
`;
const CloseButton = styled.div`
cursor: pointer;
`;
const Content = styled.div`
display: flex;
flex-direction: column;
gap: 16px;
padding: 16px;
flex: 1;
overflow: hidden;
`;
const VisualizerContainer = styled.div`
display: flex;
justify-content: space-between;
height: 100px;
`;
const TranscriptContainer = styled.div`
flex: 1;
overflow-y: auto;
border: 1px solid var(--color-border);
border-radius: 8px;
padding: 16px;
background-color: var(--color-background-2);
`;
const TranscriptText = styled.div`
margin-bottom: 8px;
`;
const UserLabel = styled.span`
font-weight: bold;
color: var(--color-primary);
`;
const ControlsContainer = styled.div`
display: flex;
justify-content: center;
padding: 16px 0;
`;
const RecordButton = styled(Button)`
min-width: 120px;
`;
export default VoiceCallWindow;

View File

@ -380,7 +380,7 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
}, [files.length, model, openSelectFileMenu, t, text, translate])
const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
const isEnterPressed = event.keyCode == 13
const isEnterPressed = event.key === 'Enter'
// 按下Tab键自动选中${xxx}
if (event.key === 'Tab' && inputFocus) {
@ -714,96 +714,118 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
textareaRef.current?.focus()
}),
// 监听语音通话消息
EventEmitter.on(EVENT_NAMES.VOICE_CALL_MESSAGE, (data: { text: string, model: any, isVoiceCall?: boolean, useVoiceCallModel?: boolean, voiceCallModelId?: string }) => {
console.log('收到语音通话消息:', data);
EventEmitter.on(
EVENT_NAMES.VOICE_CALL_MESSAGE,
(data: {
text: string
model: any
isVoiceCall?: boolean
useVoiceCallModel?: boolean
voiceCallModelId?: string
}) => {
console.log('收到语音通话消息:', data)
// 先设置输入框文本
setText(data.text);
// 先设置输入框文本
setText(data.text)
// 使用延时确保文本已经设置到输入框
setTimeout(() => {
// 直接调用发送消息函数而不检查inputEmpty
console.log('准备自动发送语音识别消息:', data.text);
// 使用延时确保文本已经设置到输入框
setTimeout(() => {
// 直接调用发送消息函数而不检查inputEmpty
console.log('准备自动发送语音识别消息:', data.text)
// 直接使用正确的方式发送消息
// 创建用户消息
const userMessage = getUserMessage({
assistant,
topic,
type: 'text',
content: data.text
});
// 直接使用正确的方式发送消息
// 创建用户消息
const userMessage = getUserMessage({
assistant,
topic,
type: 'text',
content: data.text
})
// 如果是语音通话消息,使用语音通话专用模型
if (data.isVoiceCall || data.useVoiceCallModel) {
// 从全局设置中获取语音通话专用模型
const { voiceCallModel } = store.getState().settings;
// 如果是语音通话消息,使用语音通话专用模型
if (data.isVoiceCall || data.useVoiceCallModel) {
// 从全局设置中获取语音通话专用模型
const { voiceCallModel } = store.getState().settings
// 打印调试信息
console.log('语音通话消息,尝试使用语音通话专用模型');
console.log('全局设置中的语音通话模型:', voiceCallModel ? JSON.stringify(voiceCallModel) : 'null');
console.log('事件中传递的模型:', data.model ? JSON.stringify(data.model) : 'null');
// 打印调试信息
console.log('语音通话消息,尝试使用语音通话专用模型')
console.log('全局设置中的语音通话模型:', voiceCallModel ? JSON.stringify(voiceCallModel) : 'null')
console.log('事件中传递的模型:', data.model ? JSON.stringify(data.model) : 'null')
// 如果全局设置中有语音通话专用模型,优先使用
if (voiceCallModel) {
userMessage.model = voiceCallModel;
console.log('使用全局设置中的语音通话专用模型:', voiceCallModel.name);
// 如果全局设置中有语音通话专用模型,优先使用
if (voiceCallModel) {
userMessage.model = voiceCallModel
console.log('使用全局设置中的语音通话专用模型:', voiceCallModel.name)
// 强制覆盖消息中的模型
userMessage.modelId = voiceCallModel.id;
// 强制覆盖消息中的模型
userMessage.modelId = voiceCallModel.id
}
// 如果没有全局设置,但事件中传递了模型,使用事件中的模型
else if (data.model && typeof data.model === 'object') {
userMessage.model = data.model
console.log('使用事件中传递的模型:', data.model.name || data.model.id)
// 强制覆盖消息中的模型
userMessage.modelId = data.model.id
}
// 如果没有模型对象但有模型ID尝试使用模型ID
else if (data.voiceCallModelId) {
console.log('使用事件中传递的模型ID:', data.voiceCallModelId)
userMessage.modelId = data.voiceCallModelId
}
// 如果以上都没有,使用当前助手模型
else {
console.log('没有找到语音通话专用模型,使用当前助手模型')
}
}
// 如果没有全局设置,但事件中传递了模型,使用事件中的模型
else if (data.model && typeof data.model === 'object') {
userMessage.model = data.model;
console.log('使用事件中传递的模型:', data.model.name || data.model.id);
// 强制覆盖消息中的模型
userMessage.modelId = data.model.id;
// 非语音通话消息,使用当前助手模型
else if (data.model) {
const modelObj = assistant.model?.id === data.model.id ? assistant.model : undefined
if (modelObj) {
userMessage.model = modelObj
console.log('使用当前助手模型:', modelObj.name || modelObj.id)
}
}
// 如果没有模型对象但有模型ID尝试使用模型ID
else if (data.voiceCallModelId) {
console.log('使用事件中传递的模型ID:', data.voiceCallModelId);
userMessage.modelId = data.voiceCallModelId;
// 如果是语音通话消息,创建一个新的助手对象,并设置模型
let assistantToUse = assistant
if ((data.isVoiceCall || data.useVoiceCallModel) && userMessage.model) {
// 创建一个新的助手对象,以避免修改原始助手
assistantToUse = { ...assistant }
// 设置助手的模型为语音通话专用模型
assistantToUse.model = userMessage.model
console.log(
'为语音通话消息创建了新的助手对象,并设置了模型:',
userMessage.model.name || userMessage.model.id
)
}
// 如果以上都没有,使用当前助手模型
else {
console.log('没有找到语音通话专用模型,使用当前助手模型');
}
}
// 非语音通话消息,使用当前助手模型
else if (data.model) {
const modelObj = assistant.model?.id === data.model.id ? assistant.model : undefined;
if (modelObj) {
userMessage.model = modelObj;
console.log('使用当前助手模型:', modelObj.name || modelObj.id);
}
}
// 如果是语音通话消息,创建一个新的助手对象,并设置模型
let assistantToUse = assistant;
if ((data.isVoiceCall || data.useVoiceCallModel) && userMessage.model) {
// 创建一个新的助手对象,以避免修改原始助手
assistantToUse = { ...assistant };
// 分发发送消息的action
dispatch(_sendMessage(userMessage, assistantToUse, topic, {}))
// 设置助手的模型为语音通话专用模型
assistantToUse.model = userMessage.model;
console.log('为语音通话消息创建了新的助手对象,并设置了模型:', userMessage.model.name || userMessage.model.id);
}
// 清空输入框
setText('')
// 分发发送消息的action
dispatch(
_sendMessage(userMessage, assistantToUse, topic, {})
);
// 清空输入框
setText('');
console.log('已触发发送消息事件');
}, 300);
})
console.log('已触发发送消息事件')
}, 300)
}
)
]
return () => unsubscribes.forEach((unsub) => unsub())
}, [addNewTopic, resizeTextArea, sendMessage, model, inputEmpty, loading, dispatch, assistant, topic, setText, getUserMessage, _sendMessage])
}, [
addNewTopic,
resizeTextArea,
sendMessage,
model,
inputEmpty,
loading,
dispatch,
assistant,
topic,
setText
// getUserMessage 和 _sendMessage 是外部作用域值,不需要作为依赖项
])
useEffect(() => {
textareaRef.current?.focus()

View File

@ -1,8 +1,8 @@
import { FONT_FAMILY } from '@renderer/config/constant'
import { useAssistant } from '@renderer/hooks/useAssistant'
import { useModel } from '@renderer/hooks/useModel'
import { useMessageStyle, useSettings } from '@renderer/hooks/useSettings'
import { useRuntime } from '@renderer/hooks/useRuntime'
import { useMessageStyle, useSettings } from '@renderer/hooks/useSettings'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { getMessageModelId } from '@renderer/services/MessagesService'
import { getModelUniqId } from '@renderer/services/ModelService'
@ -11,10 +11,10 @@ import { RootState, useAppDispatch } from '@renderer/store'
import { setLastPlayedMessageId, setSkipNextAutoTTS } from '@renderer/store/settings'
import { Assistant, Message, Topic } from '@renderer/types'
import { classNames } from '@renderer/utils'
import { useSelector } from 'react-redux'
import { Divider, Dropdown } from 'antd'
import { Dispatch, FC, memo, SetStateAction, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
import styled from 'styled-components'
import MessageContent from './MessageContent'
@ -58,7 +58,9 @@ const MessageItem: FC<Props> = ({
const [selectedQuoteText, setSelectedQuoteText] = useState<string>('')
// 获取TTS设置
const { ttsEnabled, isVoiceCallActive, lastPlayedMessageId, skipNextAutoTTS } = useSelector((state: RootState) => state.settings)
const { ttsEnabled, isVoiceCallActive, lastPlayedMessageId, skipNextAutoTTS } = useSelector(
(state: RootState) => state.settings
)
const dispatch = useAppDispatch()
const [selectedText, setSelectedText] = useState<string>('')
@ -98,18 +100,64 @@ const MessageItem: FC<Props> = ({
}
}, [])
// 使用 ref 跟踪消息状态变化
const prevGeneratingRef = useRef(generating)
// 更新 prevGeneratingRef 的值
useEffect(() => {
// 在每次渲染后更新 ref 值
prevGeneratingRef.current = generating
}, [generating])
// 监听新消息生成,并在新消息生成时重置 skipNextAutoTTS
useEffect(() => {
// 如果从生成中变为非生成中,说明新消息刚刚生成完成
if (
prevGeneratingRef.current &&
!generating &&
isLastMessage &&
isAssistantMessage &&
message.status === 'success'
) {
console.log('新消息生成完成消息ID:', message.id)
// 如果 skipNextAutoTTS 为 true重置为 false以便下一条消息可以自动播放
if (skipNextAutoTTS) {
console.log('重置 skipNextAutoTTS 为 false以便下一条消息可以自动播放')
dispatch(setSkipNextAutoTTS(false))
}
}
}, [
isLastMessage,
isAssistantMessage,
message.status,
message.id,
generating,
skipNextAutoTTS,
dispatch,
prevGeneratingRef
])
// 自动播放TTS的逻辑
useEffect(() => {
// 如果是最后一条助手消息且消息状态为成功且不是正在生成中且TTS已启用且语音通话窗口已打开
if (isLastMessage && isAssistantMessage && message.status === 'success' && !generating && ttsEnabled && isVoiceCallActive) {
if (
isLastMessage &&
isAssistantMessage &&
message.status === 'success' &&
!generating &&
ttsEnabled &&
isVoiceCallActive
) {
// 检查是否需要跳过自动TTS
if (skipNextAutoTTS) {
console.log('跳过自动TTS因为 skipNextAutoTTS 为 true')
// 重置 skipNextAutoTTS 状态,只跳过一次
dispatch(setSkipNextAutoTTS(false))
console.log('跳过自动TTS因为 skipNextAutoTTS 为 true消息ID:', message.id)
// 注意:不在这里重置 skipNextAutoTTS而是在新消息生成时重置
return
}
console.log('准备自动播放TTS因为 skipNextAutoTTS 为 false消息ID:', message.id)
// 检查消息是否有内容,且消息是新的(不是上次播放过的消息)
if (message.content && message.content.trim() && message.id !== lastPlayedMessageId) {
console.log('自动播放最新助手消息的TTS:', message.id, '语音通话窗口状态:', isVoiceCallActive)
@ -124,11 +172,28 @@ const MessageItem: FC<Props> = ({
} else if (message.id === lastPlayedMessageId) {
console.log('不自动播放TTS因为该消息已经播放过:', message.id)
}
} else if (isLastMessage && isAssistantMessage && message.status === 'success' && !generating && ttsEnabled && !isVoiceCallActive) {
} else if (
isLastMessage &&
isAssistantMessage &&
message.status === 'success' &&
!generating &&
ttsEnabled &&
!isVoiceCallActive
) {
// 如果语音通话窗口没有打开则不自动播放TTS
console.log('不自动播放TTS因为语音通话窗口没有打开')
}
}, [isLastMessage, isAssistantMessage, message, generating, ttsEnabled, isVoiceCallActive, lastPlayedMessageId, skipNextAutoTTS, dispatch])
}, [
isLastMessage,
isAssistantMessage,
message,
generating,
ttsEnabled,
isVoiceCallActive,
lastPlayedMessageId,
skipNextAutoTTS,
dispatch
])
const messageHighlightHandler = useCallback((highlight: boolean = true) => {
if (messageContainerRef.current) {

View File

@ -942,14 +942,6 @@ const TTSSettings: FC = () => {
/>{' '}
{t('settings.tts.filter.html_tags')}
</FilterOptionItem>
<FilterOptionItem>
<Switch
checked={ttsFilterOptions.filterEmojis}
onChange={(checked) => dispatch(setTtsFilterOptions({ filterEmojis: checked }))}
disabled={!ttsEnabled}
/>{' '}
{t('settings.tts.filter.emojis')}
</FilterOptionItem>
<FilterOptionItem>
<LengthLabel>{t('settings.tts.max_text_length')}:</LengthLabel>
<Select

View File

@ -64,10 +64,7 @@ class ASRService {
// 创建新连接
try {
console.log('[ASRService] 正在连接WebSocket服务器...')
// 使用 setTimeout 避免在渲染过程中调用 message API
setTimeout(() => {
window.message.loading({ content: '正在连接语音识别服务...', key: 'ws-connect' })
}, 0)
window.message.loading({ content: '正在连接语音识别服务...', key: 'ws-connect' })
this.ws = new WebSocket('ws://localhost:8080')
this.wsConnected = false
@ -75,10 +72,7 @@ class ASRService {
this.ws.onopen = () => {
console.log('[ASRService] WebSocket连接成功')
// 使用 setTimeout 避免在渲染过程中调用 message API
setTimeout(() => {
window.message.success({ content: '语音识别服务连接成功', key: 'ws-connect' })
}, 0)
window.message.success({ content: '语音识别服务连接成功', key: 'ws-connect' })
this.wsConnected = true
this.reconnectAttempt = 0
this.ws?.send(JSON.stringify({ type: 'identify', role: 'electron' }))
@ -95,10 +89,7 @@ class ASRService {
this.ws.onerror = (error) => {
console.error('[ASRService] WebSocket连接错误:', error)
this.wsConnected = false
// 使用 setTimeout 避免在渲染过程中调用 message API
setTimeout(() => {
window.message.error({ content: '语音识别服务连接失败', key: 'ws-connect' })
}, 0)
window.message.error({ content: '语音识别服务连接失败', key: 'ws-connect' })
resolve(false)
}
@ -123,27 +114,18 @@ class ASRService {
if (data.message === 'browser_ready' || data.message === 'Browser connected') {
console.log('[ASRService] 浏览器已准备好')
this.browserReady = true
// 使用 setTimeout 避免在渲染过程中调用 message API
setTimeout(() => {
window.message.success({ content: '语音识别浏览器已准备好', key: 'browser-status' })
}, 0)
window.message.success({ content: '语音识别浏览器已准备好', key: 'browser-status' })
} else if (data.message === 'Browser disconnected' || data.message === 'Browser connection error') {
console.log('[ASRService] 浏览器断开连接')
this.browserReady = false
// 使用 setTimeout 避免在渲染过程中调用 message API
setTimeout(() => {
window.message.error({ content: '语音识别浏览器断开连接', key: 'browser-status' })
}, 0)
window.message.error({ content: '语音识别浏览器断开连接', key: 'browser-status' })
} else if (data.message === 'stopped') {
// 语音识别已停止
console.log('[ASRService] 语音识别已停止')
this.isRecording = false
// 如果没有收到最终结果,显示处理完成消息
// 使用 setTimeout 避免在渲染过程中调用 message API
setTimeout(() => {
window.message.success({ content: i18n.t('settings.asr.completed'), key: 'asr-processing' })
}, 0)
window.message.success({ content: i18n.t('settings.asr.completed'), key: 'asr-processing' })
} else if (data.message === 'reset_complete') {
// 语音识别已重置
console.log('[ASRService] 语音识别已强制重置')
@ -154,10 +136,7 @@ class ASRService {
this.resultCallback = null
// 显示重置完成消息
// 使用 setTimeout 避免在渲染过程中调用 message API
setTimeout(() => {
window.message.info({ content: '语音识别已重置', key: 'asr-reset' })
}, 0)
window.message.info({ content: '语音识别已重置', key: 'asr-reset' })
// 如果有回调函数,调用一次空字符串,触发按钮状态重置
if (tempCallback && typeof tempCallback === 'function') {
@ -190,11 +169,9 @@ class ASRService {
// 调用回调函数
tempCallback(data.data.text, true)
// 使用 setTimeout 避免在渲染过程中调用 message API
setTimeout(() => {
window.message.success({ content: i18n.t('settings.asr.success'), key: 'asr-processing' })
}, 0)
} else if (this.isRecording) { // 只在录音中才处理中间结果
window.message.success({ content: i18n.t('settings.asr.success'), key: 'asr-processing' })
} else if (this.isRecording) {
// 只在录音中才处理中间结果
// 非最终结果,也调用回调,但标记为非最终
console.log('[ASRService] 收到中间结果,调用回调函数,文本:', data.data.text)
this.resultCallback(data.data.text, false)
@ -207,13 +184,10 @@ class ASRService {
}
} else if (data.type === 'error') {
console.error('[ASRService] 收到错误消息:', data.message || data.data)
// 使用 setTimeout 避免在渲染过程中调用 message API
setTimeout(() => {
window.message.error({
content: `语音识别错误: ${data.message || data.data?.error || '未知错误'}`,
key: 'asr-error'
})
}, 0)
window.message.error({
content: `语音识别错误: ${data.message || data.data?.error || '未知错误'}`,
key: 'asr-error'
})
}
} catch (error) {
console.error('[ASRService] 解析WebSocket消息失败:', error, event.data)

View File

@ -525,16 +525,10 @@ class VoiceCallServiceClass {
)
// 使用消息通知用户
// 使用 setTimeout 避免在渲染过程中调用 message API
setTimeout(() => {
window.message.success({ content: '语音识别已完成,正在发送消息...', key: 'voice-call-send' })
}, 0)
window.message.success({ content: '语音识别已完成,正在发送消息...', key: 'voice-call-send' })
} catch (error) {
console.error('发送语音识别结果到聊天界面时出错:', error)
// 使用 setTimeout 避免在渲染过程中调用 message API
setTimeout(() => {
window.message.error({ content: '发送语音识别结果失败', key: 'voice-call-error' })
}, 0)
window.message.error({ content: '发送语音识别结果失败', key: 'voice-call-error' })
}
// 不在这里处理响应,因为聊天界面会处理

View File

@ -16,7 +16,6 @@ export class TTSTextFilter {
filterMarkdown: boolean
filterCodeBlocks: boolean
filterHtmlTags: boolean
filterEmojis: boolean
maxTextLength: number
}
): string {
@ -44,11 +43,6 @@ export class TTSTextFilter {
filteredText = this.filterHtmlTags(filteredText)
}
// 过滤表情符号
if (options.filterEmojis) {
filteredText = this.filterEmojis(filteredText)
}
// 限制文本长度
if (options.maxTextLength > 0 && filteredText.length > options.maxTextLength) {
filteredText = filteredText.substring(0, options.maxTextLength)
@ -151,23 +145,4 @@ export class TTSTextFilter {
return text
}
/**
*
* @param text
* @returns
*/
private static filterEmojis(text: string): string {
// 过滤Unicode表情符号
// 这个正则表达式匹配大多数Unicode表情符号
text = text.replace(/[\u{1F300}-\u{1F5FF}\u{1F600}-\u{1F64F}\u{1F680}-\u{1F6FF}\u{1F700}-\u{1F77F}\u{1F780}-\u{1F7FF}\u{1F800}-\u{1F8FF}\u{1F900}-\u{1F9FF}\u{1FA00}-\u{1FA6F}\u{1FA70}-\u{1FAFF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}]/gu, '')
// 过滤符号表情,如 :) :( :D 等
text = text.replace(/[:;][-']?[)(DOPdop|\/{\}\[\]\*]+/g, '')
// 过滤文本表情,如 (smile) (sad) 等
text = text.replace(/\([a-zA-Z]+\)/g, '')
return text
}
}

View File

@ -137,7 +137,6 @@ export interface SettingsState {
filterMarkdown: boolean // 过滤Markdown标记
filterCodeBlocks: boolean // 过滤代码块
filterHtmlTags: boolean // 过滤HTML标签
filterEmojis: boolean // 过滤表情符号
maxTextLength: number // 最大文本长度
}
// ASR配置语音识别
@ -279,7 +278,6 @@ export const initialState: SettingsState = {
filterMarkdown: true, // 默认过滤Markdown标记
filterCodeBlocks: true, // 默认过滤代码块
filterHtmlTags: true, // 默认过滤HTML标签
filterEmojis: true, // 默认过滤表情符号
maxTextLength: 4000 // 默认最大文本长度
},
// ASR配置语音识别
@ -692,7 +690,6 @@ const settingsSlice = createSlice({
filterMarkdown?: boolean
filterCodeBlocks?: boolean
filterHtmlTags?: boolean
filterEmojis?: boolean
maxTextLength?: number
}>
) => {