Spaces:
Sleeping
Sleeping
| import React, { useState, useEffect, useRef, createContext, useContext } from 'react'; | |
| import { | |
| Brain, Zap, Search, Shield, Clock, Filter, Play, Menu, X, Check, ChevronRight, | |
| Coffee, BookOpen, Video, HelpCircle, Mail, Lock, Cpu, Wifi, Activity, Mic, MicOff, | |
| AlertTriangle, Radio, CreditCard, BarChart2, TrendingUp, User, LogOut, Sparkles, FileText, Users, Send | |
| } from 'lucide-react'; | |
| // ============================================ | |
| // CONFIGURAZIONE SISTEMA | |
| // ============================================ | |
| const SYSTEM_CONFIG = { | |
| OPENROUTER_KEY: import.meta.env.VITE_OPENROUTER_API_KEY || "", | |
| YOUTUBE_KEY: import.meta.env.VITE_YOUTUBE_API_KEY || "", | |
| USE_AI: true, | |
| FALLBACK_VIDEO_ID: "Y9EjnBmO2Jw", | |
| SYSTEM_FAILURE: false, // Flag for error testing | |
| SUPABASE_URL: import.meta.env.VITE_SUPABASE_URL, | |
| STRIPE_KEY: import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY | |
| }; | |
| import { supabase } from './lib/supabase'; | |
| import { loadStripe } from '@stripe/stripe-js'; | |
| import { Elements, PaymentElement, useStripe, useElements } from '@stripe/react-stripe-js'; | |
| const stripePromise = SYSTEM_CONFIG.STRIPE_KEY ? loadStripe(SYSTEM_CONFIG.STRIPE_KEY) : null; | |
| class ErrorBoundary extends React.Component { | |
| constructor(props) { | |
| super(props); | |
| this.state = { hasError: false, error: null, errorInfo: null }; | |
| } | |
| static getDerivedStateFromError(error) { | |
| return { hasError: true, error }; | |
| } | |
| componentDidCatch(error, errorInfo) { | |
| console.error("Uncaught error:", error, errorInfo); | |
| this.setState({ errorInfo }); | |
| } | |
| render() { | |
| if (this.state.hasError) { | |
| return ( | |
| <div className="fixed inset-0 z-[9999] flex items-center justify-center bg-black/80 backdrop-blur-md"> | |
| <div className="bg-red-900 border-2 border-red-500 rounded-xl p-8 max-w-2xl text-white shadow-[0_0_50px_rgba(239,68,68,0.5)]"> | |
| <h2 className="text-3xl font-black mb-4 flex items-center gap-3"><AlertTriangle className="w-8 h-8" /> SYSTEM FAILURE</h2> | |
| <div className="bg-black/50 p-4 rounded-lg font-mono text-sm mb-6 text-red-200 overflow-auto max-h-60"> | |
| {this.state.error && this.state.error.toString()} | |
| <br /> | |
| {this.state.errorInfo && this.state.errorInfo.componentStack} | |
| </div> | |
| <button onClick={() => window.location.reload()} className="w-full py-4 bg-red-600 hover:bg-red-700 font-bold rounded-lg transition-all"> | |
| RIAVVIA IL SISTEMA (RELOAD) | |
| </button> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| return this.props.children; | |
| } | |
| } | |
| // ============================================ | |
| // USER CONTEXT (Gestione Protocolli con localStorage) | |
| // ============================================ | |
| const UserContext = createContext(); | |
| const STORAGE_KEY = 'synapse_user_data'; | |
| const getStoredData = () => { | |
| try { | |
| const stored = localStorage.getItem(STORAGE_KEY); | |
| if (stored) return JSON.parse(stored); | |
| } catch (e) { } | |
| return null; | |
| }; | |
| const UserProvider = ({ children }) => { | |
| const stored = getStoredData(); | |
| const [protocol, setProtocol] = useState(stored?.protocol || 'GUEST'); | |
| const [credits, setCredits] = useState(stored?.credits ?? 5); | |
| const [showPayment, setShowPayment] = useState(false); | |
| const [currentView, setCurrentView] = useState(stored?.protocol === 'PRO' ? 'PRO_DASHBOARD' : 'LANDING'); | |
| const [userData, setUserData] = useState(stored?.userData || { name: 'Ospite', xp: 0, hours: 0, streak: 0, email: '' }); | |
| // Persist to localStorage | |
| useEffect(() => { | |
| localStorage.setItem(STORAGE_KEY, JSON.stringify({ protocol, credits, userData })); | |
| }, [protocol, credits, userData]); | |
| // Daily credit reset for Core | |
| useEffect(() => { | |
| const lastReset = localStorage.getItem('synapse_credit_reset'); | |
| const today = new Date().toDateString(); | |
| if (protocol === 'CORE' && lastReset !== today) { | |
| setCredits(5); | |
| localStorage.setItem('synapse_credit_reset', today); | |
| } | |
| }, [protocol]); | |
| const [session, setSession] = useState(null); | |
| // Supabase Auth Sync | |
| useEffect(() => { | |
| if (!supabase) return; | |
| const checkUser = (session) => { | |
| // Check for Dev Bypass | |
| const isBypass = localStorage.getItem('synapse_dev_bypass') === 'true'; | |
| if (isBypass) { | |
| setProtocol('PRO'); | |
| setUserData(p => ({ ...p, name: 'Sviluppatore (Force)', email: 'dev@synapse.os' })); | |
| return; | |
| } | |
| if (session) { | |
| if (session.user.email === 'chridipi04@gmail.com') { | |
| setProtocol('PRO'); | |
| setUserData(p => ({ ...p, name: 'Sviluppatore', email: session.user.email })); | |
| } else { | |
| setProtocol('CORE'); | |
| setUserData(p => ({ ...p, name: session.user.email.split('@')[0], email: session.user.email })); | |
| } | |
| } else { | |
| setProtocol('GUEST'); | |
| } | |
| }; | |
| supabase.auth.getSession().then(({ data: { session } }) => { | |
| setSession(session); | |
| checkUser(session); | |
| }); | |
| const { data: { subscription } } = supabase.auth.onAuthStateChange((_event, session) => { | |
| setSession(session); | |
| checkUser(session); | |
| }); | |
| return () => subscription.unsubscribe(); | |
| }, []); | |
| const upgradeToCore = () => { | |
| // Now handled by Auth | |
| setShowAuth(true); // Trigger Auth Modal | |
| }; | |
| const upgradeToPro = () => { | |
| if (!session) { setShowAuth(true); return; } | |
| // In real app, this waits for Stripe webhook. Here we simulate optimistic update after successful payment logic. | |
| setProtocol('PRO'); | |
| setUserData(prev => ({ ...prev, name: 'Scholar Pro', hours: prev.hours || 0 })); | |
| setShowPayment(false); | |
| setCurrentView('PRO_DASHBOARD'); | |
| }; | |
| const logout = async () => { | |
| if (supabase) await supabase.auth.signOut(); | |
| localStorage.removeItem(STORAGE_KEY); | |
| localStorage.removeItem('synapse_dev_bypass'); // Clear the bypass flag | |
| setProtocol('GUEST'); | |
| setSession(null); | |
| setCurrentView('LANDING'); | |
| window.location.reload(); // Force reload to ensure clean state | |
| }; | |
| const [showAuth, setShowAuth] = useState(false); | |
| const addXP = (amount) => setUserData(prev => ({ ...prev, xp: (prev.xp || 0) + amount })); | |
| const addHours = (amount) => setUserData(prev => ({ ...prev, hours: (prev.hours || 0) + amount })); | |
| return ( | |
| <UserContext.Provider value={{ | |
| protocol, credits, setCredits, showPayment, setShowPayment, | |
| userData, setUserData, upgradeToCore, upgradeToPro, | |
| currentView, setCurrentView, logout, showAuth, setShowAuth, | |
| addXP, addHours | |
| }}> | |
| {children} | |
| </UserContext.Provider> | |
| ); | |
| }; | |
| const useUser = () => useContext(UserContext); | |
| // ============================================ | |
| // VFX LAYER | |
| // ============================================ | |
| const VFXLayer = () => ( | |
| <> | |
| <div className="fixed inset-0 pointer-events-none z-50" style={{ background: `repeating-linear-gradient(0deg, transparent, transparent 2px, rgba(0,0,0,0.03) 2px, rgba(0,0,0,0.03) 4px)` }} /> | |
| <div className="fixed inset-0 pointer-events-none z-40" style={{ background: `radial-gradient(ellipse at center, transparent 40%, rgba(0,0,0,0.6) 100%)` }} /> | |
| </> | |
| ); | |
| // ============================================ | |
| // COMPONENTI UI BASE | |
| // ============================================ | |
| const GlitchText = ({ children, className = '' }) => ( | |
| <span className={`glitch-text ${className}`} data-text={children}>{children}</span> | |
| ); | |
| const HUDCard = ({ children, className = '', hoverGlow = true, locked = false, style = {} }) => ( | |
| <div className={`hud-card relative bg-slate-900/40 border border-white/5 ${className} ${hoverGlow && !locked ? 'hover:border-[#8b5cf6]/50' : ''} ${locked ? 'opacity-80' : ''}`} | |
| style={{ backgroundImage: `linear-gradient(rgba(255,255,255,0.02) 1px, transparent 1px), linear-gradient(90deg, rgba(255,255,255,0.02) 1px, transparent 1px)`, backgroundSize: '20px 20px', ...style }}> | |
| <div className="hud-corner hud-corner-tl" /><div className="hud-corner hud-corner-tr" /> | |
| <div className="hud-corner hud-corner-bl" /><div className="hud-corner hud-corner-br" /> | |
| {locked && ( | |
| <div className="absolute inset-0 bg-black/60 backdrop-blur-sm z-10 flex flex-col items-center justify-center border border-white/10"> | |
| <Lock className="w-8 h-8 text-slate-500 mb-2" /> | |
| <span className="font-mono text-xs text-slate-400 uppercase tracking-widest">Accesso Limitato</span> | |
| <span className="font-mono text-[10px] text-[#f43f5e] mt-1">RICHIESTO UPGRADE PROTOCOLLO</span> | |
| </div> | |
| )} | |
| {children} | |
| </div> | |
| ); | |
| const MagneticButton = ({ children, className = '', onClick, disabled, ...props }) => { | |
| const buttonRef = useRef(null); | |
| const [offset, setOffset] = useState({ x: 0, y: 0 }); | |
| const handleMouseMove = (e) => { | |
| if (!buttonRef.current || disabled) return; | |
| const rect = buttonRef.current.getBoundingClientRect(); | |
| setOffset({ x: (e.clientX - rect.left - rect.width / 2) * 0.2, y: (e.clientY - rect.top - rect.height / 2) * 0.2 }); | |
| }; | |
| return ( | |
| <button ref={buttonRef} onMouseMove={handleMouseMove} onMouseLeave={() => setOffset({ x: 0, y: 0 })} onClick={onClick} disabled={disabled} | |
| className={`transition-transform duration-200 ${className} ${disabled ? 'opacity-50 cursor-not-allowed' : ''}`} | |
| style={{ transform: `translate(${offset.x}px, ${offset.y}px)` }} {...props}> | |
| {children} | |
| </button> | |
| ); | |
| }; | |
| // ============================================ | |
| // NEURAL VOID (Background) | |
| // ============================================ | |
| const NeuralVoid = () => ( | |
| <div className="fixed inset-0 pointer-events-none"> | |
| <div className="absolute inset-0 bg-[#020617]" /> | |
| <div className="absolute inset-0 opacity-20" style={{ background: 'radial-gradient(circle at 50% 50%, #8b5cf6 0%, transparent 50%)', filter: 'blur(100px)' }} /> | |
| <div className="absolute top-0 left-0 w-full h-full opacity-10" style={{ backgroundImage: 'url("data:image/svg+xml,%3Csvg width=\'60\' height=\'60\' viewBox=\'0 0 60 60\' xmlns=\'http://www.w3.org/2000/svg\'%3E%3Cg fill=\'none\' fill-rule=\'evenodd\'%3E%3Cg fill=\'%239C92AC\' fill-opacity=\'0.2\'%3E%3Cpath d=\'M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z\'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E")' }} /> | |
| </div> | |
| ); | |
| // ============================================ | |
| // HEADER (Landing) | |
| // ============================================ | |
| const Header = () => { | |
| const { setShowAuth, protocol, userData, logout, setCurrentView } = useUser(); | |
| return ( | |
| <header className="fixed top-0 left-0 right-0 z-50 py-6 px-6 backdrop-blur-md border-b border-white/5 bg-[#020617]/50"> | |
| <div className="max-w-7xl mx-auto flex justify-between items-center"> | |
| <div className="flex items-center gap-2"> | |
| <Brain className="w-8 h-8 text-[#8b5cf6]" /> | |
| <span className="text-2xl font-black text-white tracking-widest">SYNAPSE</span> | |
| </div> | |
| {protocol === 'GUEST' ? ( | |
| <button onClick={() => setShowAuth(true)} className="px-4 py-2 md:px-6 bg-white/10 hover:bg-white/20 text-white font-bold text-xs md:text-base rounded-lg border border-white/10 transition-all whitespace-nowrap"> | |
| ACCESSO NEURALE | |
| </button> | |
| ) : ( | |
| <div className="flex items-center gap-2 md:gap-4"> | |
| <div className="text-right hidden md:block"> | |
| <div className="text-xs text-slate-400">BENTORNATO</div> | |
| <div className="text-sm font-bold text-white">{userData?.name || 'Utente'}</div> | |
| </div> | |
| {protocol === 'PRO' && ( | |
| <button onClick={() => setCurrentView('PRO_DASHBOARD')} className="px-3 py-2 md:px-4 bg-[#8b5cf6] text-white font-bold text-[10px] md:text-xs rounded hover:bg-[#7c3aed]"> | |
| DASHBOARD | |
| </button> | |
| )} | |
| <button onClick={logout} className="p-2 text-slate-400 hover:text-white" title="Esci"> | |
| <LogOut className="w-5 h-5" /> | |
| </button> | |
| </div> | |
| )} | |
| </div> | |
| </header> | |
| ); | |
| }; | |
| // ============================================ | |
| // NEURAL CANVAS | |
| // ============================================ | |
| const NeuralCanvas = () => { | |
| const canvasRef = useRef(null); | |
| const mouseRef = useRef({ x: 0, y: 0 }); | |
| const nodesRef = useRef([]); | |
| const animationRef = useRef(null); | |
| useEffect(() => { | |
| const canvas = canvasRef.current; | |
| if (!canvas) return; | |
| const ctx = canvas.getContext('2d'); | |
| let width = window.innerWidth, height = window.innerHeight; | |
| const resize = () => { width = window.innerWidth; height = window.innerHeight; canvas.width = width; canvas.height = height; initNodes(); }; | |
| const initNodes = () => { | |
| const count = Math.floor((width * height) / 12000); | |
| nodesRef.current = Array.from({ length: count }, () => ({ | |
| x: Math.random() * width, y: Math.random() * height, baseX: Math.random() * width, baseY: Math.random() * height, | |
| vx: (Math.random() - 0.5) * 0.4, vy: (Math.random() - 0.5) * 0.4, radius: Math.random() * 2 + 1.5, | |
| opacity: 0.3 + Math.random() * 0.5, pulsePhase: Math.random() * Math.PI * 2, pulseSpeed: 0.01 + Math.random() * 0.02, | |
| })); | |
| }; | |
| const handleMouseMove = (e) => { mouseRef.current = { x: e.clientX, y: e.clientY }; }; | |
| const animate = () => { | |
| ctx.fillStyle = '#020617'; ctx.fillRect(0, 0, width, height); | |
| const nodes = nodesRef.current, mouse = mouseRef.current; | |
| nodes.forEach((node, i) => { | |
| node.pulsePhase += node.pulseSpeed; | |
| node.opacity = 0.3 + (Math.sin(node.pulsePhase) + 1) * 0.25; | |
| const dx = mouse.x - node.x, dy = mouse.y - node.y, dist = Math.sqrt(dx * dx + dy * dy); | |
| if (dist < 150 && dist > 0) { const force = (150 - dist) / 150; node.vx += (dx / dist) * force * 0.08; node.vy += (dy / dist) * force * 0.08; } | |
| node.vx += (node.baseX - node.x) * 0.002; node.vy += (node.baseY - node.y) * 0.002; | |
| node.x += node.vx; node.y += node.vy; node.vx *= 0.96; node.vy *= 0.96; | |
| if (node.x < 0 || node.x > width) node.vx *= -0.5; if (node.y < 0 || node.y > height) node.vy *= -0.5; | |
| node.x = Math.max(0, Math.min(width, node.x)); node.y = Math.max(0, Math.min(height, node.y)); | |
| for (let j = i + 1; j < nodes.length; j++) { | |
| const other = nodes[j], distance = Math.sqrt((node.x - other.x) ** 2 + (node.y - other.y) ** 2); | |
| if (distance < 140) { | |
| const nearMouse = Math.sqrt((mouse.x - node.x) ** 2 + (mouse.y - node.y) ** 2) < 150 || Math.sqrt((mouse.x - other.x) ** 2 + (mouse.y - other.y) ** 2) < 150; | |
| ctx.beginPath(); ctx.moveTo(node.x, node.y); ctx.lineTo(other.x, other.y); | |
| ctx.strokeStyle = nearMouse ? '#06b6d4' : '#8b5cf6'; ctx.lineWidth = nearMouse ? 2 : 1; | |
| ctx.globalAlpha = (1 - distance / 140) * (nearMouse ? 0.8 : 0.3); ctx.stroke(); | |
| } | |
| } | |
| ctx.globalAlpha = node.opacity; ctx.shadowBlur = 15; ctx.shadowColor = '#8b5cf6'; | |
| ctx.beginPath(); ctx.arc(node.x, node.y, node.radius, 0, Math.PI * 2); ctx.fillStyle = '#8b5cf6'; ctx.fill(); ctx.shadowBlur = 0; | |
| }); | |
| ctx.globalAlpha = 1; animationRef.current = requestAnimationFrame(animate); | |
| }; | |
| resize(); window.addEventListener('resize', resize); window.addEventListener('mousemove', handleMouseMove); animate(); | |
| return () => { window.removeEventListener('resize', resize); window.removeEventListener('mousemove', handleMouseMove); if (animationRef.current) cancelAnimationFrame(animationRef.current); }; | |
| }, []); | |
| return <canvas ref={canvasRef} className="fixed inset-0 pointer-events-none" style={{ zIndex: 0 }} />; | |
| }; | |
| // ============================================ | |
| // LIVE TICKER | |
| // ============================================ | |
| const LiveTicker = () => { | |
| const messages = [ | |
| '[LIVE] User_X99: Sessione "Fisica" completata (+200XP)', | |
| '[LIVE] Sistema: Database aggiornato con 400 nuovi video', | |
| '[LIVE] User_Anna: Ha sbloccato "Memory Lock Pro"', | |
| '[LIVE] User_Marco: Quiz Storia completato (98%)', | |
| '[LIVE] User_Sara: 5 ore di Deep Work questa settimana', | |
| ]; | |
| return ( | |
| <div className="fixed top-[72px] left-0 right-0 z-40 bg-[#020617]/80 backdrop-blur-sm border-b border-white/5 overflow-hidden"> | |
| <div className="ticker-track flex"> | |
| {[...messages, ...messages].map((msg, i) => ( | |
| <span key={i} className="ticker-item font-mono text-xs text-slate-500 whitespace-nowrap px-8 py-2"> | |
| <span className="text-[#10b981] mr-2">●</span>{msg} | |
| </span> | |
| ))} | |
| </div> | |
| </div> | |
| ); | |
| }; | |
| // ============================================ | |
| // SYSTEM HEADER | |
| // ============================================ | |
| const SystemHeader = () => { | |
| const { protocol, userData, setShowPayment, currentView, setCurrentView, logout } = useUser(); | |
| const [isScrolled, setIsScrolled] = useState(false); | |
| const [latency, setLatency] = useState(12); | |
| useEffect(() => { | |
| const handleScroll = () => setIsScrolled(window.scrollY > 50); | |
| window.addEventListener('scroll', handleScroll); | |
| const interval = setInterval(() => setLatency(Math.floor(8 + Math.random() * 15)), 2000); | |
| return () => { window.removeEventListener('scroll', handleScroll); clearInterval(interval); }; | |
| }, []); | |
| return ( | |
| <header className={`fixed top-0 left-0 right-0 z-50 transition-all duration-500 ${isScrolled ? 'bg-slate-900/90 backdrop-blur-xl border-b border-white/5' : 'bg-transparent'}`}> | |
| <div className="max-w-7xl mx-auto px-6 py-4 flex items-center justify-between"> | |
| <div className="flex items-center gap-2"> | |
| <Brain className="w-7 h-7 text-[#8b5cf6]" strokeWidth={1.5} /> | |
| <GlitchText className="text-xl font-black text-white">SYNAPSE <span className="text-[#06b6d4] animate-pulse">[OS]</span></GlitchText> | |
| </div> | |
| <div className="flex items-center gap-2 md:gap-6"> | |
| <div className="hidden md:flex items-center gap-2 font-mono text-xs uppercase text-[#10b981]"><Wifi className="w-3 h-3" /> RETE: SICURA</div> | |
| <div className="hidden md:flex items-center gap-2 font-mono text-xs uppercase text-slate-400"><Activity className="w-3 h-3" /> LATENZA: {latency}ms</div> | |
| {protocol === 'GUEST' ? ( | |
| <MagneticButton onClick={() => document.getElementById('pricing')?.scrollIntoView({ behavior: 'smooth' })} | |
| className="px-4 py-2 bg-[#8b5cf6] text-white font-bold text-xs rounded hover:bg-[#7c3aed]"> | |
| ACCEDI | |
| </MagneticButton> | |
| ) : ( | |
| <div className="flex items-center gap-2 md:gap-3"> | |
| {protocol === 'PRO' && ( | |
| <> | |
| <span className="hidden md:inline px-2 py-1 bg-[#8b5cf6]/20 text-[#8b5cf6] text-[10px] font-bold rounded border border-[#8b5cf6]/50">PRO</span> | |
| <button onClick={() => setCurrentView(currentView === 'PRO_DASHBOARD' ? 'LANDING' : 'PRO_DASHBOARD')} | |
| className="px-3 py-1 bg-[#8b5cf6] text-white text-[10px] md:text-xs font-bold rounded hover:bg-[#7c3aed] whitespace-nowrap"> | |
| {currentView === 'PRO_DASHBOARD' ? 'HOME' : 'DASHBOARD'} | |
| </button> | |
| </> | |
| )} | |
| {protocol === 'CORE' && <span className="hidden md:inline px-2 py-1 bg-[#06b6d4]/20 text-[#06b6d4] text-[10px] font-bold rounded border border-[#06b6d4]/50">CORE</span>} | |
| <div className="text-right hidden md:block"><div className="text-xs text-white font-bold">{userData.name}</div></div> | |
| <div className="w-8 h-8 rounded-full bg-slate-800 flex items-center justify-center border border-white/10 shrink-0"><User className="w-4 h-4 text-slate-400" /></div> | |
| {protocol === 'CORE' && <button onClick={() => setShowPayment(true)} className="px-3 py-1 bg-[#06b6d4] text-black text-xs font-bold rounded hover:bg-[#22d3ee]">UPGRADE</button>} | |
| <button onClick={logout} className="p-2 text-slate-500 hover:text-[#f43f5e] transition-colors" title="Logout"><LogOut className="w-4 h-4" /></button> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| </header> | |
| ); | |
| }; | |
| // ============================================ | |
| // PAYMENT MODAL | |
| // ============================================ | |
| // ============================================ | |
| // AUTH MODAL (Login/Signup) | |
| // ============================================ | |
| const AuthModal = () => { | |
| const { showAuth, setShowAuth, setProtocol, setUserData } = useUser(); | |
| const [isLogin, setIsLogin] = useState(false); // Default to Signup as requested "Iscriviti la prima volta" | |
| const [email, setEmail] = useState(''); | |
| const [password, setPassword] = useState(''); | |
| const [loading, setLoading] = useState(false); | |
| const [error, setError] = useState(''); | |
| if (!showAuth) return null; | |
| const handleAuth = async (e) => { | |
| e.preventDefault(); | |
| setLoading(true); | |
| setError(''); | |
| try { | |
| if (!supabase) throw new Error("Supabase non configurato (Mancano API Keys)"); | |
| if (isLogin) { | |
| const { data, error } = await supabase.auth.signInWithPassword({ email, password }); | |
| if (error) throw error; | |
| console.log("Login Success", data); | |
| } else { | |
| const { data, error } = await supabase.auth.signUp({ email, password }); | |
| if (error) throw error; | |
| if (data?.user && !data?.session) { | |
| setError("Controlla la tua email per confermare l'iscrizione!"); | |
| return; | |
| } | |
| console.log("Signup Success", data); | |
| } | |
| setShowAuth(false); | |
| } catch (err) { | |
| console.error("Auth Error", err); | |
| setError(err.message); | |
| } finally { | |
| setLoading(false); | |
| } | |
| }; | |
| return ( | |
| <div className="fixed inset-0 z-[100] flex items-center justify-center px-4"> | |
| <div className="absolute inset-0 bg-black/80 backdrop-blur-md" onClick={() => setShowAuth(false)} /> | |
| <HUDCard className="w-full max-w-md p-8 bg-slate-900 border-[#06b6d4] shadow-[0_0_50px_rgba(6,182,212,0.2)] z-10"> | |
| <button onClick={() => setShowAuth(false)} className="absolute top-4 right-4 text-slate-500 hover:text-white"><X className="w-5 h-5" /></button> | |
| <h2 className="text-2xl font-black text-white mb-2 flex items-center gap-2"> | |
| <User className="w-6 h-6 text-[#06b6d4]" /> {isLogin ? 'ACCESSO NEURALE' : 'CREA IDENTITÀ'} | |
| </h2> | |
| <p className="text-slate-400 text-sm mb-6 font-mono"> | |
| {isLogin ? 'Bentornato nel sistema, Operatore.' : 'Registrati per avviare il protocollo Synapse.'} | |
| </p> | |
| {error && <div className="p-3 bg-red-500/20 border border-red-500/50 rounded mb-4 text-red-200 text-xs font-mono">{error}</div>} | |
| <form onSubmit={handleAuth} className="space-y-4"> | |
| <div> | |
| <label className="text-xs font-bold text-slate-500 mb-1 block">EMAIL</label> | |
| <input type="email" value={email} onChange={(e) => setEmail(e.target.value)} className="w-full bg-slate-800 border border-slate-700 rounded p-3 text-white focus:border-[#06b6d4] outline-none" required /> | |
| </div> | |
| <div> | |
| <label className="text-xs font-bold text-slate-500 mb-1 block">PASSWORD</label> | |
| <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} className="w-full bg-slate-800 border border-slate-700 rounded p-3 text-white focus:border-[#06b6d4] outline-none" required /> | |
| </div> | |
| <button disabled={loading} className="w-full py-4 bg-[#06b6d4] hover:bg-[#22d3ee] text-black font-bold rounded-xl transition-all flex items-center justify-center gap-2"> | |
| {loading ? <div className="w-4 h-4 border-2 border-black border-t-transparent rounded-full animate-spin" /> : (isLogin ? 'LOGIN' : 'CREA ACCOUNT')} | |
| </button> | |
| </form> | |
| <div className="mt-6 text-center"> | |
| <button type="button" onClick={() => setIsLogin(!isLogin)} className="text-slate-400 hover:text-white text-xs underline"> | |
| {isLogin ? 'Non hai un account? Iscriviti' : 'Hai già un account? Accedi'} | |
| </button> | |
| </div> | |
| </HUDCard> | |
| </div> | |
| ); | |
| }; | |
| // ============================================ | |
| // STRIPE CHECKOUT UTILS | |
| // ============================================ | |
| const StripeCheckout = ({ clientSecret }) => { | |
| const stripe = useStripe(); | |
| const elements = useElements(); | |
| const { upgradeToPro } = useUser(); | |
| const [loading, setLoading] = useState(false); | |
| const [error, setError] = useState(''); | |
| const handleSubmit = async (event) => { | |
| event.preventDefault(); | |
| if (!stripe || !elements) return; | |
| setLoading(true); | |
| const { error: submitError } = await elements.submit(); | |
| if (submitError) { | |
| setError(submitError.message); | |
| setLoading(false); | |
| return; | |
| } | |
| // Confirm the payment | |
| const { error } = await stripe.confirmPayment({ | |
| elements, | |
| clientSecret, | |
| confirmParams: { | |
| return_url: 'http://localhost:5173', // Redirects here after payment | |
| }, | |
| redirect: 'if_required' | |
| }); | |
| if (error) { | |
| setError(error.message); | |
| setLoading(false); | |
| } else { | |
| // Payment succeeded | |
| upgradeToPro(); | |
| } | |
| }; | |
| return ( | |
| <form onSubmit={handleSubmit} className="flex flex-col h-full"> | |
| <div className="flex-1 overflow-y-auto max-h-[60vh] pr-2 custom-scrollbar"> | |
| <div className="mb-6"> | |
| <label className="text-xs font-bold text-slate-500 mb-2 block">DETTAGLI CARTA</label> | |
| <div className="p-4 bg-slate-800 border border-slate-700 rounded-xl"> | |
| <PaymentElement options={{ theme: 'night', layout: 'accordion' }} /> | |
| </div> | |
| </div> | |
| {error && <div className="text-red-400 text-xs mb-4">{error}</div>} | |
| <div className="flex justify-between items-center border-t border-white/5 pt-4 mb-6"><span className="text-slate-400 text-sm">Totale</span><span className="text-xl font-bold text-white">€9.99</span></div> | |
| </div> | |
| <button disabled={!stripe || loading} className="w-full py-4 mt-4 bg-[#8b5cf6] hover:bg-[#7c3aed] text-white font-bold rounded-xl transition-all shadow-[0_0_20px_rgba(139,92,246,0.5)] shrink-0"> | |
| {loading ? 'ELABORAZIONE...' : 'PAGA ORA'} | |
| </button> | |
| </form> | |
| ); | |
| }; | |
| // ============================================ | |
| // PAYMENT MODAL (Stripe Aware) | |
| // ============================================ | |
| const PaymentModal = () => { | |
| const { showPayment, setShowPayment, upgradeToPro } = useUser(); | |
| const [clientSecret, setClientSecret] = useState(null); | |
| useEffect(() => { | |
| if (showPayment && SYSTEM_CONFIG.STRIPE_KEY) { | |
| // Fetch fetch client secret from our local node server | |
| console.log("Fetching Payment Intent..."); | |
| fetch('http://localhost:4242/create-payment-intent', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| }) | |
| .then(res => { | |
| if (!res.ok) throw new Error("Server Error"); | |
| return res.json(); | |
| }) | |
| .then(data => { | |
| console.log("Secret received"); | |
| setClientSecret(data.clientSecret); | |
| }) | |
| .catch(err => { | |
| console.error("Payment Fetch Error", err); | |
| // Fallback for demo if server is dead | |
| // alert("Server Pagamenti Offline. Controlla che 'node server.js' sia attivo."); | |
| }); | |
| } | |
| }, [showPayment]); | |
| if (!showPayment) return null; | |
| return ( | |
| <div className="fixed inset-0 z-[100] flex items-center justify-center px-4"> | |
| <div className="absolute inset-0 bg-black/80 backdrop-blur-md" onClick={() => setShowPayment(false)} /> | |
| <HUDCard className="w-full max-w-md p-8 bg-slate-900 border-[#8b5cf6] shadow-[0_0_50px_rgba(139,92,246,0.2)] z-10"> | |
| <button onClick={() => setShowPayment(false)} className="absolute top-4 right-4 text-slate-500 hover:text-white"><X className="w-5 h-5" /></button> | |
| <div className="flex items-center gap-3 mb-6"><CreditCard className="w-6 h-6 text-[#8b5cf6]" /><h3 className="text-xl font-bold text-white">UPGRADE SCHOLAR PRO</h3></div> | |
| {stripePromise && clientSecret ? ( | |
| <Elements stripe={stripePromise} options={{ clientSecret, appearance: { theme: 'night', labels: 'floating' } }}> | |
| <StripeCheckout clientSecret={clientSecret} /> | |
| </Elements> | |
| ) : ( | |
| <div className="text-center py-12"> | |
| <div className="w-16 h-16 border-4 border-[#8b5cf6] border-t-transparent rounded-full animate-spin mx-auto mb-6" /> | |
| <p className="text-white font-bold">Inizializzazione Stripe Sicuro...</p> | |
| </div> | |
| )} | |
| </HUDCard> | |
| </div> | |
| ); | |
| }; | |
| // ============================================ | |
| // TYPEWRITER HOOK | |
| // ============================================ | |
| const useTypewriter = (lines, speed = 40, pauseBetween = 500) => { | |
| const [currentLine, setCurrentLine] = useState(0); | |
| const [currentChar, setCurrentChar] = useState(0); | |
| const [displayLines, setDisplayLines] = useState([]); | |
| const [isComplete, setIsComplete] = useState(false); | |
| useEffect(() => { | |
| if (currentLine >= lines.length) { setIsComplete(true); return; } | |
| const line = lines[currentLine]; | |
| if (currentChar < line.length) { | |
| const timeout = setTimeout(() => { setDisplayLines((prev) => { const newLines = [...prev]; newLines[currentLine] = line.slice(0, currentChar + 1); return newLines; }); setCurrentChar(currentChar + 1); }, speed); | |
| return () => clearTimeout(timeout); | |
| } else { | |
| const timeout = setTimeout(() => { setCurrentLine(currentLine + 1); setCurrentChar(0); }, pauseBetween); | |
| return () => clearTimeout(timeout); | |
| } | |
| }, [currentLine, currentChar, lines, speed, pauseBetween]); | |
| return { displayLines, isComplete }; | |
| }; | |
| // ============================================ | |
| // HERO UNIT | |
| // ============================================ | |
| const HeroUnit = () => { | |
| const { displayLines, isComplete } = useTypewriter([ | |
| '> Inizializzazione Neural Uplink...', | |
| '> Ottimizzazione Protocolli di Memoria...', | |
| '> Sistema Pronto.', | |
| ], 30, 400); | |
| return ( | |
| <section className="pt-40 pb-20 flex items-center justify-center px-6 relative" style={{ zIndex: 1 }}> | |
| <div className="absolute top-20 left-1/4 w-[500px] h-[500px] rounded-full animate-pulse" style={{ background: 'rgba(139, 92, 246, 0.1)', filter: 'blur(150px)' }} /> | |
| <div className="absolute bottom-20 right-1/4 w-[400px] h-[400px] rounded-full animate-pulse" style={{ background: 'rgba(6, 182, 212, 0.1)', filter: 'blur(150px)', animationDelay: '1s' }} /> | |
| <div className="max-w-4xl mx-auto text-center relative z-10"> | |
| <h1 className="text-5xl md:text-7xl lg:text-8xl font-black tracking-tight mb-8"> | |
| <GlitchText className="bg-gradient-to-r from-[#8b5cf6] to-[#06b6d4] bg-clip-text text-transparent">POTENZIA LA TUA MENTE.</GlitchText> | |
| </h1> | |
| <HUDCard className="mb-12 max-w-md mx-auto p-4 rounded-lg" hoverGlow={false}> | |
| <div className="font-mono text-sm text-slate-400 text-left"> | |
| {displayLines.map((line, i) => (<div key={i} className={i === displayLines.length - 1 && isComplete ? 'text-[#10b981]' : ''}>{line}{i === displayLines.length - 1 && !isComplete && <span className="animate-pulse text-[#06b6d4]">█</span>}</div>))} | |
| {displayLines.length === 0 && <span className="animate-pulse text-[#06b6d4]">█</span>} | |
| </div> | |
| </HUDCard> | |
| <MagneticButton onClick={() => document.getElementById('simulator')?.scrollIntoView({ behavior: 'smooth' })} | |
| className="group px-10 py-5 bg-gradient-to-r from-[#8b5cf6] to-[#7c3aed] text-white font-bold text-lg rounded-xl hover:scale-105 shadow-[0_0_40px_rgba(139,92,246,0.5)] relative overflow-hidden"> | |
| <span className="relative z-10 flex items-center gap-3"><Zap className="w-6 h-6" strokeWidth={1.5} />AVVIA</span> | |
| <div className="absolute inset-0 bg-gradient-to-r from-[#06b6d4] to-[#8b5cf6] opacity-0 group-hover:opacity-100 transition-opacity duration-500" /> | |
| </MagneticButton> | |
| <div className="mt-20 animate-bounce"><ChevronRight className="w-6 h-6 text-slate-500 rotate-90 mx-auto" strokeWidth={1.5} /></div> | |
| </div> | |
| </section> | |
| ); | |
| }; | |
| // ============================================ | |
| // VIDEO DATABASE & SIMULATION LOGIC | |
| // ============================================ | |
| // ID Verificati & Quiz & Riassunti (Curati da Gemini 2 Flash) | |
| const VIDEO_DATABASE = { | |
| 'fisica': { | |
| id: 'Y9EjnBmO2Jw', | |
| title: 'I Principi della Dinamica (Hub Scuola)', | |
| summary: "STUDIO: La dinamica è la parte della fisica che studia come si muovono i corpi per effetto delle forze. \n1. Principio d'Inerzia: Se nessuna forza agisce su un corpo, esso mantiene il suo stato (quiete o moto rettilineo uniforme). \n2. Legge Fondamentale (F=ma): La forza è il prodotto tra massa e accelerazione. \n3. Azione e Reazione: A ogni forza corrisponde una forza uguale e contraria.", | |
| quiz: { | |
| question: "Quale principio afferma che F = m * a?", | |
| options: [{ text: "Primo Principio", correct: false }, { text: "Secondo Principio", correct: true }, { text: "Terzo Principio", correct: false }], | |
| hint: "È la legge fondamentale della dinamica che collega causa ed effetto." | |
| } | |
| }, | |
| 'napoleone': { | |
| id: '2U_YdZD5kkM', | |
| title: 'Napoleone Bonaparte - Sintesi Completa', | |
| summary: "BIOGRAFIA: Generale e Imperatore francese (1769-1821). \nASCESA: Sfruttò il caos post-rivoluzione. Famoso per le campagne d'Italia e d'Egitto. Autoproclamato Imperatore nel 1804. \nRIFORME: Introdusse il Codice Civile (basi diritto moderno). \nCADUTA: Disastrosa campagna di Russia (1812), sconfitto a Lipsia e Waterloo (1815). Esiliato a Sant'Elena.", | |
| quiz: { | |
| question: "In quale isola morì Napoleone in esilio?", | |
| options: [{ text: "Isola d'Elba", correct: false }, { text: "Sant'Elena", correct: true }, { text: "Corsica", correct: false }], | |
| hint: "Un'isola remota nell'Oceano Atlantico meridionale." | |
| } | |
| }, | |
| 'storia': { | |
| id: 'd_kS3x0lJ4k', | |
| title: 'La Prima Guerra Mondiale (In 5 minuti)', | |
| summary: "GRANDE GUERRA (1914-1918): Scatenata dall'attentato di Sarajevo. \nSCHIERAMENTI: Triplice Intesa (Francia, UK, Russia, poi Italia/USA) vs Imperi Centrali (Austria, Germania). \nCARATTERISTICHE: Guerra di trincea, logoramento, nuove armi (gas, aerei, carri). \nESITO: Crollo di 4 imperi, nascita di nuovi stati, riassetto dell'Europa con Versailles.", | |
| quiz: { | |
| question: "Quale evento fece scoppiare la guerra?", | |
| options: [{ text: "Invasione della Polonia", correct: false }, { text: "Attentato di Sarajevo", correct: true }, { text: "Presa della Bastiglia", correct: false }], | |
| hint: "L'assassinio dell'Arciduca Francesco Ferdinando." | |
| } | |
| }, | |
| 'chimica': { | |
| id: '?listType=search&list=Tavola+Periodica+Spiegazione+Semplice', | |
| title: 'La Tavola Periodica degli Elementi', | |
| summary: "STRUTTURA: Organizza gli elementi chimici ordinati per numero atomico (Z). \nGRUPPI E PERIODI: Le colonne (gruppi) hanno proprietà simili; le righe (periodi) indicano il livello energetico. \nCLASSIFICAZIONE: Metalli (sinistra), Non metalli (destra), Gas Nobili (ultima colonna, stabili). Fondamentale per prevedere le reazioni chimiche.", | |
| quiz: { | |
| question: "Come sono ordinati gli elementi nella tavola?", | |
| options: [{ text: "Per data di scoperta", correct: false }, { text: "Per numero atomico crescente", correct: true }, { text: "Alfabeticamente", correct: false }], | |
| hint: "Il numero di protoni nel nucleo decide la posizione." | |
| } | |
| }, | |
| 'matematica': { | |
| id: '?listType=search&list=Equazioni+Primo+Grado+Spiegazione', | |
| title: 'Equazioni Lineari (Algebra)', | |
| summary: "CONCETTO: Uguaglianza tra due espressioni verificata solo per certi valori (soluzioni). \nRISOLUZIONE: L'obiettivo è isolare la 'x'. \nPRINCIPI: \n1. Sommando/sottraendo la stessa quantità a entrambi i membri, il risultato non cambia. \n2. Moltiplicando/dividendo entrambi i membri per uno stesso numero (diverso da 0), l'equazione resta equivalente.", | |
| quiz: { | |
| question: "Qual è il primo passaggio per risolvere 2x + 5 = 15?", | |
| options: [{ text: "Dividere tutto per 2", correct: false }, { text: "Sottrarre 5 da entrambi i lati", correct: true }, { text: "Moltiplicare per x", correct: false }], | |
| hint: "Devi isolare il termine con la x spostando i numeri." | |
| } | |
| }, | |
| 'italiano': { | |
| id: 'fESdidM5j7s', | |
| title: 'La Divina Commedia in 10 minuti', | |
| summary: "OPERA: Poema allegorico in terzine incatenate. \nVIAGGIO: Dante attraversa i tre regni ultraterreni. \nINFERNO: Voragine a imbuto, pena del contrappasso. \nPURGATORIO: Montagna dove le anime espiano i peccati. \nPARADISO: Cieli concentrici di pura luce e beatitudine. \nGUIDE: Virgilio (Ragione), Beatrice (Teologia/Grazia).", | |
| quiz: { | |
| question: "Chi guida Dante attraverso l'Inferno?", | |
| options: [{ text: "Beatrice", correct: false }, { text: "Virgilio", correct: true }, { text: "San Pietro", correct: false }], | |
| hint: "Il sommo poeta latino autore dell'Eneide." | |
| } | |
| }, | |
| 'inglese': { | |
| id: 'M2K-kM2i_tQ', | |
| title: 'Inglese: Basi Fondamentali', | |
| summary: "GRAMMATICA BASE: \n1. Verbo To Be (Essere): I am, You are, He is. \n2. Ordine parole: Subject + Verb + Object (SVO). \n3. Present Simple: Per abitudini e verità generali (Add 's' for he/she/it). \nCONSIGLIO: La pratica dell'ascolto (listening) è cruciale quanto la grammatica.", | |
| quiz: { | |
| question: "Qual è la forma corretta?", | |
| options: [{ text: "He go to school", correct: false }, { text: "He goes to school", correct: true }, { text: "He going to school", correct: false }], | |
| hint: "Terza persona singolare richiede la 's' o 'es'." | |
| } | |
| }, | |
| }; | |
| const getTopicVideo = (topic) => { | |
| const query = topic.toLowerCase().trim(); | |
| const searchKey = Object.keys(VIDEO_DATABASE).find(key => query.includes(key)); | |
| if (searchKey) return VIDEO_DATABASE[searchKey]; | |
| // FALLBACK INTELLIGENTE: YouTube Search Embed | |
| // Genera una ricerca dinamica se non abbiamo un ID specifico | |
| return { | |
| id: `?listType=search&list=${encodeURIComponent(topic + ' spiegazione scuola')}`, | |
| title: `Ricerca approfondita: ${topic}`, | |
| summary: `Generazione sintesi automatica per: ${topic}. L'argomento richiede un'analisi approfondita delle fonti video correlate. Consulta il video per i dettagli specifici e prendi appunti sui concetti chiave.`, | |
| quiz: { | |
| question: `Qual è il concetto principale riguardo "${topic}"?`, | |
| options: [ | |
| { text: "Concetto A (Vedi Video)", correct: true }, | |
| { text: "Concetto B", correct: false }, | |
| { text: "Concetto C", correct: false } | |
| ], | |
| hint: "Guarda i primi 2 minuti del video per la risposta." | |
| } | |
| }; | |
| }; | |
| const getSimulatedResult = (topic) => { | |
| try { | |
| const video = getTopicVideo(topic); | |
| return { | |
| videoId: video.id, | |
| summary: video.summary || `Riassunto generato per ${topic}.`, | |
| notes: [ | |
| `Concetti chiave su: ${topic || 'Argomento'}`, | |
| `Analisi approfondita di: ${video.title}`, | |
| 'Sintesi dei punti fondamentali', | |
| 'Collegamenti interdisciplinari rilevati' | |
| ], | |
| planner: [ | |
| { time: '15:00', task: `Studio Intensivo: ${topic || 'Concetti Base'}`, details: 'Lettura e sottolineatura testo', duration: '45m' }, | |
| { time: '15:45', task: 'Active Recall & Quiz', details: 'Test di autovalutazione', duration: '15m' }, | |
| { time: '16:00', task: 'Analisi Video & Sintesi', details: 'Visione materiale multimediale', duration: '30m' }, | |
| ], | |
| quiz: Array.isArray(video.quiz) ? video.quiz : [video.quiz] // Normalize to array | |
| }; | |
| } catch (e) { | |
| console.error("Error generating simulated result:", e); | |
| return { | |
| videoId: 'Y9EjnBmO2Jw', // Fallback sicuro (Fisica) | |
| summary: "Errore durante il recupero dei dati. Riprova.", | |
| notes: ['Errore generazione note', 'Riprova più tardi'], | |
| planner: [], | |
| quiz: [{ question: 'Errore di sistema', options: [], hint: 'Riprova' }] | |
| }; | |
| } | |
| }; | |
| // ============================================ | |
| // CORE SIMULATOR (CUORE DEL SITO) | |
| // ============================================ | |
| const CoreSimulator = () => { | |
| const { protocol, credits, setCredits, setShowPayment, addXP } = useUser(); | |
| const [state, setState] = useState('INPUT'); | |
| const [query, setQuery] = useState(''); | |
| const [progress, setProgress] = useState(0); | |
| const [logs, setLogs] = useState([]); | |
| const [resultData, setResultData] = useState(getSimulatedResult('Introduzione')); | |
| const [isListening, setIsListening] = useState(false); | |
| const [quizAnswer, setQuizAnswer] = useState(null); | |
| const [showXP, setShowXP] = useState(false); | |
| const [apiError, setApiError] = useState(false); | |
| const startListening = () => { | |
| if ('webkitSpeechRecognition' in window || 'SpeechRecognition' in window) { | |
| const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; | |
| const recognition = new SpeechRecognition(); | |
| recognition.lang = 'it-IT'; | |
| recognition.onstart = () => setIsListening(true); | |
| recognition.onend = () => setIsListening(false); | |
| recognition.onresult = (event) => { const transcript = event.results[0][0].transcript; setQuery(transcript); setTimeout(() => handleSubmit(null, transcript), 500); }; | |
| recognition.start(); | |
| } else { alert("Riconoscimento vocale non supportato in questo browser."); } | |
| }; | |
| const handleSubmit = async (e, voiceQuery = null) => { | |
| e?.preventDefault(); | |
| if (protocol === 'GUEST') { document.getElementById('pricing')?.scrollIntoView({ behavior: 'smooth' }); return; } | |
| if (protocol === 'CORE' && credits <= 0) { setShowPayment(true); return; } | |
| if (protocol === 'CORE') setCredits(p => p - 1); | |
| const effectiveQuery = voiceQuery || query; | |
| if (voiceQuery) setQuery(voiceQuery); // Ensure state is synced | |
| setState('PROCESSING'); | |
| setProgress(0); | |
| setLogs([]); | |
| setApiError(false); | |
| }; | |
| // AI SIMULATION EFFECT | |
| useEffect(() => { | |
| if (state !== 'PROCESSING') return; | |
| const searchQuery = query; | |
| let isMounted = true; | |
| // Fake Progress Logs | |
| const logMessages = ['[INFO] Scansione 40.000 Fonti Accademiche...', '[INFO] Filtraggio Contenuti a Bassa Densità...', '[INFO] Generazione Mappe Neurali...']; | |
| let logIndex = 0; | |
| const logInt = setInterval(() => { | |
| if (logIndex < logMessages.length) { | |
| setLogs(p => [...p, logMessages[logIndex]]); | |
| logIndex++; | |
| } | |
| // Random system logs | |
| if (Math.random() > 0.7) { | |
| setLogs(prev => [...prev.slice(-4), ` [SYSTEM] ${['Analyzing patterns...', 'Decoding vector space...', 'Retrieving archives...', 'Synthesizing layout...'][Math.floor(Math.random() * 4)]}`]); | |
| } | |
| }, 600); | |
| const progInt = setInterval(() => { | |
| setProgress(p => { | |
| if (p >= 90) return p; | |
| return p + Math.floor(Math.random() * 5); | |
| }); | |
| }, 200); | |
| const runSimulation = async () => { | |
| // 1. Check Static Database First (Fast Cache) but allow AI override if key is present | |
| // Actually, we want AI to take over unless it fails. | |
| // But for specific static IDs (Fisica/Dante), users might prefer the "Perfect" curated content? | |
| // Let's Check: User said "Use Gemini for EVERY search". So we prefer AI. | |
| // But we can fallback to static if AI fails or for speed? | |
| // Let's try AI first. | |
| try { | |
| if (!SYSTEM_CONFIG.OPENROUTER_KEY) throw new Error("No API Key"); | |
| const response = await fetch("https://openrouter.ai/api/v1/chat/completions", { | |
| method: "POST", | |
| headers: { | |
| "Authorization": `Bearer ${SYSTEM_CONFIG.OPENROUTER_KEY}`, | |
| "Content-Type": "application/json", | |
| "HTTP-Referer": "http://localhost:5176", | |
| "X-Title": "Synapse OS" | |
| }, | |
| body: JSON.stringify({ | |
| "model": "google/gemini-2.0-flash-001", | |
| "messages": [ | |
| { | |
| "role": "system", | |
| "content": "You are Synapse OS, an advanced education interface. Analyze the user topic and return a STRICT JSON object (no markdown). Structure: { \"summary\": \"Comprehensive summary (8-10 sentences).\", \"notes\": [\"Point 1\", \"Point 2\", \"Point 3\"], \"videoSearchQuery\": \"${topic} documentario scuola\", \"quiz\": [ { \"question\": \"Q1?\", \"options\": [{\"text\":\"A\",\"correct\":false}, {\"text\":\"B\",\"correct\":true}, {\"text\":\"C\",\"correct\":false}], \"hint\": \"H1\" }, { \"question\": \"Q2?\", \"options\": [...], \"hint\": \"...\" }, { \"question\": \"Q3?\", \"options\": [...], \"hint\": \"...\" }, { \"question\": \"Q4?\", \"options\": [...], \"hint\": \"...\" }, { \"question\": \"Q5?\", \"options\": [...], \"hint\": \"...\" } ], \"planner\": [{\"time\":\"15:00\",\"task\":\"Deep Work\",\"details\":\"Study concept X and Y\",\"duration\":\"45m\"},{\"time\":\"16:00\",\"task\":\"Review\",\"details\":\"Test knowledge\",\"duration\":\"30m\"}] }. Notes: Quiz MUST have exactly 5 questions. Each question MUST have 3 options. Language: ITALIAN." | |
| }, | |
| { | |
| "role": "user", | |
| "content": `Topic: ${searchQuery}` | |
| } | |
| ] | |
| }) | |
| }); | |
| const data = await response.json(); | |
| if (!data.choices) throw new Error("Invalid API Response"); | |
| // Clean potential markdown code blocks ```json ... ``` | |
| let cleanContent = data.choices[0].message.content; | |
| if (cleanContent.startsWith('```')) { | |
| cleanContent = cleanContent.replace(/^```json\s*/, '').replace(/^```\s*/, '').replace(/\s*```$/, ''); | |
| } | |
| const aiContent = JSON.parse(cleanContent); | |
| let finalVideoId = `?listType=search&list=${encodeURIComponent(aiContent.videoSearchQuery || searchQuery)}`; | |
| // Try YouTube API if Key is present | |
| if (SYSTEM_CONFIG.YOUTUBE_KEY) { | |
| try { | |
| const ytRes = await fetch(`https://www.googleapis.com/youtube/v3/search?part=snippet&maxResults=1&q=${encodeURIComponent(aiContent.videoSearchQuery || searchQuery)}&type=video&key=${SYSTEM_CONFIG.YOUTUBE_KEY}`); | |
| const ytData = await ytRes.json(); | |
| if (ytData.items && ytData.items.length > 0) { | |
| finalVideoId = ytData.items[0].id.videoId; | |
| } | |
| } catch (ytErr) { | |
| console.error("YouTube API Failed:", ytErr); | |
| } | |
| } | |
| if (isMounted) { | |
| setResultData({ | |
| videoId: finalVideoId, | |
| summary: aiContent.summary, | |
| notes: aiContent.notes, | |
| planner: aiContent.planner, | |
| quiz: Array.isArray(aiContent.quiz) ? aiContent.quiz : [aiContent.quiz] | |
| }); | |
| } | |
| } catch (e) { | |
| console.error("AI GENERATION FAILED:", e); | |
| if (isMounted) { | |
| setLogs(prev => [...prev, " [ERROR] NEURAL LINK SEVERED", " [WARN] SWITCHING TO LOCAL CACHE..."]); | |
| // Fallback to static | |
| setResultData(getSimulatedResult(searchQuery)); | |
| } | |
| } finally { | |
| if (isMounted) { | |
| setProgress(100); | |
| setTimeout(() => { | |
| setLogs(prev => [...prev, " [SUCCESS] DATA STREAM ESTABLISHED"]); | |
| setTimeout(() => setState('DASHBOARD'), 500); | |
| }, 500); | |
| } | |
| } | |
| }; | |
| runSimulation(); | |
| return () => { | |
| isMounted = false; | |
| clearInterval(logInt); | |
| clearInterval(progInt); | |
| }; | |
| }, [state, query]); | |
| const [quizIndex, setQuizIndex] = useState(0); | |
| const handleQuizAnswer = (isCorrect) => { | |
| setQuizAnswer(isCorrect ? 'correct' : 'wrong'); | |
| if (isCorrect) { | |
| setShowXP(true); | |
| addXP(200); // Add real XP | |
| setTimeout(() => setShowXP(false), 2000); | |
| // Next question delay | |
| setTimeout(() => { | |
| if (resultData?.quiz && quizIndex < resultData.quiz.length - 1) { | |
| setQuizIndex(p => p + 1); | |
| setQuizAnswer(null); | |
| } | |
| }, 1500); | |
| } | |
| }; | |
| return ( | |
| <section id="simulator" className="py-20 px-6 relative z-10"> | |
| <div className="max-w-6xl mx-auto"> | |
| <div className="text-center mb-12"> | |
| <h2 className="text-4xl md:text-5xl font-black text-white mb-4">CORE <span className="text-[#06b6d4]">SIMULATOR</span></h2> | |
| <p className="text-slate-400 font-mono text-sm">INTERFACCIA ELABORAZIONE NEURALE v2.0</p> | |
| {protocol === 'CORE' && <div className="mt-4 text-xs font-mono text-[#f43f5e]">CREDITI GIORNALIERI: {credits}</div>} | |
| </div> | |
| {state === 'INPUT' && ( | |
| <form onSubmit={handleSubmit} className="max-w-2xl mx-auto relative"> | |
| <HUDCard className="p-2 rounded-xl"> | |
| <div className="flex items-center gap-3"> | |
| <Search className="w-5 h-5 text-slate-500 ml-4" /> | |
| <input type="text" value={query} onChange={e => setQuery(e.target.value)} | |
| placeholder={protocol === 'GUEST' ? "Seleziona un Protocollo..." : "Es. Storia..."} | |
| className="flex-1 px-4 py-4 bg-transparent text-white placeholder:text-slate-500 focus:outline-none font-mono" disabled={protocol === 'GUEST'} /> | |
| <button type="button" onClick={startListening} disabled={protocol === 'GUEST'} | |
| className={`p-3 rounded-lg transition-all ${isListening ? 'bg-[#f43f5e] text-white animate-pulse' : 'bg-slate-800 text-slate-400 hover:text-white hover:bg-slate-700'}`}> | |
| {isListening ? <MicOff className="w-5 h-5" /> : <Mic className="w-5 h-5" />} | |
| </button> | |
| <button type="submit" className={`px-6 py-3 font-bold rounded-lg transition-all mr-2 ${protocol === 'GUEST' ? 'bg-slate-700 text-slate-400' : 'bg-[#06b6d4] text-black hover:bg-[#22d3ee]'}`}> | |
| {protocol === 'GUEST' ? <Lock className="w-5 h-5" /> : <Zap className="w-5 h-5" />} | |
| </button> | |
| </div> | |
| </HUDCard> | |
| {protocol === 'GUEST' && <div className="absolute top-full left-0 right-0 text-center mt-4 text-[#f43f5e] font-mono text-xs animate-pulse">SELEZIONE PROTOCOLLO RICHIESTA</div>} | |
| </form> | |
| )} | |
| {state === 'PROCESSING' && ( | |
| <HUDCard className="max-w-2xl mx-auto p-8 rounded-2xl"> | |
| <div className="flex items-center gap-3 mb-6"><Cpu className="w-6 h-6 text-[#8b5cf6] animate-pulse" /><span className="text-white font-semibold">ELABORAZIONE NEURALE...</span><span className="font-mono text-[#06b6d4]">{progress}%</span></div> | |
| <div className="h-2 bg-slate-800 rounded-full overflow-hidden mb-6"><div className="h-full bg-gradient-to-r from-[#8b5cf6] to-[#06b6d4] transition-all" style={{ width: `${progress}%` }} /></div> | |
| <div className="font-mono text-xs space-y-1">{logs.map((l, i) => <div key={i} className={(l && typeof l === 'string' && l.includes('ERROR')) ? 'text-[#f43f5e]' : 'text-slate-400'}>{l}</div>)}</div> | |
| </HUDCard> | |
| )} | |
| {state === 'DASHBOARD' && resultData && ( | |
| <div className="space-y-6"> | |
| <div className="text-center mb-8"><button onClick={() => { setState('INPUT'); setQuizAnswer(null); }} className="font-mono text-xs text-slate-500 hover:text-[#06b6d4]">[RESET SIMULAZIONE]</button></div> | |
| <div className="grid md:grid-cols-4 gap-6"> | |
| {/* Planner */} | |
| <HUDCard className="p-6 rounded-2xl"> | |
| <div className="flex items-center gap-2 mb-6"><Clock className="w-5 h-5 text-[#8b5cf6]" /><h3 className="text-white font-bold">PLANNER</h3></div> | |
| <div className="pl-6 border-l-2 border-[#8b5cf6]/30 space-y-6">{resultData.planner?.map((x, i) => ( | |
| <div key={i} className="relative"> | |
| <div className="absolute -left-[31px] top-0 w-4 h-4 rounded-full bg-[#8b5cf6] border-4 border-[#020617]" /> | |
| <div className="font-mono text-xs text-[#06b6d4] mb-1">{x.time}</div> | |
| <div className="text-white text-sm font-bold">{x.task}</div> | |
| {x.details && <div className="text-slate-400 text-xs mt-1 italic">"{x.details}"</div>} | |
| <div className="text-slate-500 text-xs mt-1">{x.duration}</div> | |
| </div> | |
| ))}</div> | |
| </HUDCard> | |
| {/* Summary Card (NEW) */} | |
| <HUDCard className="p-6 rounded-2xl"> | |
| <div className="flex items-center gap-2 mb-6"><BookOpen className="w-5 h-5 text-[#f43f5e]" /><h3 className="text-white font-bold">RIASSUNTO</h3></div> | |
| <div className="text-sm text-slate-300 leading-relaxed font-light"> | |
| {resultData.summary} | |
| </div> | |
| <div className="mt-4 pt-4 border-t border-white/5"> | |
| <div className="text-xs text-[#f43f5e] font-mono">INTEL. ARTIFICIALE: 98% PRECISIONE</div> | |
| </div> | |
| </HUDCard> | |
| {/* Video Stream */} | |
| <HUDCard className="p-6 rounded-2xl"> | |
| <div className="flex items-center gap-2 mb-6"><Video className="w-5 h-5 text-[#06b6d4]" /><h3 className="text-white font-bold">VIDEO STREAM</h3></div> | |
| <div className="aspect-video bg-black rounded-xl overflow-hidden mb-4"> | |
| <iframe | |
| src={resultData.videoId?.startsWith('?') | |
| ? `https://www.youtube.com/embed?${resultData.videoId.substring(1)}&origin=${window.location.origin}` | |
| : `https://www.youtube.com/embed/${resultData.videoId || 'Y9EjnBmO2Jw'}?origin=${window.location.origin}`} | |
| className="w-full h-full" | |
| title="Video Educativo" | |
| allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" | |
| allowFullScreen | |
| /> | |
| </div> | |
| <div className="text-xs font-mono text-slate-400 mb-2">BIGNAMI NOTES:</div> | |
| <ul className="space-y-1">{resultData.notes?.map((n, i) => <li key={i} className="text-slate-300 text-sm flex gap-2"><span className="text-[#8b5cf6]">•</span>{n}</li>)}</ul> | |
| </HUDCard> | |
| {/* Active Recall (Multi-Step) */} | |
| <HUDCard className="p-6 rounded-2xl relative"> | |
| <div className="flex items-center gap-2 mb-6"><Brain className="w-5 h-5 text-[#f43f5e]" /><h3 className="text-white font-bold">ACTIVE RECALL ({quizIndex + 1}/{resultData.quiz?.length || 1})</h3></div> | |
| {showXP && <div className="absolute top-4 right-4 text-[#f43f5e] font-black animate-bounce">+200 XP</div>} | |
| {resultData.quiz && resultData.quiz[quizIndex] ? ( | |
| <> | |
| <div className="mb-6 text-sm font-medium">{resultData.quiz[quizIndex]?.question || "Caricamento..."}</div> | |
| <div className="space-y-3"> | |
| {resultData.quiz[quizIndex]?.options?.map((opt, i) => ( | |
| <button key={i} onClick={() => handleQuizAnswer(opt.correct)} | |
| className={`w-full p-4 rounded-xl border text-left transition-all ${quizAnswer === null | |
| ? 'border-slate-700 hover:border-[#f43f5e] hover:bg-[#f43f5e]/10' | |
| : opt.correct | |
| ? 'border-green-500 bg-green-500/20 text-green-200' | |
| : 'border-slate-800 opacity-50' | |
| }`}> | |
| {opt.text} | |
| </button> | |
| ))} | |
| </div> | |
| {quizAnswer === 'wrong' && <div className="mt-4 text-xs text-red-400">Riprova, concentrati sul video...</div>} | |
| <div className="mt-6 pt-4 border-t border-slate-800 text-xs text-slate-500 italic"> | |
| HINT: {resultData.quiz[quizIndex].hint} | |
| </div> | |
| </> | |
| ) : ( | |
| <div className="text-slate-500">Nessun quiz disponibile.</div> | |
| )} | |
| </HUDCard> | |
| </div> | |
| </div> | |
| ) | |
| } | |
| </div > | |
| </section > | |
| ); | |
| }; | |
| // ============================================ | |
| // FEATURE MODULES (Holographic Cards) | |
| // ============================================ | |
| const FeatureModules = () => { | |
| const [tilt, setTilt] = useState({ x: 0, y: 0, id: null }); | |
| const handleMouseMove = (e, id) => { const rect = e.currentTarget.getBoundingClientRect(); setTilt({ x: (e.clientX - rect.left - rect.width / 2) / 10, y: -(e.clientY - rect.top - rect.height / 2) / 10, id }); }; | |
| const features = [ | |
| { id: 1, icon: Clock, title: 'TIME DILATION', subtitle: 'Planner', description: 'Algoritmo che adatta lo studio al tuo ritmo circadiano.', color: '#8b5cf6' }, | |
| { id: 2, icon: Filter, title: 'SIGNAL FILTER', subtitle: 'Video', description: 'Elimina rumore, intro e sponsor dai video educativi.', color: '#06b6d4' }, | |
| { id: 3, icon: Brain, title: 'MEMORY LOCK', subtitle: 'Quiz', description: 'Interrogazioni adattive che rinforzano le sinapsi deboli.', color: '#8b5cf6' }, | |
| ]; | |
| return ( | |
| <section className="py-20 px-6 relative" style={{ zIndex: 1 }}> | |
| <div className="max-w-6xl mx-auto"> | |
| <div className="text-center mb-16"><h2 className="text-3xl md:text-5xl font-black text-white mb-4">MODULI <span className="text-[#8b5cf6]">FEATURE</span></h2><p className="text-slate-400 font-mono text-sm uppercase">Componenti Interfaccia Olografica</p></div> | |
| <div className="grid md:grid-cols-3 gap-8" style={{ perspective: '1000px' }}> | |
| {features.map((f) => ( | |
| <div key={f.id} onMouseMove={(e) => handleMouseMove(e, f.id)} onMouseLeave={() => setTilt({ x: 0, y: 0, id: null })} | |
| style={{ transform: tilt.id === f.id ? `rotateX(${tilt.y}deg) rotateY(${tilt.x}deg) scale(1.02)` : 'rotateX(0) rotateY(0) scale(1)', transformStyle: 'preserve-3d', transition: 'transform 0.1s ease-out' }}> | |
| <HUDCard className="p-8 rounded-2xl cursor-pointer h-full"> | |
| <div className="w-16 h-16 rounded-2xl flex items-center justify-center mb-6" style={{ background: `${f.color}20`, boxShadow: `0 0 30px ${f.color}40` }}> | |
| <f.icon className="w-8 h-8" style={{ color: f.color }} strokeWidth={1.5} /> | |
| </div> | |
| <div className="font-mono text-xs text-slate-500 uppercase mb-1">{f.subtitle}</div> | |
| <h3 className="text-xl font-bold text-white mb-3">{f.title}</h3> | |
| <p className="text-slate-400 text-sm leading-relaxed">{f.description}</p> | |
| </HUDCard> | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| </section> | |
| ); | |
| }; | |
| // ============================================ | |
| // PARENTS DASHBOARD | |
| // ============================================ | |
| const ParentsDashboard = () => { | |
| const { protocol, userData } = useUser(); | |
| const locked = protocol !== 'PRO'; | |
| return ( | |
| <HUDCard className="p-8 rounded-2xl" locked={locked}> | |
| <div className="flex items-center gap-3 mb-6"><BarChart2 className="w-6 h-6 text-[#06b6d4]" /><h3 className="text-white font-bold">DASHBOARD GENITORI {locked && '[BLOCCATA]'}</h3></div> | |
| {!locked ? ( | |
| <div className="grid md:grid-cols-2 gap-8"> | |
| <div className="space-y-6"> | |
| <div className="bg-slate-800/50 p-4 rounded-xl border border-white/5"> | |
| <div className="text-slate-400 text-xs font-mono mb-1">ATTIVITÀ SETTIMANALE</div> | |
| <div className="text-3xl font-bold text-white">{userData.hours || 0} <span className="text-sm font-normal text-slate-500">ore</span></div> | |
| <div className="flex items-center gap-1 text-[#10b981] text-xs mt-2"><TrendingUp className="w-3 h-3" /> +{(userData.hours * 0.5).toFixed(1)}% vs settimana scorsa</div> | |
| </div> | |
| <div className="bg-slate-800/50 p-4 rounded-xl border border-white/5"> | |
| <div className="text-slate-400 text-xs font-mono mb-1">PUNTEGGIO FOCUS</div> | |
| <div className="text-3xl font-bold text-white">{Math.min(10, (userData.xp / 500)).toFixed(1)} <span className="text-sm font-normal text-slate-500">/ 10</span></div> | |
| <div className="w-full h-1 bg-slate-700 rounded-full mt-3 overflow-hidden"><div className="h-full bg-[#06b6d4]" style={{ width: `${Math.min(100, (userData.xp / 500) * 10)}%` }} /></div> | |
| </div> | |
| <div className="bg-slate-800/50 p-4 rounded-xl border border-white/5"> | |
| <div className="text-slate-400 text-xs font-mono mb-2">REPORT EMAIL</div> | |
| <div className="text-sm text-slate-300 italic">"Tuo figlio ha completato {userData.hours || 0} ore di studio. Voto attuale: {Math.min(10, (userData.xp / 500)).toFixed(1)}"</div> | |
| </div> | |
| </div> | |
| <div> | |
| <div className="text-slate-400 text-xs font-mono mb-4">RIPARTIZIONE MATERIE</div> | |
| <div className="space-y-3">{[{ l: 'Storia', v: 75, c: '#8b5cf6' }, { l: 'Scienze', v: 45, c: '#06b6d4' }, { l: 'Matematica', v: 60, c: '#10b981' }].map((item, i) => ( | |
| <div key={i}><div className="flex justify-between text-xs text-slate-300 mb-1"><span>{item.l}</span><span>{item.v}%</span></div><div className="w-full h-1 bg-slate-700 rounded-full overflow-hidden"><div className="h-full" style={{ width: `${item.v}%`, background: item.c }} /></div></div> | |
| ))}</div> | |
| </div> | |
| </div> | |
| ) : ( | |
| <div className="py-12 bg-black/50 rounded-xl border border-white/5 blur-sm select-none"><div className="flex items-center justify-center h-full text-slate-600 font-mono">VISUALIZZAZIONE DATI CRITTOGRAFATA</div></div> | |
| )} | |
| </HUDCard> | |
| ); | |
| }; | |
| // ============================================ | |
| // PRICING SECTION | |
| // ============================================ | |
| const PricingSection = () => { | |
| const { upgradeToCore, setShowPayment, protocol } = useUser(); | |
| const coreFeatures = ['Neural Engine Base', '5 Ricerche al Giorno', 'Accesso Community', 'Video Streaming Base']; | |
| const proFeatures = [ | |
| 'Ricerche Illimitate', 'Dashboard Genitori Completa', 'Elaborazione Prioritaria', 'Input Vocale Avanzato', | |
| 'Memory Lock Pro', 'Time Dilation Algorithm', 'Signal Filter HD', 'Report Email Settimanali', | |
| 'Supporto Prioritario 24/7', 'Nessuna Pubblicità' | |
| ]; | |
| return ( | |
| <section id="pricing" className="py-20 px-6 relative z-10"> | |
| <div className="max-w-5xl mx-auto"> | |
| <div className="text-center mb-16"><h2 className="text-4xl font-black text-white mb-4">SCEGLI <span className="text-[#06b6d4]">PROTOCOLLO</span></h2></div> | |
| <div className="grid md:grid-cols-2 gap-8 mb-16"> | |
| {/* Student Core */} | |
| <HUDCard className={`p-8 rounded-2xl ${protocol === 'CORE' ? 'border-[#10b981]' : ''}`}> | |
| {protocol === 'CORE' && <div className="absolute -top-3 left-1/2 -translate-x-1/2 px-4 py-1 bg-[#10b981] text-white text-xs rounded-full font-bold">ATTIVO</div>} | |
| <h3 className="text-2xl font-bold text-white mb-2">STUDENT CORE</h3> | |
| <div className="text-4xl font-black text-slate-400 mb-6">€0</div> | |
| <ul className="space-y-3 mb-8 text-sm text-slate-400">{coreFeatures.map((x, i) => <li key={i} className="flex gap-2"><Check className="w-4 h-4 text-slate-500" />{x}</li>)}</ul> | |
| <MagneticButton onClick={upgradeToCore} disabled={protocol !== 'GUEST'} | |
| className="w-full py-3 border border-slate-600 text-slate-400 font-bold rounded-xl hover:text-white hover:border-white disabled:opacity-50"> | |
| {protocol === 'GUEST' ? 'ATTIVA GRATIS' : 'GIÀ ATTIVO'} | |
| </MagneticButton> | |
| </HUDCard> | |
| {/* Scholar Pro */} | |
| <HUDCard className={`p-8 rounded-2xl relative ${protocol === 'PRO' ? 'border-[#10b981]' : 'border-[#8b5cf6]'} shadow-[0_0_30px_rgba(139,92,246,0.15)]`}> | |
| <div className="absolute -top-3 left-1/2 -translate-x-1/2 px-4 py-1 bg-[#8b5cf6] text-white text-xs rounded-full font-bold"> | |
| {protocol === 'PRO' ? 'ATTIVO' : 'CONSIGLIATO'} | |
| </div> | |
| <h3 className="text-2xl font-bold text-white mb-2">SCHOLAR PRO</h3> | |
| <div className="text-4xl font-black text-white mb-6">€9.99<span className="text-lg font-normal text-slate-400">/mese</span></div> | |
| <ul className="space-y-2 mb-8 text-sm text-white max-h-48 overflow-y-auto pr-2">{proFeatures.map((x, i) => <li key={i} className="flex gap-2"><Check className="w-4 h-4 text-[#8b5cf6] shrink-0" />{x}</li>)}</ul> | |
| <MagneticButton onClick={() => setShowPayment(true)} disabled={protocol === 'PRO'} | |
| className="w-full py-3 bg-[#8b5cf6] text-white font-bold rounded-xl hover:bg-[#7c3aed] disabled:opacity-50"> | |
| {protocol === 'PRO' ? 'GIÀ PRO' : 'UPGRADE ORA'} | |
| </MagneticButton> | |
| </HUDCard> | |
| </div> | |
| <ParentsDashboard /> | |
| </div> | |
| </section> | |
| ); | |
| }; | |
| // ============================================ | |
| // PRO DASHBOARD (Interfaccia Dedicata Pro) | |
| // ============================================ | |
| const ProDashboard = () => { | |
| const { userData, setCurrentView } = useUser(); | |
| const [activeTab, setActiveTab] = useState('overview'); | |
| const [isListening, setIsListening] = useState(false); | |
| const [voiceQuery, setVoiceQuery] = useState(''); | |
| const [searchHistory, setSearchHistory] = useState([]); | |
| const [memoryCards, setMemoryCards] = useState([]); | |
| const [flippedCard, setFlippedCard] = useState(null); | |
| const [cardTopic, setCardTopic] = useState(''); | |
| const [isGeneratingCards, setIsGeneratingCards] = useState(false); | |
| const generateFlashcards = async () => { | |
| if (!cardTopic.trim()) return; | |
| setIsGeneratingCards(true); | |
| try { | |
| if (!SYSTEM_CONFIG.OPENROUTER_KEY) throw new Error("No Key"); | |
| const response = await fetch("https://openrouter.ai/api/v1/chat/completions", { | |
| method: "POST", | |
| headers: { | |
| "Authorization": `Bearer ${SYSTEM_CONFIG.OPENROUTER_KEY}`, | |
| "Content-Type": "application/json", | |
| "HTTP-Referer": "http://localhost:5173", | |
| "X-Title": "Synapse OS" | |
| }, | |
| body: JSON.stringify({ | |
| "model": "google/gemini-2.0-flash-001", | |
| "messages": [ | |
| { | |
| "role": "system", | |
| "content": "You are Synapse OS. Analyze the topic and return a STRICT JSON array of 3 objects: [{ \"id\": 1, \"front\": \"Question?\", \"back\": \"Answer\", \"mastery\": 0 }]. Limit answers to 5 words max. Language: ITALIAN." | |
| }, | |
| { role: 'user', content: `Topic: ${cardTopic}` } | |
| ] | |
| }) | |
| }); | |
| const data = await response.json(); | |
| let cleanContent = data.choices[0].message.content.replace(/^```json\s*/, '').replace(/^```\s*/, '').replace(/\s*```$/, ''); | |
| const newCards = JSON.parse(cleanContent); | |
| // Add random mastery for simulation | |
| const cardsWithMastery = newCards.map((c, i) => ({ ...c, id: Date.now() + i, mastery: Math.floor(Math.random() * 40) })); | |
| setMemoryCards(cardsWithMastery); | |
| setCardTopic(''); | |
| } catch (e) { | |
| console.error("Card Gen Failed", e); | |
| } finally { | |
| setIsGeneratingCards(false); | |
| } | |
| }; | |
| const [plannerEvents, setPlannerEvents] = useState([]); | |
| const [videoTopic, setVideoTopic] = useState(''); | |
| const [videoSearching, setVideoSearching] = useState(false); | |
| const [currentVideo, setCurrentVideo] = useState({ id: SYSTEM_CONFIG.FALLBACK_VIDEO_ID, title: 'La Rivoluzione Francese - Documentario Completo', topic: 'Rivoluzione Francese' }); | |
| const [videoQueue, setVideoQueue] = useState([ | |
| { id: 'dQw4w9WgXcQ', title: 'Fisica Quantistica Semplificata', duration: '15:00' }, | |
| { id: 'dQw4w9WgXcQ', title: 'Fisica Quantistica Semplificata', duration: '15:00' }, | |
| { id: 'jNQXAC9IVRw', title: 'Storia del Rinascimento', duration: '18:00' }, | |
| ]); | |
| // Support Chat State | |
| const [chatMessages, setChatMessages] = useState([ | |
| { role: 'system', content: 'Synapse Support Agent Online. Come posso aiutarti oggi?' } | |
| ]); | |
| const [chatInput, setChatInput] = useState(''); | |
| const [isTyping, setIsTyping] = useState(false); | |
| const handleSupportMessage = async () => { | |
| if (!chatInput.trim()) return; | |
| const userMsg = chatInput; | |
| setChatMessages(p => [...p, { role: 'user', content: userMsg }]); | |
| setChatInput(''); | |
| setIsTyping(true); | |
| try { | |
| if (!SYSTEM_CONFIG.OPENROUTER_KEY) throw new Error("No Key"); | |
| const response = await fetch("https://openrouter.ai/api/v1/chat/completions", { | |
| method: "POST", | |
| headers: { | |
| "Authorization": `Bearer ${SYSTEM_CONFIG.OPENROUTER_KEY}`, | |
| "Content-Type": "application/json", | |
| "HTTP-Referer": "http://localhost:5173", | |
| "X-Title": "Synapse OS" | |
| }, | |
| body: JSON.stringify({ | |
| "model": "google/gemini-2.0-flash-001", | |
| "messages": [ | |
| { | |
| "role": "system", | |
| "content": "You are Synapse Support, a helpful AI assistant for the Synapse OS platform. Answer questions about the app (Time Dilation, Signal Filters, Memory Lock), subscription (Pro is 9.99/mo), or general technical support. Be concise, professional, and use a futuristic tone. Language: ITALIAN." | |
| }, | |
| ...chatMessages.map(m => ({ role: m.role === 'system' ? 'assistant' : m.role, content: m.content })), | |
| { role: 'user', content: userMsg } | |
| ] | |
| }) | |
| }); | |
| const data = await response.json(); | |
| if (!data.choices) throw new Error("AI Error"); | |
| setChatMessages(p => [...p, { role: 'system', content: data.choices[0].message.content }]); | |
| } catch (e) { | |
| setChatMessages(p => [...p, { role: 'system', content: "ERRORE CONNESSIONE: Impossibile contattare il supporto centrale." }]); | |
| } finally { | |
| setIsTyping(false); | |
| } | |
| }; | |
| const searchVideo = async () => { | |
| if (!videoTopic.trim()) return; | |
| setVideoSearching(true); | |
| // Fallback function | |
| const useFallback = () => { | |
| const foundVideo = getTopicVideo(videoTopic); | |
| setCurrentVideo({ ...foundVideo, topic: videoTopic }); | |
| }; | |
| try { | |
| // 1. Ask Gemini for the best search query | |
| if (!SYSTEM_CONFIG.OPENROUTER_KEY) throw new Error("No AI Key"); | |
| const response = await fetch("https://openrouter.ai/api/v1/chat/completions", { | |
| method: "POST", | |
| headers: { | |
| "Authorization": `Bearer ${SYSTEM_CONFIG.OPENROUTER_KEY}`, | |
| "Content-Type": "application/json", | |
| "HTTP-Referer": "http://localhost:5173", | |
| "X-Title": "Synapse OS" | |
| }, | |
| body: JSON.stringify({ | |
| "model": "google/gemini-2.0-flash-001", | |
| "messages": [ | |
| { | |
| "role": "system", | |
| "content": "You are Synapse OS. Analyze the topic and return a STRICT JSON: { \"videoSearchQuery\": \"optimized youtube search query for topic\", \"title\": \"Formal Title of the topic\" }. Language: ITALIAN." | |
| }, | |
| { "role": "user", "content": `Topic: ${videoTopic}` } | |
| ] | |
| }) | |
| }); | |
| const data = await response.json(); | |
| if (!data.choices) throw new Error("AI Error"); | |
| let cleanContent = data.choices[0].message.content.replace(/^```json\s*/, '').replace(/^```\s*/, '').replace(/\s*```$/, ''); | |
| const aiContent = JSON.parse(cleanContent); | |
| let finalVideoId = `?listType=search&list=${encodeURIComponent(aiContent.videoSearchQuery)}`; | |
| let finalTitle = aiContent.title; | |
| // 2. Fetch from YouTube API if available | |
| if (SYSTEM_CONFIG.YOUTUBE_KEY) { | |
| try { | |
| const ytRes = await fetch(`https://www.googleapis.com/youtube/v3/search?part=snippet&maxResults=1&q=${encodeURIComponent(aiContent.videoSearchQuery)}&type=video&key=${SYSTEM_CONFIG.YOUTUBE_KEY}`); | |
| const ytData = await ytRes.json(); | |
| if (ytData.items && ytData.items.length > 0) { | |
| finalVideoId = ytData.items[0].id.videoId; | |
| finalTitle = ytData.items[0].snippet.title; | |
| } | |
| } catch (e) { console.error("YT API Error", e); } | |
| } | |
| setCurrentVideo({ | |
| id: finalVideoId, | |
| title: finalTitle, | |
| topic: videoTopic | |
| }); | |
| } catch (error) { | |
| console.error("Search failed, using fallback", error); | |
| useFallback(); | |
| } finally { | |
| setVideoSearching(false); | |
| setVideoTopic(''); | |
| } | |
| }; | |
| const startVoiceCommand = () => { | |
| if ('webkitSpeechRecognition' in window || 'SpeechRecognition' in window) { | |
| const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; | |
| const recognition = new SpeechRecognition(); | |
| recognition.lang = 'it-IT'; | |
| recognition.continuous = true; | |
| recognition.onstart = () => setIsListening(true); | |
| recognition.onend = () => setIsListening(false); | |
| recognition.onresult = (event) => { | |
| const transcript = event.results[event.results.length - 1][0].transcript; | |
| setVoiceQuery(transcript); | |
| }; | |
| recognition.start(); | |
| } | |
| }; | |
| const tabs = [ | |
| { id: 'overview', label: 'Panoramica', icon: BarChart2 }, | |
| { id: 'planner', label: 'Time Dilation', icon: Clock }, | |
| { id: 'video', label: 'Signal Filter', icon: Video }, | |
| { id: 'memory', label: 'Memory Lock', icon: Brain }, | |
| { id: 'voice', label: 'Voice Commander', icon: Mic }, | |
| { id: 'reports', label: 'Report', icon: Mail }, | |
| { id: 'support', label: 'Supporto 24/7', icon: HelpCircle }, | |
| ]; | |
| return ( | |
| <div className="min-h-screen pt-32 pb-20 px-6 relative z-10"> | |
| <div className="max-w-7xl mx-auto"> | |
| {/* Header */} | |
| <div className="flex flex-col md:flex-row md:items-center justify-between gap-4 mb-8"> | |
| <div> | |
| <div className="flex items-center gap-3 mb-2"> | |
| <div className="px-3 py-1 bg-[#8b5cf6]/20 text-[#8b5cf6] text-xs font-bold rounded border border-[#8b5cf6]/50">SCHOLAR PRO</div> | |
| <span className="font-mono text-xs text-slate-500">ID: SYN-{Math.floor(Math.random() * 9000) + 1000}</span> | |
| </div> | |
| <h1 className="text-2xl md:text-3xl font-black text-white">Benvenuto, <span className="text-[#8b5cf6]">{userData.name}</span></h1> | |
| </div> | |
| <button onClick={() => setCurrentView('LANDING')} className="w-full md:w-auto px-4 py-2 bg-slate-800 text-slate-400 font-mono text-xs rounded hover:text-white hover:bg-slate-700 text-center"> | |
| ← TORNA ALLA HOME | |
| </button> | |
| </div> | |
| {/* Stats Bar */} | |
| <div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8"> | |
| {[ | |
| { label: 'XP Totali', value: userData.xp, color: '#8b5cf6', icon: Sparkles }, | |
| { label: 'Ore Studio', value: `${userData.hours}h`, color: '#06b6d4', icon: Clock }, | |
| { label: 'Streak', value: `${userData.streak} giorni`, color: '#10b981', icon: Zap }, | |
| { label: 'Ricerche', value: '∞', color: '#f59e0b', icon: Search }, | |
| ].map((stat, i) => ( | |
| <HUDCard key={i} className="p-4 rounded-xl"> | |
| <div className="flex items-center gap-3"> | |
| <div className="w-10 h-10 rounded-lg flex items-center justify-center" style={{ background: `${stat.color}20` }}> | |
| <stat.icon className="w-5 h-5" style={{ color: stat.color }} /> | |
| </div> | |
| <div> | |
| <div className="text-2xl font-bold text-white">{stat.value}</div> | |
| <div className="text-xs text-slate-500 font-mono">{stat.label}</div> | |
| </div> | |
| </div> | |
| </HUDCard> | |
| ))} | |
| </div> | |
| {/* Tabs */} | |
| <div className="flex gap-2 mb-8 overflow-x-auto pb-2"> | |
| {tabs.map((tab) => ( | |
| <button key={tab.id} onClick={() => setActiveTab(tab.id)} | |
| className={`flex items-center gap-2 px-4 py-2 rounded-lg font-mono text-xs whitespace-nowrap transition-all | |
| ${activeTab === tab.id ? 'bg-[#8b5cf6] text-white' : 'bg-slate-800/50 text-slate-400 hover:bg-slate-700'}`}> | |
| <tab.icon className="w-4 h-4" />{tab.label} | |
| </button> | |
| ))} | |
| </div> | |
| {/* Tab Content */} | |
| <div className="space-y-6"> | |
| {/* Overview */} | |
| {activeTab === 'overview' && ( | |
| <div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6"> | |
| <HUDCard className="p-6 rounded-2xl col-span-2"> | |
| <h3 className="text-white font-bold mb-4 flex items-center gap-2"><TrendingUp className="w-5 h-5 text-[#10b981]" />Attività Settimanale</h3> | |
| <div className="h-40 flex items-end gap-2"> | |
| {['Lun', 'Mar', 'Mer', 'Gio', 'Ven', 'Sab', 'Dom'].map((d, i) => { | |
| // Distribute total hours roughly across week for viz | |
| const avgDaily = userData.hours / 7; | |
| const h = Math.min(100, (avgDaily + (Math.random() * avgDaily * 0.5)) * 10); | |
| return (<div key={d} className="flex-1 flex flex-col items-center gap-2"> | |
| <div className="w-full bg-slate-800 rounded-t transition-all duration-1000" style={{ height: `${h || 2}%`, background: `linear-gradient(to top, #8b5cf6, #06b6d4)` }} /> | |
| <span className="text-xs text-slate-500">{d}</span> | |
| </div>); | |
| })} | |
| </div> | |
| </HUDCard> | |
| <HUDCard className="p-6 rounded-2xl"> | |
| <h3 className="text-white font-bold mb-4 flex items-center gap-2"><Search className="w-5 h-5 text-[#06b6d4]" />Ricerche Recenti</h3> | |
| <div className="space-y-3"> | |
| {searchHistory.length === 0 ? <div className="text-slate-500 text-sm italic py-4 text-center">Nessuna ricerca recente</div> : searchHistory.map((s, i) => ( | |
| <div key={i} className="flex justify-between items-center text-sm"> | |
| <span className="text-slate-300">{s.query}</span> | |
| <span className="text-xs text-slate-500">{s.results} risultati</span> | |
| </div> | |
| ))}</div> | |
| </HUDCard> | |
| </div> | |
| )} | |
| {/* Time Dilation Planner */} | |
| {activeTab === 'planner' && ( | |
| <HUDCard className="p-6 rounded-2xl"> | |
| <div className="flex items-center justify-between mb-6"> | |
| <h3 className="text-white font-bold flex items-center gap-2"><Clock className="w-5 h-5 text-[#8b5cf6]" />TIME DILATION ALGORITHM</h3> | |
| <span className="text-xs text-slate-500 font-mono">Adattato al tuo ritmo circadiano</span> | |
| </div> | |
| <div className="space-y-4"> | |
| {plannerEvents.length === 0 ? <div className="text-slate-500 text-sm italic py-8 text-center">Nessun evento programmato. Usa il simulatore per generare un piano.</div> : plannerEvents.map((event, i) => ( | |
| <div key={i} className={`flex items-center gap-4 p-4 rounded-xl border ${event.completed ? 'bg-[#10b981]/10 border-[#10b981]/30' : 'bg-slate-800/30 border-white/5'}`}> | |
| <div className={`w-12 h-12 rounded-xl flex items-center justify-center ${event.completed ? 'bg-[#10b981]/20' : 'bg-[#8b5cf6]/20'}`}> | |
| {event.type === 'focus' && <Brain className="w-6 h-6 text-[#8b5cf6]" />} | |
| {event.type === 'break' && <Coffee className="w-6 h-6 text-[#06b6d4]" />} | |
| {event.type === 'video' && <Video className="w-6 h-6 text-[#8b5cf6]" />} | |
| {event.type === 'quiz' && <HelpCircle className="w-6 h-6 text-[#8b5cf6]" />} | |
| </div> | |
| <div className="flex-1"> | |
| <div className="text-white font-semibold">{event.task}</div> | |
| <div className="text-xs text-slate-500 font-mono">{event.time} • {event.duration}</div> | |
| </div> | |
| {event.completed && <Check className="w-6 h-6 text-[#10b981]" />} | |
| </div> | |
| ))} | |
| </div> | |
| </HUDCard> | |
| )} | |
| {/* Signal Filter Video */} | |
| {activeTab === 'video' && ( | |
| <div className="space-y-6"> | |
| {/* Search Input */} | |
| <HUDCard className="p-6 rounded-2xl"> | |
| <h3 className="text-white font-bold mb-4 flex items-center gap-2"><Filter className="w-5 h-5 text-[#06b6d4]" />SIGNAL FILTER HD</h3> | |
| <p className="text-slate-400 text-sm mb-4">Inserisci un argomento e l'AI troverà il video perfetto per te. Intro, sponsor e contenuti a bassa densità rimossi automaticamente.</p> | |
| <div className="flex gap-3"> | |
| <div className="flex-1 relative"> | |
| <Search className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-slate-500" /> | |
| <input type="text" value={videoTopic} onChange={(e) => setVideoTopic(e.target.value)} | |
| onKeyDown={(e) => e.key === 'Enter' && searchVideo()} | |
| placeholder="Es. Rivoluzione Francese, Fisica, Matematica..." | |
| className="w-full bg-slate-800 border border-slate-600 rounded-xl py-3 pl-12 pr-4 text-white placeholder:text-slate-500 focus:border-[#06b6d4] focus:outline-none font-mono" /> | |
| </div> | |
| <button onClick={searchVideo} disabled={videoSearching || !videoTopic.trim()} | |
| className="px-6 py-3 bg-[#06b6d4] text-black font-bold rounded-xl hover:bg-[#22d3ee] disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"> | |
| {videoSearching ? <><div className="w-4 h-4 border-2 border-black border-t-transparent rounded-full animate-spin" />Cerco...</> : <><Sparkles className="w-4 h-4" />TROVA VIDEO</>} | |
| </button> | |
| </div> | |
| </HUDCard> | |
| {/* Video Player */} | |
| <div className="grid md:grid-cols-3 gap-6"> | |
| <HUDCard className="p-6 rounded-2xl md:col-span-2"> | |
| {videoSearching ? ( | |
| <div className="aspect-video bg-slate-800 rounded-xl flex flex-col items-center justify-center"> | |
| <div className="w-16 h-16 border-4 border-[#06b6d4] border-t-transparent rounded-full animate-spin mb-4" /> | |
| <p className="text-[#06b6d4] font-mono text-sm">Analisi Neural in corso...</p> | |
| <p className="text-slate-500 font-mono text-xs mt-2">Filtro contenuti attivo</p> | |
| </div> | |
| ) : ( | |
| <> | |
| <div className="flex items-center gap-2 mb-3"> | |
| <span className="px-2 py-1 bg-[#10b981]/20 text-[#10b981] text-[10px] font-bold rounded">FILTRATO</span> | |
| <span className="text-slate-400 text-xs font-mono">{currentVideo.topic}</span> | |
| </div> | |
| <div className="aspect-video bg-black rounded-xl overflow-hidden mb-3"> | |
| <iframe | |
| src={currentVideo.id?.startsWith('?') | |
| ? `https://www.youtube.com/embed?${currentVideo.id.substring(1)}&origin=${window.location.origin}` | |
| : `https://www.youtube.com/embed/${currentVideo.id}?origin=${window.location.origin}`} | |
| className="w-full h-full" | |
| title="Video Filtrato" | |
| allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" | |
| allowFullScreen | |
| /> | |
| </div> | |
| <h4 className="text-white font-semibold">{currentVideo.title}</h4> | |
| </> | |
| )} | |
| </HUDCard> | |
| {/* Video Queue */} | |
| <HUDCard className="p-6 rounded-2xl"> | |
| <h3 className="text-white font-bold mb-4">Coda Suggerita</h3> | |
| <div className="space-y-3"> | |
| {Object.entries(VIDEO_DATABASE).slice(0, 4).map(([key, video], i) => ( | |
| <div key={i} onClick={() => setCurrentVideo({ ...video, topic: key })} | |
| className="flex items-center gap-3 p-3 bg-slate-800/30 rounded-lg hover:bg-slate-700/30 cursor-pointer transition-all"> | |
| <Play className="w-4 h-4 text-[#06b6d4]" /> | |
| <span className="text-slate-300 text-sm line-clamp-1">{video.title}</span> | |
| </div> | |
| ))} | |
| </div> | |
| </HUDCard> | |
| </div> | |
| </div> | |
| )} | |
| {/* Memory Lock Pro */} | |
| {activeTab === 'memory' && ( | |
| <HUDCard className="p-6 rounded-2xl"> | |
| <div className="flex items-center justify-between mb-6"> | |
| <h3 className="text-white font-bold flex items-center gap-2"><Brain className="w-5 h-5 text-[#8b5cf6]" />MEMORY LOCK PRO</h3> | |
| <div className="flex gap-2"> | |
| <input | |
| type="text" | |
| value={cardTopic} | |
| onChange={(e) => setCardTopic(e.target.value)} | |
| onKeyDown={(e) => e.key === 'Enter' && generateFlashcards()} | |
| placeholder="Argomento flashcard..." | |
| className="bg-slate-900 border border-slate-700 rounded-lg px-3 py-1 text-sm text-white focus:outline-none focus:border-[#8b5cf6]" | |
| /> | |
| <button onClick={generateFlashcards} disabled={isGeneratingCards || !cardTopic.trim()} | |
| className="p-2 bg-[#8b5cf6] text-white rounded-lg hover:bg-[#7c3aed] disabled:opacity-50"> | |
| {isGeneratingCards ? <div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin" /> : <Sparkles className="w-4 h-4" />} | |
| </button> | |
| </div> | |
| </div> | |
| <div className="grid md:grid-cols-3 gap-6"> | |
| {memoryCards.map((card) => ( | |
| <div key={card.id} onClick={() => setFlippedCard(flippedCard === card.id ? null : card.id)} | |
| className="relative h-48 cursor-pointer" style={{ perspective: '1000px' }}> | |
| <div className={`absolute inset-0 transition-transform duration-500 rounded-xl`} | |
| style={{ transformStyle: 'preserve-3d', transform: flippedCard === card.id ? 'rotateY(180deg)' : '' }}> | |
| {/* Front */} | |
| <div className="absolute inset-0 bg-gradient-to-br from-[#8b5cf6]/20 to-[#06b6d4]/20 border border-white/10 rounded-xl p-6 flex flex-col justify-between" | |
| style={{ backfaceVisibility: 'hidden' }}> | |
| <div className="text-white font-semibold text-center mt-4">{card.front}</div> | |
| <div className="flex items-center gap-2"> | |
| <div className="flex-1 h-1 bg-slate-700 rounded-full overflow-hidden"> | |
| <div className="h-full bg-[#8b5cf6]" style={{ width: `${card.mastery}%` }} /> | |
| </div> | |
| <span className="text-xs text-slate-400">{card.mastery}%</span> | |
| </div> | |
| </div> | |
| {/* Back */} | |
| <div className="absolute inset-0 bg-[#10b981]/20 border border-[#10b981]/30 rounded-xl p-6 flex items-center justify-center" | |
| style={{ backfaceVisibility: 'hidden', transform: 'rotateY(180deg)' }}> | |
| <div className="text-[#10b981] font-bold text-xl text-center">{card.back}</div> | |
| </div> | |
| </div> | |
| </div> | |
| ))} | |
| </div> | |
| </HUDCard> | |
| )} | |
| {/* Voice Commander */} | |
| {activeTab === 'voice' && ( | |
| <HUDCard className="p-6 rounded-2xl"> | |
| <h3 className="text-white font-bold mb-6 flex items-center gap-2"><Mic className="w-5 h-5 text-[#06b6d4]" />VOICE COMMANDER AVANZATO</h3> | |
| <div className="text-center py-12"> | |
| <button onClick={startVoiceCommand} | |
| className={`w-32 h-32 rounded-full flex items-center justify-center transition-all mx-auto mb-6 | |
| ${isListening ? 'bg-[#f43f5e] animate-pulse shadow-[0_0_60px_rgba(244,63,94,0.5)]' : 'bg-[#8b5cf6] hover:bg-[#7c3aed] shadow-[0_0_40px_rgba(139,92,246,0.3)]'}`}> | |
| {isListening ? <MicOff className="w-12 h-12 text-white" /> : <Mic className="w-12 h-12 text-white" />} | |
| </button> | |
| <p className="text-slate-400 font-mono text-sm mb-4">{isListening ? 'Ascolto in corso...' : 'Clicca per attivare i comandi vocali'}</p> | |
| {voiceQuery && <div className="p-4 bg-slate-800/50 rounded-xl inline-block"><span className="text-white">"{voiceQuery}"</span></div>} | |
| <div className="mt-8 grid md:grid-cols-3 gap-4 text-left"> | |
| {['Cerca [argomento]', 'Avvia sessione studio', 'Mostra statistiche'].map((cmd, i) => ( | |
| <div key={i} className="p-3 bg-slate-800/30 rounded-lg"><span className="text-[#06b6d4] font-mono text-xs">{cmd}</span></div> | |
| ))} | |
| </div> | |
| </div> | |
| </HUDCard> | |
| )} | |
| {/* Reports */} | |
| {activeTab === 'reports' && ( | |
| <HUDCard className="p-6 rounded-2xl"> | |
| <h3 className="text-white font-bold mb-6 flex items-center gap-2"><Mail className="w-5 h-5 text-[#8b5cf6]" />REPORT EMAIL SETTIMANALI</h3> | |
| <div className="bg-slate-800/30 rounded-xl p-6 border border-white/5"> | |
| <div className="flex items-center gap-4 mb-6"> | |
| <div className="w-12 h-12 bg-[#8b5cf6]/20 rounded-full flex items-center justify-center"><Mail className="w-6 h-6 text-[#8b5cf6]" /></div> | |
| <div> | |
| <div className="text-white font-bold">Report Settimanale - Synapse OS</div> | |
| <div className="text-xs text-slate-500">Inviato ogni Domenica alle 20:00</div> | |
| </div> | |
| </div> | |
| <div className="space-y-4 text-slate-300 text-sm"> | |
| <p>📊 <strong>Attività:</strong> 14.5 ore di Deep Work completate</p> | |
| <p>🎯 <strong>Focus Score:</strong> 8.5/10 (+12% vs settimana precedente)</p> | |
| <p>📚 <strong>Materie:</strong> Storia (75%), Scienze (45%), Matematica (60%)</p> | |
| <p>🏆 <strong>Voto Simulato:</strong> 8.5</p> | |
| </div> | |
| <div className="mt-6 pt-4 border-t border-white/5"> | |
| <label className="flex items-center gap-3 cursor-pointer"> | |
| <input type="checkbox" defaultChecked className="w-4 h-4 accent-[#8b5cf6]" /> | |
| <span className="text-slate-400 text-sm">Invia report anche ai genitori</span> | |
| </label> | |
| </div> | |
| </div> | |
| </HUDCard> | |
| )} | |
| {/* Support Chat (AI) */} | |
| {activeTab === 'support' && ( | |
| <HUDCard className="p-6 rounded-2xl h-[600px] flex flex-col"> | |
| <div className="flex items-center justify-between mb-4"> | |
| <h3 className="text-white font-bold flex items-center gap-2"><HelpCircle className="w-5 h-5 text-[#10b981]" />SUPPORTO PRIORITARIO 24/7</h3> | |
| <div className="flex items-center gap-2"> | |
| <div className="w-2 h-2 bg-[#10b981] rounded-full animate-pulse" /> | |
| <span className="text-[#10b981] font-mono text-xs">AGENT ONLINE</span> | |
| </div> | |
| </div> | |
| <div className="flex-1 overflow-y-auto space-y-4 mb-4 pr-2 custom-scrollbar"> | |
| {chatMessages.map((msg, i) => ( | |
| <div key={i} className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}> | |
| <div className={`max-w-[80%] p-3 rounded-xl text-sm ${msg.role === 'user' ? 'bg-[#8b5cf6] text-white rounded-br-none' : 'bg-slate-800 text-slate-300 rounded-bl-none'}`}> | |
| {msg.content} | |
| </div> | |
| </div> | |
| ))} | |
| {isTyping && <div className="text-slate-500 text-xs animate-pulse">L'agente sta scrivendo...</div>} | |
| </div> | |
| <div className="pt-4 border-t border-white/10 flex gap-2"> | |
| <input | |
| type="text" | |
| value={chatInput} | |
| onChange={(e) => setChatInput(e.target.value)} | |
| onKeyDown={(e) => e.key === 'Enter' && handleSupportMessage()} | |
| placeholder="Scrivi il tuo problema..." | |
| className="flex-1 bg-slate-900 border border-slate-700 rounded-lg px-4 py-2 text-white focus:outline-none focus:border-[#10b981]" | |
| /> | |
| <button onClick={handleSupportMessage} disabled={isTyping || !chatInput.trim()} | |
| className="p-3 bg-[#10b981] text-black rounded-lg hover:bg-[#059669] disabled:opacity-50"> | |
| <Send className="w-5 h-5" /> | |
| </button> | |
| </div> | |
| </HUDCard> | |
| )} | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| }; | |
| // ============================================ | |
| // SYSTEM FOOTER | |
| // ============================================ | |
| const SystemFooter = () => ( | |
| <footer className="py-12 px-6 border-t border-white/5 relative" style={{ zIndex: 1 }}> | |
| <div className="max-w-6xl mx-auto"> | |
| <div className="grid grid-cols-2 md:grid-cols-4 gap-8 font-mono text-xs uppercase text-slate-500"> | |
| <div><div className="text-slate-400 mb-1">ID SISTEMA</div><div className="text-white">SYN-8842</div></div> | |
| <div><div className="text-slate-400 mb-1">SICUREZZA</div><div className="text-[#10b981] flex items-center gap-1"><Lock className="w-3 h-3" strokeWidth={1.5} />AES-256</div></div> | |
| <div><div className="text-slate-400 mb-1">VERSIONE</div><div className="text-white">v2.0.1-stabile</div></div> | |
| <div><div className="text-slate-400 mb-1">STATO</div><div className="text-[#10b981] flex items-center gap-1"><div className="w-2 h-2 rounded-full bg-[#10b981] animate-pulse" />OPERATIVO</div></div> | |
| </div> | |
| <div className="mt-12 pt-8 border-t border-white/5 flex flex-col md:flex-row items-center justify-between gap-4"> | |
| <div className="flex items-center gap-2"><Brain className="w-5 h-5 text-[#8b5cf6]" strokeWidth={1.5} /><GlitchText className="text-white font-bold">SYNAPSE</GlitchText><span className="text-[#06b6d4]">[OS]</span></div> | |
| <div className="font-mono text-xs text-slate-500">COPYRIGHT © 2026 SYNAPSE CORP. TUTTI I DIRITTI RISERVATI.</div> | |
| <div className="font-mono text-xs text-slate-500 flex items-center gap-2"><Shield className="w-3 h-3 text-[#8b5cf6]" strokeWidth={1.5} />RETE NEURALE PROTETTA</div> | |
| </div> | |
| </div> | |
| </footer> | |
| ); | |
| // ============================================ | |
| // MAIN APP | |
| // ============================================ | |
| const AppContent = () => { | |
| const { currentView, protocol } = useUser(); | |
| // Landing Page (Guest or Core) | |
| if (currentView === 'LANDING' || protocol !== 'PRO') { | |
| return ( | |
| <div className="min-h-screen bg-[#020617] relative overflow-hidden text-slate-300 font-sans selection:bg-[#8b5cf6] selection:text-white"> | |
| <NeuralVoid /> | |
| <NeuralCanvas /> | |
| <Header /> | |
| <AuthModal /> | |
| <PaymentModal /> | |
| <LiveTicker /> | |
| <main> | |
| <HeroUnit /> | |
| <ErrorBoundary> | |
| <CoreSimulator /> | |
| </ErrorBoundary> | |
| <FeatureModules /> | |
| <PricingSection /> | |
| </main> | |
| <SystemFooter /> | |
| <style>{` | |
| @keyframes glitch { 0%,90%{transform:translate(0)} 92%{transform:translate(-2px,1px) skewX(-1deg)} 94%{transform:translate(2px,-1px) skewX(1deg)} 96%{transform:translate(0)} } | |
| .glitch-text { animation: glitch 5s infinite; display: inline-block; } | |
| .hud-corner { position:absolute; width:10px; height:10px; border-color:white; opacity:0.3; border-style:solid; } | |
| .hud-corner-tl { top:5px; left:5px; border-width:1px 0 0 1px; } | |
| .hud-corner-tr { top:5px; right:5px; border-width:1px 1px 0 0; } | |
| .hud-corner-bl { bottom:5px; left:5px; border-width:0 0 1px 1px; } | |
| .hud-corner-br { bottom:5px; right:5px; border-width:0 1px 1px 0; } | |
| .hud-card:hover .hud-corner { opacity:1; border-color:#8b5cf6; } | |
| @keyframes ticker { 0%{transform:translateX(0)} 100%{transform:translateX(-50%)} } | |
| .ticker-track { animation: ticker 30s linear infinite; } | |
| `}</style> | |
| </div> | |
| ); | |
| } | |
| // Pro Dashboard (Fully Unlocked & Real AI) | |
| return ( | |
| <div className="bg-[#020617] min-h-screen relative overflow-x-hidden font-sans selection:bg-[#8b5cf6] selection:text-white"> | |
| <VFXLayer /> | |
| <NeuralCanvas /> | |
| <PaymentModal /> | |
| <SystemHeader /> | |
| <ProDashboard /> | |
| <style>{` | |
| @keyframes glitch { 0%,90%{transform:translate(0)} 92%{transform:translate(-2px,1px) skewX(-1deg)} 94%{transform:translate(2px,-1px) skewX(1deg)} 96%{transform:translate(0)} } | |
| .glitch-text { animation: glitch 5s infinite; display: inline-block; } | |
| .hud-corner { position:absolute; width:10px; height:10px; border-color:white; opacity:0.3; border-style:solid; } | |
| .hud-corner-tl { top:5px; left:5px; border-width:1px 0 0 1px; } | |
| .hud-corner-tr { top:5px; right:5px; border-width:1px 1px 0 0; } | |
| .hud-corner-bl { bottom:5px; left:5px; border-width:0 0 1px 1px; } | |
| .hud-corner-br { bottom:5px; right:5px; border-width:0 1px 1px 0; } | |
| .hud-card:hover .hud-corner { opacity:1; border-color:#8b5cf6; } | |
| @keyframes ticker { 0%{transform:translateX(0)} 100%{transform:translateX(-50%)} } | |
| .ticker-track { animation: ticker 30s linear infinite; } | |
| `}</style> | |
| </div> | |
| ); | |
| }; | |
| function App() { | |
| return ( | |
| <UserProvider> | |
| <ErrorBoundary> | |
| <AppContent /> | |
| </ErrorBoundary> | |
| </UserProvider> | |
| ); | |
| } | |
| export default App; | |