feat: app 端 ui 设计完成

This commit is contained in:
liqupan
2026-01-28 19:10:19 +08:00
commit a4e7898e94
149 changed files with 11302 additions and 0 deletions

View File

@@ -0,0 +1,232 @@
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;