171 lines
6.1 KiB
JavaScript
171 lines
6.1 KiB
JavaScript
import { Mic, MicOff, Phone, PhoneOff, Loader2, MessageSquare } from 'lucide-react';
|
|
import { useVoiceChat } from '../hooks/useVoiceChat';
|
|
import SubtitleDisplay from './SubtitleDisplay';
|
|
|
|
export default function VoicePanel({ settings, onVoiceEnd, chatHistory = [], sessionId: parentSessionId }) {
|
|
const {
|
|
isActive,
|
|
isMuted,
|
|
isConnecting,
|
|
subtitles,
|
|
connectionState,
|
|
error,
|
|
duration,
|
|
start,
|
|
stop,
|
|
toggleMute,
|
|
clearError,
|
|
} = useVoiceChat();
|
|
|
|
const formatTime = (s) => {
|
|
const m = Math.floor(s / 60);
|
|
const sec = s % 60;
|
|
return `${m.toString().padStart(2, '0')}:${sec.toString().padStart(2, '0')}`;
|
|
};
|
|
|
|
const handleStart = () => {
|
|
start({
|
|
botName: settings.botName,
|
|
systemRole: settings.systemRole,
|
|
speakingStyle: settings.speakingStyle,
|
|
modelVersion: settings.modelVersion,
|
|
speaker: settings.speaker,
|
|
enableWebSearch: settings.enableWebSearch,
|
|
chatHistory: chatHistory.length > 0 ? chatHistory.slice(-10) : undefined,
|
|
parentSessionId,
|
|
});
|
|
};
|
|
|
|
return (
|
|
<div className="rounded-2xl bg-slate-800/60 border border-slate-700/50 overflow-hidden">
|
|
{/* Status Bar */}
|
|
<div className="px-4 py-2.5 border-b border-slate-700/40 flex items-center justify-between">
|
|
<div className="flex items-center gap-2">
|
|
<div
|
|
className={`w-2 h-2 rounded-full ${
|
|
isActive ? 'bg-emerald-400 animate-pulse' : isConnecting ? 'bg-amber-400 animate-pulse' : 'bg-slate-500'
|
|
}`}
|
|
/>
|
|
<span className="text-xs text-slate-400">
|
|
{isActive
|
|
? `通话中 · ${formatTime(duration)}`
|
|
: isConnecting
|
|
? '正在连接...'
|
|
: '未连接'}
|
|
</span>
|
|
</div>
|
|
{isActive && (
|
|
<div className="flex items-center gap-2 text-xs text-slate-500">
|
|
<span>模型: {settings.modelVersion}</span>
|
|
<span>·</span>
|
|
<span>{settings.botName}</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Visualization Area */}
|
|
<div className="relative flex flex-col items-center justify-center py-12 px-4">
|
|
{/* Voice Wave Animation */}
|
|
<div className="relative w-32 h-32 mb-6">
|
|
{/* Outer rings */}
|
|
{isActive && !isMuted && (
|
|
<>
|
|
<div className="absolute inset-0 rounded-full bg-violet-500/10 animate-ping" style={{ animationDuration: '2s' }} />
|
|
<div className="absolute inset-3 rounded-full bg-violet-500/10 animate-ping" style={{ animationDuration: '2.5s' }} />
|
|
</>
|
|
)}
|
|
{/* Center circle */}
|
|
<div
|
|
className={`absolute inset-4 rounded-full flex items-center justify-center transition-all duration-300 ${
|
|
isActive
|
|
? isMuted
|
|
? 'bg-slate-600 shadow-lg shadow-slate-600/20'
|
|
: 'bg-gradient-to-br from-violet-500 to-indigo-600 shadow-lg shadow-violet-500/30'
|
|
: 'bg-slate-700'
|
|
}`}
|
|
>
|
|
{isConnecting ? (
|
|
<Loader2 className="w-10 h-10 text-white animate-spin" />
|
|
) : isActive ? (
|
|
isMuted ? (
|
|
<MicOff className="w-10 h-10 text-slate-300" />
|
|
) : (
|
|
<Mic className="w-10 h-10 text-white" />
|
|
)
|
|
) : (
|
|
<Phone className="w-10 h-10 text-slate-400" />
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* AI Info */}
|
|
<div className="text-center mb-6">
|
|
<h2 className="text-lg font-semibold text-white">{settings.botName}</h2>
|
|
<p className="text-xs text-slate-400 mt-1 max-w-sm">
|
|
{isActive
|
|
? isMuted
|
|
? '麦克风已静音'
|
|
: '正在聆听...'
|
|
: '点击下方按钮开始语音通话'}
|
|
</p>
|
|
</div>
|
|
|
|
{/* Controls */}
|
|
<div className="flex items-center gap-4">
|
|
{isActive ? (
|
|
<>
|
|
<button
|
|
onClick={toggleMute}
|
|
className={`w-12 h-12 rounded-full flex items-center justify-center transition-all ${
|
|
isMuted
|
|
? 'bg-amber-500/20 text-amber-400 hover:bg-amber-500/30'
|
|
: 'bg-slate-700 text-slate-300 hover:bg-slate-600'
|
|
}`}
|
|
title={isMuted ? '取消静音' : '静音'}
|
|
>
|
|
{isMuted ? <MicOff className="w-5 h-5" /> : <Mic className="w-5 h-5" />}
|
|
</button>
|
|
<button
|
|
onClick={async () => {
|
|
const result = await stop();
|
|
if (onVoiceEnd) onVoiceEnd(result);
|
|
}}
|
|
className="w-14 h-14 rounded-full bg-red-500 hover:bg-red-600 text-white flex items-center justify-center transition-all shadow-lg shadow-red-500/25"
|
|
title="结束通话并转接文字对话"
|
|
>
|
|
<PhoneOff className="w-6 h-6" />
|
|
</button>
|
|
</>
|
|
) : (
|
|
<button
|
|
onClick={handleStart}
|
|
disabled={isConnecting}
|
|
className="w-14 h-14 rounded-full bg-gradient-to-br from-violet-500 to-indigo-600 hover:from-violet-600 hover:to-indigo-700 text-white flex items-center justify-center transition-all shadow-lg shadow-violet-500/25 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
title="开始通话"
|
|
>
|
|
{isConnecting ? (
|
|
<Loader2 className="w-6 h-6 animate-spin" />
|
|
) : (
|
|
<Phone className="w-6 h-6" />
|
|
)}
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
{/* Error */}
|
|
{error && (
|
|
<div className="mt-4 px-4 py-2 rounded-lg bg-red-500/10 border border-red-500/20 text-red-400 text-xs max-w-md text-center">
|
|
{error}
|
|
<button onClick={clearError} className="ml-2 underline hover:text-red-300">
|
|
关闭
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Subtitles */}
|
|
<SubtitleDisplay subtitles={subtitles} isActive={isActive} />
|
|
</div>
|
|
);
|
|
}
|