mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-30 07:39:06 +08:00
修复部分问题
This commit is contained in:
parent
644995dd76
commit
a4eeea6732
@ -3,12 +3,10 @@ import { replaceDevtoolsFont } from '@main/utils/windowUtil'
|
|||||||
import { IpcChannel } from '@shared/IpcChannel'
|
import { IpcChannel } from '@shared/IpcChannel'
|
||||||
import { app, ipcMain } from 'electron'
|
import { app, ipcMain } from 'electron'
|
||||||
import installExtension, { REACT_DEVELOPER_TOOLS, REDUX_DEVTOOLS } from 'electron-devtools-installer'
|
import installExtension, { REACT_DEVELOPER_TOOLS, REDUX_DEVTOOLS } from 'electron-devtools-installer'
|
||||||
import Logger from 'electron-log'
|
|
||||||
|
|
||||||
import { registerIpc } from './ipc'
|
import { registerIpc } from './ipc'
|
||||||
import { configManager } from './services/ConfigManager'
|
import { configManager } from './services/ConfigManager'
|
||||||
import { registerMsTTSIpcHandlers } from './services/MsTTSIpcHandler'
|
import { registerMsTTSIpcHandlers } from './services/MsTTSIpcHandler'
|
||||||
import mcpService from './services/MCPService'
|
|
||||||
import { CHERRY_STUDIO_PROTOCOL, handleProtocolUrl, registerProtocolClient } from './services/ProtocolClient'
|
import { CHERRY_STUDIO_PROTOCOL, handleProtocolUrl, registerProtocolClient } from './services/ProtocolClient'
|
||||||
import { registerShortcuts } from './services/ShortcutService'
|
import { registerShortcuts } from './services/ShortcutService'
|
||||||
import { TrayService } from './services/TrayService'
|
import { TrayService } from './services/TrayService'
|
||||||
@ -98,15 +96,6 @@ if (!app.requestSingleInstanceLock()) {
|
|||||||
app.isQuitting = true
|
app.isQuitting = true
|
||||||
})
|
})
|
||||||
|
|
||||||
app.on('will-quit', async () => {
|
|
||||||
// event.preventDefault()
|
|
||||||
try {
|
|
||||||
await mcpService.cleanup()
|
|
||||||
} catch (error) {
|
|
||||||
Logger.error('Error cleaning up MCP service:', error)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// In this file you can include the rest of your app"s specific main process
|
// In this file you can include the rest of your app"s specific main process
|
||||||
// code. You can also put them in separate files and require them here.
|
// code. You can also put them in separate files and require them here.
|
||||||
}
|
}
|
||||||
|
|||||||
@ -322,30 +322,6 @@ class McpService {
|
|||||||
// 转换回字符串
|
// 转换回字符串
|
||||||
return Array.from(existingPaths).join(pathSeparator)
|
return Array.from(existingPaths).join(pathSeparator)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 清理所有MCP客户端连接
|
|
||||||
*/
|
|
||||||
public async cleanup(): Promise<void> {
|
|
||||||
Logger.info('[MCP] Cleaning up all MCP clients...')
|
|
||||||
const closePromises: Promise<void>[] = []
|
|
||||||
|
|
||||||
// 关闭所有客户端连接
|
|
||||||
for (const [serverKey, client] of this.clients.entries()) {
|
|
||||||
try {
|
|
||||||
Logger.info(`[MCP] Closing client for server: ${serverKey}`)
|
|
||||||
closePromises.push(client.close())
|
|
||||||
} catch (error) {
|
|
||||||
Logger.error(`[MCP] Error closing client for server: ${serverKey}`, error)
|
|
||||||
} finally {
|
|
||||||
this.clients.delete(serverKey)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 等待所有关闭操作完成
|
|
||||||
await Promise.allSettled(closePromises)
|
|
||||||
Logger.info('[MCP] All MCP clients cleaned up')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new McpService()
|
export default new McpService()
|
||||||
|
|||||||
@ -1,36 +1,35 @@
|
|||||||
import { LoadingOutlined, PhoneOutlined } from '@ant-design/icons'
|
import React, { useState } from 'react';
|
||||||
import { Button, Tooltip } from 'antd'
|
import { Button, Tooltip } from 'antd';
|
||||||
import React, { useState } from 'react'
|
import { PhoneOutlined, LoadingOutlined } from '@ant-design/icons';
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import VoiceCallModal from './VoiceCallModal';
|
||||||
import { VoiceCallService } from '../services/VoiceCallService'
|
import { VoiceCallService } from '../services/VoiceCallService';
|
||||||
import VoiceCallModal from './VoiceCallModal'
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
disabled?: boolean
|
disabled?: boolean;
|
||||||
style?: React.CSSProperties
|
style?: React.CSSProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
const VoiceCallButton: React.FC<Props> = ({ disabled = false, style }) => {
|
const VoiceCallButton: React.FC<Props> = ({ disabled = false, style }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation();
|
||||||
const [isModalVisible, setIsModalVisible] = useState(false)
|
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
const handleClick = async () => {
|
const handleClick = async () => {
|
||||||
if (disabled || isLoading) return
|
if (disabled || isLoading) return;
|
||||||
|
|
||||||
setIsLoading(true)
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
// 初始化语音服务
|
// 初始化语音服务
|
||||||
await VoiceCallService.initialize()
|
await VoiceCallService.initialize();
|
||||||
setIsModalVisible(true)
|
setIsModalVisible(true);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to initialize voice call:', error)
|
console.error('Failed to initialize voice call:', error);
|
||||||
window.message.error(t('voice_call.initialization_failed'))
|
window.message.error(t('voice_call.initialization_failed'));
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false)
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -43,9 +42,14 @@ const VoiceCallButton: React.FC<Props> = ({ disabled = false, style }) => {
|
|||||||
style={style}
|
style={style}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{isModalVisible && <VoiceCallModal visible={isModalVisible} onClose={() => setIsModalVisible(false)} />}
|
{isModalVisible && (
|
||||||
|
<VoiceCallModal
|
||||||
|
visible={isModalVisible}
|
||||||
|
onClose={() => setIsModalVisible(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default VoiceCallButton
|
export default VoiceCallButton;
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { Modal, Button, Space, Tooltip } from 'antd';
|
||||||
import {
|
import {
|
||||||
AudioMutedOutlined,
|
AudioMutedOutlined,
|
||||||
AudioOutlined,
|
AudioOutlined,
|
||||||
@ -5,128 +7,125 @@ import {
|
|||||||
PauseCircleOutlined,
|
PauseCircleOutlined,
|
||||||
PlayCircleOutlined,
|
PlayCircleOutlined,
|
||||||
SoundOutlined
|
SoundOutlined
|
||||||
} from '@ant-design/icons'
|
} from '@ant-design/icons';
|
||||||
import { Button, Modal, Space, Tooltip } from 'antd'
|
import styled from 'styled-components';
|
||||||
import React, { useCallback, useEffect, useState } from 'react'
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useTranslation } from 'react-i18next'
|
import VoiceVisualizer from './VoiceVisualizer';
|
||||||
import styled from 'styled-components'
|
import { VoiceCallService } from '../services/VoiceCallService';
|
||||||
|
|
||||||
import { VoiceCallService } from '../services/VoiceCallService'
|
|
||||||
import VoiceVisualizer from './VoiceVisualizer'
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
visible: boolean
|
visible: boolean;
|
||||||
onClose: () => void
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const VoiceCallModal: React.FC<Props> = ({ visible, onClose }) => {
|
const VoiceCallModal: React.FC<Props> = ({ visible, onClose }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation();
|
||||||
const [isMuted, setIsMuted] = useState(false)
|
const [isMuted, setIsMuted] = useState(false);
|
||||||
const [isPaused, setIsPaused] = useState(false)
|
const [isPaused, setIsPaused] = useState(false);
|
||||||
const [transcript, setTranscript] = useState('')
|
const [transcript, setTranscript] = useState('');
|
||||||
const [response, setResponse] = useState('')
|
const [response, setResponse] = useState('');
|
||||||
const [isListening, setIsListening] = useState(false)
|
const [isListening, setIsListening] = useState(false);
|
||||||
const [isSpeaking, setIsSpeaking] = useState(false)
|
const [isSpeaking, setIsSpeaking] = useState(false);
|
||||||
const [isRecording, setIsRecording] = useState(false)
|
const [isRecording, setIsRecording] = useState(false);
|
||||||
const [isProcessing, setIsProcessing] = useState(false)
|
const [isProcessing, setIsProcessing] = useState(false);
|
||||||
|
|
||||||
const handleClose = useCallback(() => {
|
|
||||||
VoiceCallService.endCall()
|
|
||||||
onClose()
|
|
||||||
}, [onClose])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const startVoiceCall = async () => {
|
const startVoiceCall = async () => {
|
||||||
try {
|
try {
|
||||||
await VoiceCallService.startCall({
|
await VoiceCallService.startCall({
|
||||||
onTranscript: (text: string) => setTranscript(text),
|
onTranscript: (text) => setTranscript(text),
|
||||||
onResponse: (text: string) => setResponse(text),
|
onResponse: (text) => setResponse(text),
|
||||||
onListeningStateChange: setIsListening,
|
onListeningStateChange: setIsListening,
|
||||||
onSpeakingStateChange: setIsSpeaking
|
onSpeakingStateChange: setIsSpeaking,
|
||||||
})
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Voice call error:', error)
|
console.error('Voice call error:', error);
|
||||||
window.message.error(t('voice_call.error'))
|
window.message.error(t('voice_call.error'));
|
||||||
handleClose()
|
handleClose();
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
if (visible) {
|
if (visible) {
|
||||||
startVoiceCall()
|
startVoiceCall();
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
VoiceCallService.endCall()
|
VoiceCallService.endCall();
|
||||||
}
|
};
|
||||||
}, [visible, t, handleClose])
|
}, [visible, t]);
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
VoiceCallService.endCall();
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
const toggleMute = () => {
|
const toggleMute = () => {
|
||||||
const newMuteState = !isMuted
|
const newMuteState = !isMuted;
|
||||||
setIsMuted(newMuteState)
|
setIsMuted(newMuteState);
|
||||||
VoiceCallService.setMuted(newMuteState)
|
VoiceCallService.setMuted(newMuteState);
|
||||||
}
|
};
|
||||||
|
|
||||||
const togglePause = () => {
|
const togglePause = () => {
|
||||||
const newPauseState = !isPaused
|
const newPauseState = !isPaused;
|
||||||
setIsPaused(newPauseState)
|
setIsPaused(newPauseState);
|
||||||
VoiceCallService.setPaused(newPauseState)
|
VoiceCallService.setPaused(newPauseState);
|
||||||
}
|
};
|
||||||
|
|
||||||
// 长按说话相关处理
|
// 长按说话相关处理
|
||||||
const handleRecordStart = async (e: React.MouseEvent | React.TouchEvent) => {
|
const handleRecordStart = async (e: React.MouseEvent | React.TouchEvent) => {
|
||||||
e.preventDefault() // 防止触摸事件的默认行为
|
e.preventDefault(); // 防止触摸事件的默认行为
|
||||||
|
|
||||||
if (isProcessing || isPaused) return
|
if (isProcessing || isPaused) return;
|
||||||
|
|
||||||
setIsRecording(true)
|
setIsRecording(true);
|
||||||
await VoiceCallService.startRecording()
|
await VoiceCallService.startRecording();
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleRecordEnd = async (e: React.MouseEvent | React.TouchEvent) => {
|
const handleRecordEnd = async (e: React.MouseEvent | React.TouchEvent) => {
|
||||||
e.preventDefault() // 防止触摸事件的默认行为
|
e.preventDefault(); // 防止触摸事件的默认行为
|
||||||
|
|
||||||
if (!isRecording) return
|
if (!isRecording) return;
|
||||||
|
|
||||||
// 立即更新UI状态
|
// 立即更新UI状态
|
||||||
setIsRecording(false)
|
setIsRecording(false);
|
||||||
setIsProcessing(true)
|
setIsProcessing(true);
|
||||||
|
|
||||||
// 确保录音完全停止
|
// 确保录音完全停止
|
||||||
try {
|
try {
|
||||||
await VoiceCallService.stopRecording()
|
await VoiceCallService.stopRecording();
|
||||||
console.log('录音已停止')
|
console.log('录音已停止');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('停止录音出错:', error)
|
console.error('停止录音出错:', error);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理结果会通过回调函数返回,不需要在这里处理
|
// 处理结果会通过回调函数返回,不需要在这里处理
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setIsProcessing(false)
|
setIsProcessing(false);
|
||||||
}, 500) // 添加短暂延迟,防止用户立即再次点击
|
}, 500); // 添加短暂延迟,防止用户立即再次点击
|
||||||
}
|
};
|
||||||
|
|
||||||
// 处理鼠标/触摸离开按钮的情况
|
// 处理鼠标/触摸离开按钮的情况
|
||||||
const handleRecordCancel = async (e: React.MouseEvent | React.TouchEvent) => {
|
const handleRecordCancel = async (e: React.MouseEvent | React.TouchEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault();
|
||||||
|
|
||||||
if (isRecording) {
|
if (isRecording) {
|
||||||
// 立即更新UI状态
|
// 立即更新UI状态
|
||||||
setIsRecording(false)
|
setIsRecording(false);
|
||||||
setIsProcessing(true)
|
setIsProcessing(true);
|
||||||
|
|
||||||
// 取消录音,不发送给AI
|
// 取消录音,不发送给AI
|
||||||
try {
|
try {
|
||||||
await VoiceCallService.cancelRecording()
|
await VoiceCallService.cancelRecording();
|
||||||
console.log('录音已取消')
|
console.log('录音已取消');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('取消录音出错:', error)
|
console.error('取消录音出错:', error);
|
||||||
}
|
}
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setIsProcessing(false)
|
setIsProcessing(false);
|
||||||
}, 500)
|
}, 500);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
@ -136,7 +135,8 @@ const VoiceCallModal: React.FC<Props> = ({ visible, onClose }) => {
|
|||||||
footer={null}
|
footer={null}
|
||||||
width={500}
|
width={500}
|
||||||
centered
|
centered
|
||||||
maskClosable={false}>
|
maskClosable={false}
|
||||||
|
>
|
||||||
<Container>
|
<Container>
|
||||||
<VisualizerContainer>
|
<VisualizerContainer>
|
||||||
<VoiceVisualizer isActive={isListening || isRecording} type="input" />
|
<VoiceVisualizer isActive={isListening || isRecording} type="input" />
|
||||||
@ -174,7 +174,7 @@ const VoiceCallModal: React.FC<Props> = ({ visible, onClose }) => {
|
|||||||
/>
|
/>
|
||||||
<Tooltip title={t('voice_call.press_to_talk')}>
|
<Tooltip title={t('voice_call.press_to_talk')}>
|
||||||
<RecordButton
|
<RecordButton
|
||||||
type={isRecording ? 'primary' : 'default'}
|
type={isRecording ? "primary" : "default"}
|
||||||
icon={<SoundOutlined />}
|
icon={<SoundOutlined />}
|
||||||
onMouseDown={handleRecordStart}
|
onMouseDown={handleRecordStart}
|
||||||
onMouseUp={handleRecordEnd}
|
onMouseUp={handleRecordEnd}
|
||||||
@ -183,7 +183,8 @@ const VoiceCallModal: React.FC<Props> = ({ visible, onClose }) => {
|
|||||||
onTouchEnd={handleRecordEnd}
|
onTouchEnd={handleRecordEnd}
|
||||||
onTouchCancel={handleRecordCancel}
|
onTouchCancel={handleRecordCancel}
|
||||||
size="large"
|
size="large"
|
||||||
disabled={isProcessing || isPaused}>
|
disabled={isProcessing || isPaused}
|
||||||
|
>
|
||||||
{isRecording ? t('voice_call.release_to_send') : t('voice_call.press_to_talk')}
|
{isRecording ? t('voice_call.release_to_send') : t('voice_call.press_to_talk')}
|
||||||
</RecordButton>
|
</RecordButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@ -199,21 +200,21 @@ const VoiceCallModal: React.FC<Props> = ({ visible, onClose }) => {
|
|||||||
</ControlsContainer>
|
</ControlsContainer>
|
||||||
</Container>
|
</Container>
|
||||||
</Modal>
|
</Modal>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
const Container = styled.div`
|
const Container = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
height: 400px;
|
height: 400px;
|
||||||
`
|
`;
|
||||||
|
|
||||||
const VisualizerContainer = styled.div`
|
const VisualizerContainer = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
height: 100px;
|
height: 100px;
|
||||||
`
|
`;
|
||||||
|
|
||||||
const TranscriptContainer = styled.div`
|
const TranscriptContainer = styled.div`
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@ -222,33 +223,33 @@ const TranscriptContainer = styled.div`
|
|||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
background-color: var(--color-background-2);
|
background-color: var(--color-background-2);
|
||||||
`
|
`;
|
||||||
|
|
||||||
const TranscriptText = styled.p`
|
const TranscriptText = styled.p`
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
color: var(--color-text-1);
|
color: var(--color-text-1);
|
||||||
`
|
`;
|
||||||
|
|
||||||
const ResponseText = styled.p`
|
const ResponseText = styled.p`
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
color: var(--color-primary);
|
color: var(--color-primary);
|
||||||
`
|
`;
|
||||||
|
|
||||||
const UserLabel = styled.span`
|
const UserLabel = styled.span`
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: var(--color-text-1);
|
color: var(--color-text-1);
|
||||||
`
|
`;
|
||||||
|
|
||||||
const AILabel = styled.span`
|
const AILabel = styled.span`
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: var(--color-primary);
|
color: var(--color-primary);
|
||||||
`
|
`;
|
||||||
|
|
||||||
const ControlsContainer = styled.div`
|
const ControlsContainer = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 10px 0;
|
padding: 10px 0;
|
||||||
`
|
`;
|
||||||
|
|
||||||
const RecordButton = styled(Button)`
|
const RecordButton = styled(Button)`
|
||||||
min-width: 150px;
|
min-width: 150px;
|
||||||
@ -257,6 +258,6 @@ const RecordButton = styled(Button)`
|
|||||||
&:active {
|
&:active {
|
||||||
transform: scale(0.95);
|
transform: scale(0.95);
|
||||||
}
|
}
|
||||||
`
|
`;
|
||||||
|
|
||||||
export default VoiceCallModal
|
export default VoiceCallModal;
|
||||||
|
|||||||
@ -1,74 +1,74 @@
|
|||||||
import React, { useEffect, useRef } from 'react'
|
import React, { useEffect, useRef } from 'react';
|
||||||
import { useTranslation } from 'react-i18next'
|
import styled from 'styled-components';
|
||||||
import styled from 'styled-components'
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
isActive: boolean
|
isActive: boolean;
|
||||||
type: 'input' | 'output'
|
type: 'input' | 'output';
|
||||||
}
|
}
|
||||||
|
|
||||||
const VoiceVisualizer: React.FC<Props> = ({ isActive, type }) => {
|
const VoiceVisualizer: React.FC<Props> = ({ isActive, type }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation();
|
||||||
const canvasRef = useRef<HTMLCanvasElement>(null)
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
const animationRef = useRef<number | undefined>(undefined)
|
const animationRef = useRef<number | undefined>(undefined);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const canvas = canvasRef.current
|
const canvas = canvasRef.current;
|
||||||
if (!canvas) return
|
if (!canvas) return;
|
||||||
|
|
||||||
const ctx = canvas.getContext('2d')
|
const ctx = canvas.getContext('2d');
|
||||||
if (!ctx) return
|
if (!ctx) return;
|
||||||
|
|
||||||
const width = canvas.width
|
const width = canvas.width;
|
||||||
const height = canvas.height
|
const height = canvas.height;
|
||||||
|
|
||||||
const drawVisualizer = () => {
|
const drawVisualizer = () => {
|
||||||
ctx.clearRect(0, 0, width, height)
|
ctx.clearRect(0, 0, width, height);
|
||||||
|
|
||||||
if (!isActive) {
|
if (!isActive) {
|
||||||
// 绘制静态波形
|
// 绘制静态波形
|
||||||
ctx.beginPath()
|
ctx.beginPath();
|
||||||
ctx.moveTo(0, height / 2)
|
ctx.moveTo(0, height / 2);
|
||||||
ctx.lineTo(width, height / 2)
|
ctx.lineTo(width, height / 2);
|
||||||
ctx.strokeStyle = type === 'input' ? 'var(--color-text-2)' : 'var(--color-primary)'
|
ctx.strokeStyle = type === 'input' ? 'var(--color-text-2)' : 'var(--color-primary)';
|
||||||
ctx.lineWidth = 2
|
ctx.lineWidth = 2;
|
||||||
ctx.stroke()
|
ctx.stroke();
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 绘制动态波形
|
// 绘制动态波形
|
||||||
const barCount = 30
|
const barCount = 30;
|
||||||
const barWidth = width / barCount
|
const barWidth = width / barCount;
|
||||||
const color = type === 'input' ? 'var(--color-text-1)' : 'var(--color-primary)'
|
const color = type === 'input' ? 'var(--color-text-1)' : 'var(--color-primary)';
|
||||||
|
|
||||||
for (let i = 0; i < barCount; i++) {
|
for (let i = 0; i < barCount; i++) {
|
||||||
const barHeight = Math.random() * (height / 2) + 10
|
const barHeight = Math.random() * (height / 2) + 10;
|
||||||
const x = i * barWidth
|
const x = i * barWidth;
|
||||||
const y = height / 2 - barHeight / 2
|
const y = height / 2 - barHeight / 2;
|
||||||
|
|
||||||
ctx.fillStyle = color
|
ctx.fillStyle = color;
|
||||||
ctx.fillRect(x, y, barWidth - 2, barHeight)
|
ctx.fillRect(x, y, barWidth - 2, barHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
animationRef.current = requestAnimationFrame(drawVisualizer)
|
animationRef.current = requestAnimationFrame(drawVisualizer);
|
||||||
}
|
};
|
||||||
|
|
||||||
drawVisualizer()
|
drawVisualizer();
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (animationRef.current) {
|
if (animationRef.current) {
|
||||||
cancelAnimationFrame(animationRef.current)
|
cancelAnimationFrame(animationRef.current);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
}, [isActive, type])
|
}, [isActive, type]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container $type={type}>
|
<Container $type={type}>
|
||||||
<Label>{type === 'input' ? t('voice_call.you') : t('voice_call.ai')}</Label>
|
<Label>{type === 'input' ? t('voice_call.you') : t('voice_call.ai')}</Label>
|
||||||
<Canvas ref={canvasRef} width={200} height={50} />
|
<Canvas ref={canvasRef} width={200} height={50} />
|
||||||
</Container>
|
</Container>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
const Container = styled.div<{ $type: 'input' | 'output' }>`
|
const Container = styled.div<{ $type: 'input' | 'output' }>`
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -77,17 +77,21 @@ const Container = styled.div<{ $type: 'input' | 'output' }>`
|
|||||||
width: 45%;
|
width: 45%;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
background-color: ${(props) => (props.$type === 'input' ? 'var(--color-background-3)' : 'var(--color-primary-bg)')};
|
background-color: ${props =>
|
||||||
`
|
props.$type === 'input'
|
||||||
|
? 'var(--color-background-3)'
|
||||||
|
: 'var(--color-primary-bg)'
|
||||||
|
};
|
||||||
|
`;
|
||||||
|
|
||||||
const Label = styled.div`
|
const Label = styled.div`
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
`
|
`;
|
||||||
|
|
||||||
const Canvas = styled.canvas`
|
const Canvas = styled.canvas`
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 50px;
|
height: 50px;
|
||||||
`
|
`;
|
||||||
|
|
||||||
export default VoiceVisualizer
|
export default VoiceVisualizer;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user