mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-24 18:50:56 +08:00
添加了 TTS 相关服务并更新了设置
This commit is contained in:
parent
a7a16272d3
commit
db3293bbb4
@ -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 }} />
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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.",
|
||||
|
||||
@ -1427,7 +1427,6 @@
|
||||
"filter.markdown": "过滤Markdown标记",
|
||||
"filter.code_blocks": "过滤代码块",
|
||||
"filter.html_tags": "过滤HTML标签",
|
||||
"filter.emojis": "过滤表情符号",
|
||||
"max_text_length": "最大文本长度",
|
||||
"test": "测试语音",
|
||||
"help": "语音合成功能支持将文本转换为自然语音。",
|
||||
|
||||
@ -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;
|
||||
@ -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()
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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' })
|
||||
}
|
||||
|
||||
// 不在这里处理响应,因为聊天界面会处理
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}>
|
||||
) => {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user