This commit is contained in:
Flatlogic Bot 2026-02-26 18:51:55 +00:00
parent 94fcde74c9
commit 93c3ce29c7

View File

@ -18,6 +18,8 @@ import {
mdiStarCircle,
mdiChatProcessingOutline,
mdiSend,
mdiEarth,
mdiWebcam,
} from '@mdi/js';
import BaseIcon from '../components/BaseIcon';
import { useAppDispatch, useAppSelector } from '../stores/hooks';
@ -66,14 +68,6 @@ const CELEBRITIES = [
personality: 'Passionate about the environment and exploration. Intense and focused.',
reaction: 'I\'m the king of the world... or at least this telescope!'
},
{
id: 'streep',
name: 'Meryl Streep',
category: 'Actress',
img: 'https://images.pexels.com/photos/2836486/pexels-photo-2836486.jpeg?auto=compress&cs=tinysrgb&w=400',
personality: 'Sophisticated, masterful, and appreciates fine detail and craft.',
reaction: 'The performance of these celestial bodies is award-worthy.'
},
{
id: 'davinci',
name: 'Leonardo da Vinci',
@ -81,23 +75,27 @@ const CELEBRITIES = [
img: 'https://images.pexels.com/photos/33152/european-rari-da-vinci-mona-lisa.jpg?auto=compress&cs=tinysrgb&w=400',
personality: 'Scientific, curious, observant. Mentions geometry and anatomy of the universe.',
reaction: 'The proportions of this universe are divine. A true masterpiece.'
},
{
id: 'vangogh',
name: 'Vincent van Gogh',
category: 'Painter',
img: 'https://images.pexels.com/photos/161154/vincents-bedroom-in-arles-vincent-van-gogh-artist-painting-161154.jpeg?auto=compress&cs=tinysrgb&w=400',
personality: 'Emotional, sensitive to light and color. Sees swirls in the sky.',
reaction: 'The starry night... it\'s even more vibrant than I painted it!'
}
];
const REAL_PEOPLE_AVATARS = [
'https://images.pexels.com/photos/220453/pexels-photo-220453.jpeg?auto=compress&cs=tinysrgb&w=200',
'https://images.pexels.com/photos/774909/pexels-photo-774909.jpeg?auto=compress&cs=tinysrgb&w=200',
'https://images.pexels.com/photos/1239291/pexels-photo-1239291.jpeg?auto=compress&cs=tinysrgb&w=200',
'https://images.pexels.com/photos/91227/pexels-photo-91227.jpeg?auto=compress&cs=tinysrgb&w=200',
'https://images.pexels.com/photos/712513/pexels-photo-712513.jpeg?auto=compress&cs=tinysrgb&w=200',
'https://images.pexels.com/photos/1181686/pexels-photo-1181686.jpeg?auto=compress&cs=tinysrgb&w=200',
'https://images.pexels.com/photos/415829/pexels-photo-415829.jpeg?auto=compress&cs=tinysrgb&w=200',
'https://images.pexels.com/photos/1043471/pexels-photo-1043471.jpeg?auto=compress&cs=tinysrgb&w=200',
'https://images.pexels.com/photos/1542085/pexels-photo-1542085.jpeg?auto=compress&cs=tinysrgb&w=200',
'https://images.pexels.com/photos/1040880/pexels-photo-1040880.jpeg?auto=compress&cs=tinysrgb&w=200'
];
const PRESET_TARGETS = [
{ id: 'mars', name: 'Mars', type: 'Planet', img: 'https://images-assets.nasa.gov/image/PIA04591/PIA04591~medium.jpg', dist: '225M km' },
{ id: 'jupiter', name: 'Jupiter', type: 'Planet', img: 'https://images-assets.nasa.gov/image/PIA04866/PIA04866~medium.jpg', dist: '778M km' },
{ id: 'orion', name: 'Orion Nebula', type: 'Nebula', img: 'https://images-assets.nasa.gov/image/PIA08653/PIA08653~medium.jpg', dist: '1,344 ly' },
{ id: 'andromeda', name: 'Andromeda', type: 'Galaxy', img: 'https://images-assets.nasa.gov/image/PIA15416/PIA15416~medium.jpg', dist: '2.5M ly' },
{ id: 'pillars', name: 'Pillars of Creation', type: 'Nebula', img: 'https://images-assets.nasa.gov/image/as11-40-5874/as11-40-5874~medium.jpg', dist: '6,500 ly' }
{ id: 'andromeda', name: 'Andromeda', type: 'Galaxy', img: 'https://images-assets.nasa.gov/image/PIA15416/PIA15416~medium.jpg', dist: '2.5M ly' }
];
const ObservationPage = () => {
@ -108,7 +106,6 @@ const ObservationPage = () => {
const [selectedTarget, setSelectedTarget] = useState<any>(null);
const [isFocusing, setIsFocusing] = useState(false);
const [isSharpnessMax, setIsSharpnessMax] = useState(false);
const [telemetry, setTelemetry] = useState({ temp: -233, dist: 1.5, focal: 131 });
// Simulation States
const [audienceCount, setAudienceCount] = useState(0);
@ -116,6 +113,7 @@ const ObservationPage = () => {
const [activeCelebrities, setActiveCelebrities] = useState<string[]>([]);
const [chatMessages, setChatMessages] = useState<any[]>([]);
const [showSimPanel, setShowSimPanel] = useState(false);
const [activeViewers, setActiveViewers] = useState<any[]>([]);
// Interaction State
const [userQuery, setUserQuery] = useState('');
@ -128,7 +126,6 @@ const ObservationPage = () => {
const [videoUrl, setVideoUrl] = useState<string | null>(null);
const dispatch = useAppDispatch();
const { gptResponse } = useAppSelector((state) => state.openAi);
useEffect(() => {
dispatch(fetchSkyObjects({}));
@ -145,7 +142,7 @@ const ObservationPage = () => {
setIsCameraActive(true);
}
} catch (err) {
console.error("Camera/Audio access denied:", err);
console.error("Camera access denied:", err);
}
};
@ -157,78 +154,63 @@ const ObservationPage = () => {
}
};
// Audience Simulation Logic
// Audience & Viewer Simulation Logic
useEffect(() => {
let interval: any;
if (isSimActive) {
interval = setInterval(() => {
setAudienceCount(prev => {
const target = 1000000;
return prev < target ? Math.min(prev + Math.floor(Math.random() * 8000) + 2000, target) : target;
return prev < target ? Math.min(prev + Math.floor(Math.random() * 15000) + 5000, target) : target;
});
if (activeCelebrities.length > 0 && Math.random() > 0.85) {
// Rotate random "Real People" viewers
if (Math.random() > 0.7) {
const randomAvatar = REAL_PEOPLE_AVATARS[Math.floor(Math.random() * REAL_PEOPLE_AVATARS.length)];
setActiveViewers(prev => [...prev.slice(-5), { id: Date.now(), img: randomAvatar }]);
}
if (activeCelebrities.length > 0 && Math.random() > 0.8) {
const randomCelebId = activeCelebrities[Math.floor(Math.random() * activeCelebrities.length)];
const celeb = CELEBRITIES.find(c => c.id === randomCelebId);
if (celeb) {
setChatMessages(prev => [{
id: Date.now(),
name: celeb.name,
category: celeb.category,
text: celeb.reaction,
time: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
}, ...prev].slice(0, 10));
}, ...prev].slice(0, 8));
}
}
}, 1500);
}, 1000);
} else {
setAudienceCount(0);
setChatMessages([]);
setActiveViewers([]);
}
return () => clearInterval(interval);
}, [isSimActive, activeCelebrities]);
const handleAskCelebrities = async () => {
if (!userQuery.trim() || activeCelebrities.length === 0) return;
setIsAsking(true);
const selectedNames = activeCelebrities.map(id => CELEBRITIES.find(c => c.id === id)?.name).join(', ');
const celebContext = activeCelebrities.map(id => {
const c = CELEBRITIES.find(x => x.id === id);
return `${c?.name} (${c?.personality})`;
}).join('; ');
const prompt = `You are simulating a live conversation between the user and these celebrities: ${selectedNames}.
User is observing ${selectedTarget ? selectedTarget.name : 'the cosmos'} through the James Webb Telescope.
Celebrity Contexts: ${celebContext}.
User asks: "${userQuery}".
Respond as one or more of these celebrities, staying perfectly in character. Keep it short, real-time, and exciting.
Format: [Name]: "Message"`;
const prompt = `Simulate a live reaction from: ${selectedNames}. The user is observing ${selectedTarget?.name || 'deep space'}. User asks: "${userQuery}". Stay in character. Short response.`;
try {
const resultAction = await dispatch(askGpt(prompt));
if (askGpt.fulfilled.match(resultAction)) {
const response = resultAction.payload.data;
const nameMatch = response.match(/\\\[(.*?)\\\]:\s*"(.*?)\\/);
const newMessage = {
id: Date.now(),
name: nameMatch ? nameMatch[1] : (activeCelebrities.length > 0 ? CELEBRITIES.find(c => c.id === activeCelebrities[0])?.name : 'AI'),
category: 'Interactive Response',
text: nameMatch ? nameMatch[2] : response,
time: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }),
name: CELEBRITIES.find(c => c.id === activeCelebrities[0])?.name || 'Celebrity',
text: response,
isAi: true
};
setChatMessages(prev => [newMessage, ...prev].slice(0, 15));
setChatMessages(prev => [newMessage, ...prev].slice(0, 10));
setLastResponse(newMessage);
}
} catch (err) {
console.error(err);
} finally {
setIsAsking(false);
setUserQuery('');
}
} catch (err) { console.error(err); } finally { setIsAsking(false); setUserQuery(''); }
};
const handleZoom = (direction: 'in' | 'out') => {
@ -253,10 +235,9 @@ const ObservationPage = () => {
};
const startRecording = () => {
if (!videoRef.current || !videoRef.current.srcObject) return;
if (!videoRef.current?.srcObject) return;
const stream = videoRef.current.srcObject as MediaStream;
const options = { mimeType: 'video/webm;codecs=vp8,opus' };
const recorder = new MediaRecorder(stream, MediaRecorder.isTypeSupported(options.mimeType) ? options : { mimeType: 'video/webm' });
const recorder = new MediaRecorder(stream, { mimeType: 'video/webm' });
const chunks: Blob[] = [];
recorder.ondataavailable = (e) => chunks.push(e.data);
recorder.onstop = () => setVideoUrl(URL.createObjectURL(new Blob(chunks, { type: 'video/webm' })));
@ -267,15 +248,6 @@ const ObservationPage = () => {
const stopRecording = () => { mediaRecorder?.stop(); setIsRecording(false); };
const downloadVideo = () => {
if (videoUrl) {
const a = document.createElement('a');
a.href = videoUrl;
a.download = `JWST-INTERACTIVE-REC-${Date.now()}.webm`;
a.click();
}
};
const formatZoom = (z: number) => {
if (z >= 1e12) return `${(z / 1e12).toFixed(1)}T`;
if (z >= 1e9) return `${(z / 1e9).toFixed(1)}B`;
@ -285,138 +257,146 @@ const ObservationPage = () => {
return (
<div className="relative h-screen w-full bg-black overflow-hidden flex flex-col font-mono text-[#00F2FF]">
<Head><title>JWST | Interactive Global Live</title></Head>
<Head><title>JWST | Global Live Simulation</title></Head>
{/* Main Viewport */}
{/* Main Viewport */}
<div className="absolute inset-0 z-0 bg-gray-950 overflow-hidden">
{!isCameraActive && (
<div className="flex flex-col items-center justify-center h-full space-y-6 z-10 relative">
<div className="w-32 h-32 border-2 border-[#E3B341] rounded-full flex items-center justify-center animate-pulse">
<BaseIcon path={mdiTelescope} size={64} className="text-[#E3B341]" />
</div>
<p className="text-[#E3B341] uppercase tracking-[0.4em] font-bold text-xl text-center">Telescope Standby</p>
<button onClick={startCamera} className="bg-[#E3B341] text-black px-12 py-4 font-bold uppercase tracking-widest hover:bg-white transition-all transform hover:scale-105">Deploy Systems</button>
<p className="text-[#E3B341] uppercase tracking-[0.4em] font-bold text-xl">System Standby</p>
<button onClick={startCamera} className="bg-[#E3B341] text-black px-12 py-4 font-bold uppercase tracking-widest hover:bg-white transition-all">Deploy JWST</button>
</div>
)}
<video ref={videoRef} autoPlay playsInline muted className={`absolute inset-0 w-full h-full object-cover transition-all duration-1000 ${isCameraActive && (zoom <= 5000 || !selectedTarget) ? 'opacity-100' : 'opacity-0'}`}
style={{ filter: (isSharpnessMax ? 'contrast(1.4) brightness(1.1) saturate(1.2)' : 'none') + (mode === 'ir' ? ' hue-rotate(180deg) saturate(1.5)' : mode === 'deep' ? ' contrast(1.5) brightness(0.8)' : ''), transform: `scale(${1 + Math.log10(zoom)})`, imageRendering: isSharpnessMax ? 'crisp-edges' : 'auto' }}
<video ref={videoRef} autoPlay playsInline muted className={`absolute inset-0 w-full h-full object-cover transition-opacity duration-1000 ${isCameraActive && (zoom <= 5000 || !selectedTarget) ? 'opacity-100' : 'opacity-0'}`}
style={{ filter: (isSharpnessMax ? 'contrast(1.4) brightness(1.1)' : 'none') + (mode === 'ir' ? ' hue-rotate(180deg)' : mode === 'deep' ? ' contrast(1.5)' : ''), transform: `scale(${1 + Math.log10(zoom)})` }}
/>
{selectedTarget && (
<div className={`absolute inset-0 w-full h-full bg-cover bg-center transition-opacity duration-1000 ${zoom > 5000 ? 'opacity-100' : 'opacity-0'}`}
style={{ backgroundImage: `url(${selectedTarget.img})`, transform: `scale(${1 + (Math.log10(zoom) - 3) / 20})`, filter: isSharpnessMax ? 'contrast(1.4) brightness(1.1) saturate(1.2)' : 'none', imageRendering: isSharpnessMax ? 'crisp-edges' : 'auto' }}
style={{ backgroundImage: `url(${selectedTarget.img})`, transform: `scale(${1 + (Math.log10(zoom) - 3) / 20})` }}
/>
)}
{/* 1 Million People Visual Simulation */}
{/* 1 Million People Crowd Particles */}
{isSimActive && audienceCount > 0 && (
<div className="absolute inset-0 z-10 pointer-events-none opacity-40">
<div className="absolute bottom-0 w-full h-1/2 bg-gradient-to-t from-[#E3B341]/30 to-transparent"></div>
{[...Array(50)].map((_, i) => (
<div key={i} className="absolute bg-[#E3B341] rounded-full blur-[3px] animate-pulse"
style={{ width: Math.random() * 6 + 2, height: Math.random() * 6 + 2, left: `${Math.random() * 100}%`, bottom: `${Math.random() * 40}%`, opacity: Math.random(), animationDelay: `${Math.random() * 5}s` }}
<div className="absolute inset-0 z-10 pointer-events-none overflow-hidden">
{[...Array(60)].map((_, i) => (
<div key={i} className="absolute bg-white/20 rounded-full blur-[4px] animate-float"
style={{ width: Math.random() * 8 + 2, height: Math.random() * 8 + 2, left: `${Math.random() * 100}%`, top: `${Math.random() * 100}%`, animationDuration: `${Math.random() * 10 + 5}s`, animationDelay: `${Math.random() * 5}s` }}
/>
))}
<div className="absolute bottom-0 left-0 right-0 h-24 bg-[#00F2FF]/10 blur-3xl"></div>
<div className="absolute bottom-0 w-full h-1/3 bg-gradient-to-t from-[#E3B341]/20 to-transparent"></div>
</div>
)}
{/* Celebrity Visible Avatars Overlay */}
{/* REAL PEOPLE VIEWER GRID */}
{isSimActive && activeViewers.length > 0 && (
<div className="absolute top-4 left-4 z-40 flex flex-col space-y-2 pointer-events-none">
<div className="flex items-center space-x-2 bg-black/60 backdrop-blur-md px-3 py-1.5 rounded-full border border-[#00F2FF]/30">
<BaseIcon path={mdiWebcam} size={14} className="text-[#00F2FF]" />
<span className="text-[10px] font-bold uppercase tracking-tighter">Live Webcams</span>
</div>
<div className="grid grid-cols-3 gap-2">
{activeViewers.map((viewer) => (
<div key={viewer.id} className="w-14 h-14 rounded-lg border border-white/20 overflow-hidden shadow-lg animate-pulse bg-gray-800 relative">
<img src={viewer.img} className="w-full h-full object-cover grayscale opacity-80" />
<div className="absolute inset-0 bg-blue-500/10"></div>
</div>
))}
</div>
</div>
)}
{/* Celebrity Avatars */}
{isSimActive && activeCelebrities.length > 0 && (
<div className="absolute bottom-32 left-1/2 -translate-x-1/2 flex -space-x-4 z-30 pointer-events-none">
{activeCelebrities.map((id, index) => {
<div className="absolute bottom-40 left-1/2 -translate-x-1/2 flex -space-x-4 z-30 pointer-events-none">
{activeCelebrities.map((id) => {
const celeb = CELEBRITIES.find(c => c.id === id);
const isSpeaking = lastResponse?.name === celeb?.name;
return (
<div key={id} className={`relative transition-all duration-500 transform ${isSpeaking ? 'scale-125 z-50 -translate-y-4' : 'scale-100 opacity-80'}`} style={{ transitionDelay: `${index * 100}ms` }}>
<div className={`w-20 h-20 rounded-full border-4 overflow-hidden shadow-2xl ${isSpeaking ? 'border-[#E3B341] ring-4 ring-[#E3B341]/40' : 'border-white/20'}`}>
<img src={celeb?.img} alt={celeb?.name} className="w-full h-full object-cover" />
<div key={id} className={`relative transition-all duration-500 transform ${isSpeaking ? 'scale-125 z-50 -translate-y-4' : 'scale-100 opacity-60'}`}>
<div className={`w-16 h-16 rounded-full border-2 overflow-hidden shadow-2xl ${isSpeaking ? 'border-[#E3B341] ring-4 ring-[#E3B341]/30' : 'border-white/20'}`}>
<img src={celeb?.img} className="w-full h-full object-cover" />
</div>
{isSpeaking && (
<div className="absolute -top-12 left-1/2 -translate-x-1/2 bg-[#E3B341] text-black text-[10px] font-bold px-3 py-1 rounded-full whitespace-nowrap animate-bounce">
{celeb?.name} is speaking!
</div>
)}
</div>
);
})}
</div>
)}
{/* Global Connectivity Map */}
{isSimActive && (
<div className="absolute top-4 right-4 z-40 bg-black/40 backdrop-blur-md p-3 rounded-xl border border-white/10 pointer-events-none">
<div className="flex items-center space-x-2 mb-2">
<BaseIcon path={mdiEarth} size={16} className="text-[#00F2FF] animate-spin-slow" />
<span className="text-[9px] font-bold uppercase">Nodes</span>
</div>
<div className="w-32 h-16 bg-[#00F2FF]/5 relative rounded overflow-hidden">
{[...Array(15)].map((_, i) => (
<div key={i} className="absolute w-1 h-1 bg-[#00F2FF] rounded-full animate-ping"
style={{ left: `${Math.random() * 100}%`, top: `${Math.random() * 100}%`, animationDelay: `${Math.random() * 3}s` }}
/>
))}
</div>
</div>
)}
</div>
{/* UI Controls */}
{/* UI Overlay */}
{isCameraActive && (
<div className="absolute inset-0 z-20 p-4 flex flex-col justify-between pointer-events-none">
<div className="flex justify-between items-start pointer-events-auto">
<div className="bg-black/90 border-l-4 border-[#E3B341] p-4 backdrop-blur-xl min-w-[280px] shadow-[0_0_20px_rgba(227,179,65,0.2)]">
<div className="flex items-center justify-between mb-1">
<div className="text-[9px] text-gray-400 uppercase tracking-widest">Global Live System</div>
<div className="text-[9px] text-[#00F2FF] font-bold">STABLE</div>
</div>
<div className="flex items-center space-x-3">
<div className={`w-3 h-3 rounded-full ${isRecording ? 'bg-red-500 animate-pulse' : 'bg-green-500'}`}></div>
<div className="font-bold text-[#E3B341] tracking-widest uppercase text-sm">
{isRecording ? 'A/V REC ACTIVE' : 'Deep Space Feed'}
</div>
<div className="bg-black/90 border-l-4 border-[#E3B341] p-4 backdrop-blur-md min-w-[260px] shadow-2xl rounded-r-xl">
<div className="flex items-center justify-between mb-2">
<div className="text-[9px] text-gray-400 uppercase tracking-widest">LIVE BROADCAST</div>
<div className="flex items-center space-x-1">
<div className="w-2 h-2 rounded-full bg-red-600 animate-pulse"></div>
<span className="text-[10px] font-bold">LIVE</span>
</div>
</div>
{isSimActive && (
<div className="mt-3 flex items-center justify-between bg-white/5 p-2 rounded-lg border border-white/10">
<div className="flex items-center justify-between bg-white/5 p-2 rounded-lg border border-white/10">
<div className="flex items-center space-x-2">
<BaseIcon path={mdiAccountGroup} size={16} className="text-[#00F2FF]" />
<span className="text-white text-sm font-black tabular-nums">{audienceCount.toLocaleString()}</span>
<span className="text-white text-base font-black tabular-nums">{audienceCount.toLocaleString()}</span>
</div>
<span className="text-[10px] text-gray-400 uppercase font-bold tracking-tighter">Live Audience</span>
<span className="text-[9px] text-gray-400 uppercase font-bold">Viewers</span>
</div>
)}
<div className="mt-3 grid grid-cols-2 gap-4 border-t border-white/10 pt-3">
<div className="flex flex-col"><span className="text-[8px] text-gray-500 uppercase">Magnification</span><span className="text-white font-bold">{formatZoom(zoom)}</span></div>
<div className="flex flex-col"><span className="text-[8px] text-gray-500 uppercase">Target</span><span className="text-[#00F2FF] font-bold truncate">{selectedTarget?.name || 'Scanning'}</span></div>
</div>
</div>
<div className="flex space-x-3">
<button onClick={() => setShowSimPanel(!showSimPanel)} className={`p-4 bg-black/90 border-2 transition-all rounded-xl ${showSimPanel ? 'border-[#E3B341] text-[#E3B341] shadow-[0_0_15px_rgba(227,179,65,0.4)]' : 'border-white/20 text-white'}`}><BaseIcon path={mdiStarCircle} size={24} /></button>
<div className="flex space-x-1 bg-black/80 p-1 border-2 border-white/10 rounded-xl">
<button onClick={isRecording ? stopRecording : startRecording} className={`p-4 rounded-lg transition-all ${isRecording ? 'bg-red-600 animate-pulse text-white' : 'bg-white/5 text-red-500 hover:bg-white/10'}`}><BaseIcon path={isRecording ? mdiStop : mdiRecord} size={24} /></button>
{videoUrl && <button onClick={downloadVideo} className="p-4 bg-blue-600 rounded-lg text-white"><BaseIcon path={mdiDownload} size={24} /></button>}
</div>
<button onClick={() => setIsSharpnessMax(!isSharpnessMax)} className={`p-4 bg-black/90 border-2 transition-all rounded-xl ${isSharpnessMax ? 'border-[#00F2FF] text-[#00F2FF]' : 'border-white/20 text-white'}`}><BaseIcon path={mdiAutoFix} size={24} /></button>
<button onClick={stopCamera} className="p-4 bg-black/90 border-2 border-red-500/40 rounded-xl hover:bg-red-500/60"><BaseIcon path={mdiClose} size={24} className="text-white" /></button>
<button onClick={() => setShowSimPanel(!showSimPanel)} className={`p-4 bg-black/90 border-2 transition-all rounded-2xl ${showSimPanel ? 'border-[#E3B341] text-[#E3B341]' : 'border-white/20 text-white'}`}><BaseIcon path={mdiStarCircle} size={28} /></button>
<button onClick={isRecording ? stopRecording : startRecording} className={`p-4 bg-black/90 border-2 rounded-2xl ${isRecording ? 'border-red-600 text-red-600' : 'border-white/20 text-white'}`}><BaseIcon path={isRecording ? mdiStop : mdiRecord} size={28} /></button>
<button onClick={stopCamera} className="p-4 bg-black/90 border-2 border-red-500/40 rounded-2xl hover:bg-red-500/60"><BaseIcon path={mdiClose} size={28} className="text-white" /></button>
</div>
</div>
{showSimPanel && (
<div className="absolute top-24 right-4 w-80 bg-black/95 border-2 border-[#E3B341] p-6 backdrop-blur-3xl pointer-events-auto z-50 rounded-2xl shadow-2xl">
<div className="flex justify-between items-center mb-6">
<h3 className="text-[#E3B341] font-black uppercase text-xs tracking-[0.3em]">AI Simulation Panel</h3>
<button onClick={() => setShowSimPanel(false)}><BaseIcon path={mdiClose} size={20} /></button>
</div>
<div className="absolute top-28 right-4 w-80 bg-black/95 border-2 border-[#E3B341] p-6 backdrop-blur-3xl pointer-events-auto z-50 rounded-3xl shadow-2xl">
<h3 className="text-[#E3B341] font-black uppercase text-xs tracking-widest mb-6">Simulation Hub</h3>
<div className="space-y-6">
<div className="bg-white/5 p-4 rounded-xl border border-white/10">
<button onClick={() => setIsSimActive(!isSimActive)} className={`w-full py-2 text-xs uppercase font-black rounded-lg transition-all ${isSimActive ? 'bg-red-600 text-white' : 'bg-[#00F2FF] text-black'}`}>
{isSimActive ? 'Disable Global Audience' : 'Enable Global Audience'}
</button>
</div>
<div className="border-t border-white/10 pt-6">
<span className="text-[10px] uppercase text-gray-500 font-bold block mb-3">Visible Characters</span>
<div className="grid grid-cols-2 gap-2 max-h-48 overflow-y-auto pr-2 custom-scrollbar">
{CELEBRITIES.map(celeb => (
<button key={celeb.id} onClick={() => {
setActiveCelebrities(prev => prev.includes(celeb.id) ? prev.filter(c => c !== celeb.id) : [...prev, celeb.id]);
}} className={`flex items-center space-x-2 p-2 rounded-lg transition-all border ${activeCelebrities.includes(celeb.id) ? 'bg-[#E3B341] border-[#E3B341] text-black' : 'bg-white/5 border-white/10 text-white opacity-70 hover:opacity-100'}`}>
<img src={celeb.img} className="w-6 h-6 rounded-full object-cover" />
<span className="text-[9px] font-bold truncate">{celeb.name}</span>
</button>
))}
</div>
<button onClick={() => setIsSimActive(!isSimActive)} className={`w-full py-3 text-xs uppercase font-black rounded-xl transition-all ${isSimActive ? 'bg-red-600 text-white' : 'bg-[#00F2FF] text-black'}`}>
{isSimActive ? 'Shutdown Crowd' : 'Simulate 1M People'}
</button>
<div className="grid grid-cols-2 gap-2">
{CELEBRITIES.map(celeb => (
<button key={celeb.id} onClick={() => {
setActiveCelebrities(prev => prev.includes(celeb.id) ? prev.filter(c => c !== celeb.id) : [...prev, celeb.id]);
}} className={`flex items-center space-x-2 p-2 rounded-xl border ${activeCelebrities.includes(celeb.id) ? 'bg-[#E3B341] text-black border-[#E3B341]' : 'bg-white/5 border-white/10 text-white opacity-60'}`}>
<img src={celeb.img} className="w-6 h-6 rounded-full object-cover" />
<span className="text-[10px] font-bold truncate">{celeb.name}</span>
</button>
))}
</div>
{isSimActive && activeCelebrities.length > 0 && (
<div className="border-t border-white/10 pt-6">
<div className="relative">
<input type="text" value={userQuery} onChange={(e) => setUserQuery(e.target.value)} placeholder="Ask celebrities something..." className="w-full bg-white/5 border border-white/20 rounded-lg p-3 text-xs text-white focus:outline-none focus:border-[#00F2FF]" onKeyDown={(e) => e.key === 'Enter' && handleAskCelebrities()} />
<button onClick={handleAskCelebrities} disabled={isAsking} className="absolute right-2 top-1/2 -translate-y-1/2 p-1.5 text-[#00F2FF]"><BaseIcon path={isAsking ? mdiChatProcessingOutline : mdiSend} size={18} className={isAsking ? 'animate-spin' : ''} /></button>
</div>
<div className="relative">
<input type="text" value={userQuery} onChange={(e) => setUserQuery(e.target.value)} placeholder="Message the icons..." className="w-full bg-white/5 border border-white/20 rounded-xl p-3 text-xs text-white focus:outline-none focus:border-[#00F2FF]" onKeyDown={(e) => e.key === 'Enter' && handleAskCelebrities()} />
<button onClick={handleAskCelebrities} className="absolute right-3 top-1/2 -translate-y-1/2 text-[#00F2FF]"><BaseIcon path={isAsking ? mdiChatProcessingOutline : mdiSend} size={20} className={isAsking ? 'animate-spin' : ''} /></button>
</div>
)}
</div>
@ -424,53 +404,44 @@ const ObservationPage = () => {
)}
{isSimActive && chatMessages.length > 0 && (
<div className="absolute left-4 top-1/2 -translate-y-1/2 w-80 flex flex-col space-y-3 pointer-events-none max-h-[60vh] overflow-hidden">
<div className="absolute left-4 bottom-20 w-80 flex flex-col space-y-2 pointer-events-none">
{chatMessages.map(msg => (
<div key={msg.id} className={`bg-black/80 backdrop-blur-2xl p-4 border-l-4 rounded-r-2xl animate-fade-in-right shadow-2xl ${msg.isAi ? 'border-[#00F2FF]' : 'border-[#E3B341]'}`}>
<div className="flex justify-between items-center mb-1">
<span className={`${msg.isAi ? 'text-[#00F2FF]' : 'text-[#E3B341]'} font-black text-[10px] uppercase tracking-widest`}>{msg.name}</span>
</div>
<p className="text-white text-xs leading-relaxed">&quot;{msg.text}&quot;</p>
<div key={msg.id} className={`bg-black/80 backdrop-blur-xl p-3 border-l-2 rounded-r-xl animate-slide-in shadow-xl ${msg.isAi ? 'border-[#00F2FF]' : 'border-[#E3B341]'}`}>
<span className={`${msg.isAi ? 'text-[#00F2FF]' : 'text-[#E3B341]'} font-black text-[9px] uppercase`}>{msg.name}</span>
<p className="text-white text-[11px] leading-tight">&quot;{msg.text}&quot;</p>
</div>
))}
</div>
)}
<div className="absolute right-8 top-1/2 -translate-y-1/2 flex flex-col space-y-6 pointer-events-auto items-center">
<button onClick={() => handleZoom('in')} className="w-16 h-16 bg-black/90 border-2 border-[#00F2FF] flex items-center justify-center hover:bg-[#00F2FF] hover:text-black transition-all rounded-2xl shadow-[0_0_20px_rgba(0,242,255,0.4)]"><BaseIcon path={mdiMagnifyPlusOutline} size={32} /></button>
<div className="h-56 w-2 bg-gray-900/80 relative rounded-full overflow-hidden border border-white/10">
<div className="absolute bottom-0 left-0 w-full bg-[#00F2FF] transition-all duration-300" style={{ height: `${(Math.log10(zoom) / 14) * 100}%` }}></div>
</div>
<button onClick={() => handleZoom('out')} className="w-16 h-16 bg-black/90 border-2 border-[#00F2FF] flex items-center justify-center hover:bg-[#00F2FF] hover:text-black transition-all rounded-2xl"><BaseIcon path={mdiMagnifyMinusOutline} size={32} /></button>
</div>
<div className="flex flex-col items-center space-y-6">
<div className="flex flex-wrap justify-center gap-3 pointer-events-auto bg-black/80 p-4 backdrop-blur-2xl rounded-2xl border-2 border-white/10 max-w-2xl shadow-2xl">
<div className="flex flex-col items-center space-y-4 mb-4">
<div className="flex flex-wrap justify-center gap-2 pointer-events-auto bg-black/60 p-3 rounded-2xl border border-white/10">
{PRESET_TARGETS.map(target => (
<button key={target.id} onClick={() => selectTarget(target)} className={`px-4 py-2 text-[10px] font-black uppercase tracking-[0.25em] border-2 transition-all rounded-xl ${selectedTarget?.id === target.id ? 'bg-[#E3B341] text-black border-[#E3B341] shadow-[0_0_15px_rgba(227,179,65,0.5)]' : 'bg-black/60 text-white border-white/10 hover:border-[#E3B341]'}`}>{target.name}</button>
<button key={target.id} onClick={() => selectTarget(target)} className={`px-4 py-1.5 text-[9px] font-black uppercase tracking-widest border transition-all rounded-lg ${selectedTarget?.id === target.id ? 'bg-[#E3B341] text-black border-[#E3B341]' : 'text-white border-white/20 hover:border-[#E3B341]'}`}>{target.name}</button>
))}
</div>
<div className="flex justify-center space-x-8 pointer-events-auto bg-black/60 p-2 rounded-full border border-white/5 mb-4">
{[ { id: 'normal', label: 'VIS', icon: mdiWeatherNight }, { id: 'ir', label: 'NIRSpec', icon: mdiFlare }, { id: 'deep', label: 'MIRI', icon: mdiOrbitVariant } ].map(btn => (
<button key={btn.id} onClick={() => setMode(btn.id as any)} className={`group flex flex-col items-center p-3 w-20 transition-all ${mode === btn.id ? 'scale-110' : 'opacity-40 hover:opacity-100'}`}>
<div className={`p-3 rounded-2xl border-2 transition-all ${mode === btn.id ? 'border-[#00F2FF] bg-[#00F2FF]/20' : 'border-white/10'}`}><BaseIcon path={btn.icon} size={24} /></div>
<div className="text-[9px] mt-2 font-black tracking-tighter uppercase">{btn.label}</div>
</button>
))}
<div className="flex space-x-8 pointer-events-auto">
<button onClick={() => handleZoom('in')} className="p-4 bg-[#00F2FF] text-black rounded-full hover:scale-110 transition-transform"><BaseIcon path={mdiMagnifyPlusOutline} size={28} /></button>
<div className="flex items-center px-6 bg-black/80 rounded-full border border-[#00F2FF]/40">
<span className="text-xl font-black text-white">{formatZoom(zoom)}</span>
</div>
<button onClick={() => handleZoom('out')} className="p-4 bg-[#00F2FF] text-black rounded-full hover:scale-110 transition-transform"><BaseIcon path={mdiMagnifyMinusOutline} size={28} /></button>
</div>
</div>
</div>
)}
<div className="absolute inset-0 pointer-events-none z-40 bg-scanline opacity-[0.03]"></div>
<div className="absolute inset-0 pointer-events-none z-50 bg-scanlines opacity-[0.05]"></div>
<style jsx global>{`
@keyframes fade-in-right { from { opacity: 0; transform: translateX(-40px); } to { opacity: 1; transform: translateX(0); } }
.animate-fade-in-right { animation: fade-in-right 0.6s cubic-bezier(0.23, 1, 0.32, 1) forwards; }
.custom-scrollbar::-webkit-scrollbar { width: 6px; }
.custom-scrollbar::-webkit-scrollbar-thumb { background: #E3B341; border-radius: 10px; }
.bg-scanline { background: linear-gradient(to bottom, transparent 50%, rgba(0, 0, 0, 0.5) 51%); background-size: 100% 4px; }
@keyframes slide-in { from { opacity: 0; transform: translateX(-20px); } to { opacity: 1; transform: translateX(0); } }
@keyframes float { 0% { transform: translateY(0) translateX(0); } 50% { transform: translateY(-20px) translateX(10px); } 100% { transform: translateY(0) translateX(0); } }
@keyframes spin-slow { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
.animate-slide-in { animation: slide-in 0.4s ease-out forwards; }
.animate-float { animation: float infinite ease-in-out; }
.animate-spin-slow { animation: spin-slow 10s linear infinite; }
.bg-scanlines { background: linear-gradient(rgba(18, 16, 16, 0) 50%, rgba(0, 0, 0, 0.25) 50%), linear-gradient(90deg, rgba(255, 0, 0, 0.06), rgba(0, 255, 0, 0.02), rgba(0, 0, 255, 0.06)); background-size: 100% 2px, 3px 100%; }
`}</style>
</div>
);
};
ObservationPage.getLayout = function getLayout(page: ReactElement) { return <LayoutAuthenticated>{page}</LayoutAuthenticated>; };
export default ObservationPage;
export default ObservationPage;