Files
bigwo/test2/client/src/components/VoicePanel.jsx
2026-03-12 12:47:56 +08:00

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>
);
}