Files
app/wei-ai-demo/pages/Interaction.tsx
2026-01-28 19:10:19 +08:00

232 lines
11 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useState, useRef, useEffect } from 'react';
import { Send, Mic, MicOff, ChevronLeft, MoreVertical, Headphones, PhoneOff, Volume2, VolumeX } from 'lucide-react';
import { Message, Character } from '../types';
interface InteractionProps {
character: Character | null;
onBack: () => void;
}
const Interaction: React.FC<InteractionProps> = ({ character, onBack }) => {
const [messages, setMessages] = useState<Message[]>([]);
const [inputValue, setInputValue] = useState('');
const [isVoiceMode, setIsVoiceMode] = useState(false);
const [isMicMuted, setIsMicMuted] = useState(false);
const [isSpeakerOn, setIsSpeakerOn] = useState(true);
const scrollRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (character) {
setMessages([
{
id: 'init',
text: character.description ? `${character.description}\n\n指挥官${character.name} 已就位。` : '连接建立。',
sender: 'ai',
type: 'text',
timestamp: Date.now(),
}
]);
}
}, [character]);
useEffect(() => {
if (scrollRef.current) {
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
}
}, [messages]);
const handleSend = () => {
if (!inputValue.trim()) return;
const newMsg: Message = {
id: Date.now().toString(),
text: inputValue,
sender: 'user',
type: 'text',
timestamp: Date.now(),
};
setMessages(prev => [...prev, newMsg]);
setInputValue('');
setTimeout(() => {
const aiMsg: Message = {
id: (Date.now() + 1).toString(),
text: '收到反馈。硬件同步率正在上升...',
sender: 'ai',
type: 'text',
timestamp: Date.now(),
};
setMessages(prev => [...prev, aiMsg]);
}, 1500);
};
if (isVoiceMode && character) {
return (
<div className="fixed inset-0 z-[70] bg-[#2e1065] flex flex-col items-center justify-between py-safe overflow-hidden">
<div className="absolute inset-0 z-0">
<img src={character.avatar} className="w-full h-full object-cover opacity-30 blur-2xl scale-125" />
<div className="absolute inset-0 bg-gradient-to-b from-[#2e1065] via-[#4c1d95]/80 to-[#2e1065]"></div>
</div>
<div className="relative z-10 w-full px-6 pt-6 flex justify-between items-start">
<button onClick={() => setIsVoiceMode(false)} className="p-2 rounded-full bg-white/10 backdrop-blur-sm border border-white/10 text-white/70 active:bg-white/20 transition-colors">
<ChevronLeft size={24} />
</button>
</div>
<div className="relative z-10 flex-1 flex flex-col items-center justify-center w-full">
<div className="text-center mb-12">
<h2 className="text-2xl font-bold text-white tracking-wide drop-shadow-lg mb-2">{character.name}</h2>
<p className="text-white/60 text-xs font-medium tracking-[0.2em] uppercase">
{isMicMuted ? 'Mic Muted' : 'Listening...'}
</p>
</div>
<div className="relative w-48 h-48 flex items-center justify-center">
{!isMicMuted && (
<>
<div className="absolute inset-0 rounded-full border border-white/20 animate-[ping_3s_ease-in-out_infinite] opacity-50"></div>
<div className="absolute inset-0 rounded-full border border-white/10 animate-[ping_3s_ease-in-out_infinite_1s] opacity-30"></div>
</>
)}
<div className="w-40 h-40 rounded-full overflow-hidden border-2 border-white/30 relative shadow-[0_0_50px_rgba(192,132,252,0.5)] z-10">
<img src={character.avatar} className="w-full h-full object-cover" />
</div>
</div>
<div className="mt-12 flex items-center gap-1 h-8">
{[...Array(5)].map((_, i) => (
<div key={i} className={`w-1 bg-white rounded-full transition-all duration-300 ${isMicMuted ? 'h-1 opacity-20' : 'animate-pulse'}`} style={{ height: isMicMuted ? 4 : Math.random() * 24 + 8, animationDelay: `${i * 0.1}s` }}></div>
))}
</div>
</div>
<div className="relative z-10 w-full px-12 pb-12 flex items-center justify-between">
<button
onClick={() => setIsMicMuted(!isMicMuted)}
className={`p-4 rounded-full border transition-all duration-300 ${
isMicMuted
? 'bg-white text-[#2e1065] border-white shadow-lg'
: 'bg-white/10 backdrop-blur-md border-white/10 text-white hover:bg-white/20'
}`}
>
{isMicMuted ? <MicOff size={24} /> : <Mic size={24} />}
</button>
<button
onClick={() => setIsVoiceMode(false)}
className="w-20 h-20 rounded-full bg-red-500 text-white flex items-center justify-center shadow-lg active:scale-95 transition-transform"
>
<PhoneOff size={32} fill="white" />
</button>
<button
onClick={() => setIsSpeakerOn(!isSpeakerOn)}
className={`p-4 rounded-full border transition-all duration-300 ${
isSpeakerOn
? 'bg-white/10 backdrop-blur-md border-white/30 text-white'
: 'bg-white/5 backdrop-blur-md border-white/10 text-white/40'
}`}
>
{isSpeakerOn ? <Volume2 size={24} /> : <VolumeX size={24} />}
</button>
</div>
</div>
);
}
if (!character) return null;
return (
<div className="fixed inset-0 z-[60] bg-[#2e1065] flex flex-col overflow-hidden">
{/* Background */}
<div className="absolute inset-0 z-0 pointer-events-none">
<img
src={character.avatar}
alt="AI Character"
className="w-full h-full object-cover opacity-20 blur-xl scale-110"
/>
<div className="absolute inset-0 bg-gradient-to-t from-[#2e1065] via-[#2e1065]/90 to-[#2e1065]/50"></div>
</div>
{/* Header */}
<div className="relative z-20 pt-safe px-4 py-3 flex items-center justify-between border-b border-white/10 bg-[#2e1065]/60 backdrop-blur-md">
<button onClick={onBack} className="p-2 -ml-2 text-white/80 hover:text-white active:scale-95 transition-transform">
<ChevronLeft size={24} strokeWidth={1.5} />
</button>
<div className="flex flex-col items-center">
<h3 className="text-base font-bold text-white tracking-wide drop-shadow-md">{character.name}</h3>
<span className="text-[10px] text-white/60 font-medium tracking-wider uppercase">{character.tagline}</span>
</div>
<button className="p-2 -mr-2 text-white/70 hover:text-white active:scale-95 transition-transform">
<MoreVertical size={20} strokeWidth={1.5} />
</button>
</div>
{/* Chat Area */}
<div className="flex-1 z-10 flex flex-col justify-end pb-2 px-3 overflow-hidden relative">
<div className="absolute top-0 left-0 w-full h-8 bg-gradient-to-b from-[#2e1065]/50 to-transparent z-10 pointer-events-none"></div>
<div
ref={scrollRef}
className="flex-1 overflow-y-auto no-scrollbar space-y-6 pt-4 pb-2"
>
{messages.map((msg) => (
<div key={msg.id} className={`flex ${msg.sender === 'user' ? 'justify-end' : 'justify-start'} group items-end`}>
{msg.sender === 'ai' && (
<div className="w-8 h-8 rounded-full overflow-hidden mr-3 border border-white/20 shrink-0">
<img src={character.avatar} className="w-full h-full object-cover" />
</div>
)}
<div className={`max-w-[80%] p-3 relative ${
msg.sender === 'user'
? 'bg-gradient-to-br from-[#C084FC] to-[#F472B6] text-white rounded-2xl rounded-tr-sm shadow-md'
: 'bg-white/10 backdrop-blur-md border border-white/10 text-white rounded-2xl rounded-tl-sm shadow-sm'
}`}>
{msg.type === 'text' && <p className="text-sm leading-relaxed tracking-wide font-normal">{msg.text}</p>}
</div>
</div>
))}
</div>
{/* Input Area */}
<div className="relative flex items-center gap-2 pt-1 pb-1">
<button
onClick={() => setIsVoiceMode(true)}
className="w-10 h-10 flex items-center justify-center rounded-full transition-all duration-300 border border-white/10 bg-white/10 backdrop-blur-md text-white/70 hover:text-white hover:bg-white/20"
>
<Headphones size={20} strokeWidth={1.5} />
</button>
<div className="flex-1 h-11 bg-white/10 backdrop-blur-md rounded-full flex items-center px-4 border border-white/10 focus-within:border-white/30 focus-within:bg-white/20 transition-all shadow-sm">
<input
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
placeholder="输入指令..."
className="bg-transparent w-full text-sm text-white placeholder-white/40 focus:outline-none"
onKeyDown={(e) => e.key === 'Enter' && handleSend()}
/>
<button className="text-white/50 hover:text-white ml-1 transition-colors">
<Mic size={18} strokeWidth={1.5} />
</button>
</div>
<button
onClick={handleSend}
disabled={!inputValue.trim()}
className={`w-10 h-10 flex items-center justify-center rounded-full transition-all duration-300 ${
inputValue.trim()
? 'bg-white text-[#2e1065] scale-100 opacity-100 shadow-lg'
: 'bg-white/5 text-white/20 scale-95 opacity-50'
}`}
>
<Send size={18} className="-ml-0.5 mt-0.5" strokeWidth={2.5} />
</button>
</div>
</div>
</div>
);
};
export default Interaction;