| import React, { useState, useEffect, useRef } from 'react'; |
| import { |
| Disc, |
| Play, |
| Pause, |
| SkipForward, |
| SkipBack, |
| Volume2, |
| Volume1, |
| VolumeX, |
| Plus, |
| Trash2, |
| Wand2, |
| Terminal, |
| HelpCircle, |
| AlertTriangle, |
| Layers, |
| Cpu, |
| Tv, |
| ListMusic, |
| CheckCircle, |
| ExternalLink, |
| ChevronRight, |
| ShieldCheck, |
| Zap, |
| Music, |
| Maximize2, |
| LogOut, |
| Send, |
| Sliders, |
| AudioLines, |
| Sparkles, |
| Info, |
| X, |
| MessageSquare |
| } from 'lucide-react'; |
| import { SongTrack, BotStats, DiscordCommand, MockDiscordMessage, INITIAL_QUEUE, BOT_COMMANDS } from './types'; |
|
|
| |
| class WebMusicSynth { |
| private ctx: AudioContext | null = null; |
| private activeOscillators: OscillatorNode[] = []; |
| private mainGainNode: GainNode | null = null; |
|
|
| init() { |
| if (!this.ctx) { |
| const AudioContextClass = window.AudioContext || (window as any).webkitAudioContext; |
| if (AudioContextClass) { |
| this.ctx = new AudioContextClass(); |
| } |
| } |
| } |
|
|
| |
| playAirhorn() { |
| this.init(); |
| if (!this.ctx) return; |
| |
| const now = this.ctx.currentTime; |
| const gain = this.ctx.createGain(); |
| gain.connect(this.ctx.destination); |
| gain.gain.setValueAtTime(0, now); |
| gain.gain.linearRampToValueAtTime(0.3, now + 0.05); |
| gain.gain.linearRampToValueAtTime(0.2, now + 0.2); |
| gain.gain.exponentialRampToValueAtTime(0.001, now + 0.8); |
|
|
| |
| const frequencies = [220, 222, 330, 440]; |
| frequencies.forEach(f => { |
| const osc = this.ctx!.createOscillator(); |
| osc.type = 'sawtooth'; |
| osc.frequency.setValueAtTime(f, now); |
| osc.frequency.linearRampToValueAtTime(f - 5, now + 0.6); |
| |
| const filter = this.ctx!.createBiquadFilter(); |
| filter.type = 'lowpass'; |
| filter.frequency.setValueAtTime(1000, now); |
|
|
| osc.connect(filter); |
| filter.connect(gain); |
| osc.start(now); |
| osc.stop(now + 0.8); |
| }); |
| } |
|
|
| |
| playLaser() { |
| this.init(); |
| if (!this.ctx) return; |
|
|
| const now = this.ctx.currentTime; |
| const osc = this.ctx.createOscillator(); |
| const gain = this.ctx.createGain(); |
| |
| osc.type = 'triangle'; |
| osc.frequency.setValueAtTime(1200, now); |
| osc.frequency.exponentialRampToValueAtTime(80, now + 0.45); |
|
|
| gain.gain.setValueAtTime(0.25, now); |
| gain.gain.exponentialRampToValueAtTime(0.001, now + 0.45); |
|
|
| osc.connect(gain); |
| gain.connect(this.ctx.destination); |
| osc.start(now); |
| osc.stop(now + 0.5); |
| } |
|
|
| |
| playSynthPad() { |
| this.init(); |
| if (!this.ctx) return; |
|
|
| const now = this.ctx.currentTime; |
| const chords = [196.00, 246.94, 293.66, 392.00]; |
| const masterGain = this.ctx.createGain(); |
| masterGain.gain.setValueAtTime(0, now); |
| masterGain.gain.linearRampToValueAtTime(0.3, now + 0.2); |
| masterGain.gain.exponentialRampToValueAtTime(0.001, now + 2.5); |
| masterGain.connect(this.ctx.destination); |
|
|
| chords.forEach((freq, idx) => { |
| const osc = this.ctx!.createOscillator(); |
| const oscGain = this.ctx!.createGain(); |
| |
| osc.type = 'sine'; |
| osc.frequency.setValueAtTime(freq + (idx * 0.4), now); |
| |
| oscGain.gain.setValueAtTime(0.08, now); |
| |
| osc.connect(oscGain); |
| oscGain.connect(masterGain); |
| osc.start(now); |
| osc.stop(now + 2.6); |
| }); |
| } |
|
|
| |
| toggleRain(enable: boolean) { |
| this.init(); |
| if (!this.ctx) return; |
|
|
| if (!enable) { |
| if (this.mainGainNode) { |
| try { |
| this.mainGainNode.gain.setValueAtTime(this.mainGainNode.gain.value, this.ctx.currentTime); |
| this.mainGainNode.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + 0.8); |
| } catch {} |
| } |
| return; |
| } |
|
|
| const now = this.ctx.currentTime; |
| this.mainGainNode = this.ctx.createGain(); |
| this.mainGainNode.gain.setValueAtTime(0, now); |
| this.mainGainNode.gain.linearRampToValueAtTime(0.25, now + 1.0); |
| this.mainGainNode.connect(this.ctx.destination); |
|
|
| const bufferSize = 2 * this.ctx.sampleRate; |
| const noiseBuffer = this.ctx.createBuffer(1, bufferSize, this.ctx.sampleRate); |
| const output = noiseBuffer.getChannelData(0); |
| let lastOut = 0.0; |
|
|
| for (let i = 0; i < bufferSize; i++) { |
| const white = Math.random() * 2 - 1; |
| output[i] = (lastOut + (0.02 * white)) / 1.02; |
| lastOut = output[i]; |
| output[i] *= 3.5; |
| } |
|
|
| const noiseSource = this.ctx.createBufferSource(); |
| noiseSource.buffer = noiseBuffer; |
| noiseSource.loop = true; |
|
|
| const lowpass = this.ctx.createBiquadFilter(); |
| lowpass.type = 'lowpass'; |
| lowpass.frequency.setValueAtTime(700, now); |
|
|
| noiseSource.connect(lowpass); |
| lowpass.connect(this.mainGainNode); |
| noiseSource.start(now); |
| } |
| } |
|
|
| export default function App() { |
| const [activeTab, setActiveTab] = useState<'home' | 'dashboard' | 'console'>('home'); |
| const [joinedChannel, setJoinedChannel] = useState(false); |
| const [guildName, setGuildName] = useState('Mi Servidor Principal'); |
|
|
| |
| const [isPlaying, setIsPlaying] = useState(true); |
| const [songQueue, setSongQueue] = useState<SongTrack[]>(INITIAL_QUEUE); |
| const [currentTrackIndex, setCurrentTrackIndex] = useState(0); |
| const [playProgress, setPlayProgress] = useState(15); |
| const [volume, setVolume] = useState(90); |
| const [bassboostMode, setBassboostMode] = useState<'Off' | 'Bass+ Low'| 'Glizh Heavy'>('Off'); |
| const [activeFilter, setActiveFilter] = useState<'None' | 'Nightcore' | 'Vaporwave' | 'Lo-Fi Chill'>('None'); |
|
|
| |
| const [manualTitle, setManualTitle] = useState(''); |
| const [manualArtist, setManualArtist] = useState(''); |
| const [geminiSearchQuery, setGeminiSearchQuery] = useState(''); |
| const [isAiSuggesting, setIsAiSuggesting] = useState(false); |
|
|
| |
| const synthRef = useRef<WebMusicSynth | null>(null); |
| const [isRainEnabled, setIsRainEnabled] = useState(false); |
|
|
| |
| const [botStats, setBotStats] = useState<BotStats>({ |
| guilds: 18249, |
| users: 489210, |
| voiceChannels: 3412, |
| ping: 18, |
| uptime: '28d 4h 12m' |
| }); |
|
|
| |
| const [discordChat, setDiscordChat] = useState<MockDiscordMessage[]>([ |
| { |
| id: 'msg-1', |
| author: { username: 'cristophergamer', avatar: '🎮', isBot: false, colorClass: 'text-amber-400 font-bold' }, |
| content: 'Buenas gente, ¿qué bot de música vamos a usar hoy? Hay que armar una sesión chill.', |
| timestamp: 'Hoy a las 02:01 AM' |
| }, |
| { |
| id: 'msg-2', |
| author: { username: 'mandy_xx', avatar: '🐱', isBot: false, colorClass: 'text-rose-450 font-bold' }, |
| content: '¡Usemos Glizh! Suena muchísimo más limpio que los otros bots y tiene unos graves brutales.', |
| timestamp: 'Hoy a las 02:02 AM' |
| }, |
| { |
| id: 'msg-3', |
| author: { username: 'Glizh', avatar: '👑', isBot: true, colorClass: 'text-yellow-400 font-bold' }, |
| timestamp: 'Hoy a las 02:02 AM', |
| embed: { |
| title: '🎶 Glizh Sound System v4.8 Activo', |
| description: '¡Gracias por llamarme! Estoy escuchando en el canal de voz **🔊 General Ambient**. Puedes invitar a tus amigos o usar `/play` directamente aquí.', |
| color: 'border-amber-500', |
| fields: [ |
| { name: 'DSP Core', value: '🟢 Ultra Stereo Link (320kbps)', inline: true }, |
| { name: 'Filtro Activo', value: '✨ Glizh ClearAudio v4', inline: true }, |
| ], |
| footer: 'Listo | Comandos de barra activos' |
| } |
| } |
| ]); |
| const [consoleInput, setConsoleInput] = useState(''); |
| const [isConsoleGenerating, setIsConsoleGenerating] = useState(false); |
|
|
| |
| const [showInviteModal, setShowInviteModal] = useState(false); |
| const [inviteServerName, setInviteServerName] = useState('Mi Servidor Dorado'); |
| const [inviteCompleted, setInviteCompleted] = useState(false); |
|
|
| |
| const [isAssistantOpen, setIsAssistantOpen] = useState(false); |
| const [assistantHistory, setAssistantHistory] = useState<{ sender: 'user' | 'bot'; text: string }[]>([ |
| { sender: 'bot', text: '¡Hola! Soy Glizh AI, el asistente virtual integrado. Pregúntame lo que quieras sobre comandos de música, filtros de audio DSP, o cómo solucionar problemas con Lavalink. ✨' } |
| ]); |
| const [assistantInput, setAssistantInput] = useState(''); |
| const [isAssistantLoading, setIsAssistantLoading] = useState(false); |
|
|
| const assistantEndRef = useRef<HTMLDivElement | null>(null); |
|
|
| const activeTrack: SongTrack | undefined = songQueue[currentTrackIndex]; |
|
|
| |
| useEffect(() => { |
| if (isAssistantOpen) { |
| assistantEndRef.current?.scrollIntoView({ behavior: 'smooth' }); |
| } |
| }, [assistantHistory, isAssistantOpen]); |
|
|
| |
| useEffect(() => { |
| let interval: any = null; |
| if (isPlaying && activeTrack) { |
| interval = setInterval(() => { |
| setPlayProgress(prev => { |
| const rawDuration = activeTrack.duration; |
| const [m, s] = rawDuration.split(':').map(Number); |
| const totalSecs = (m * 60) + s; |
| if (prev >= totalSecs) { |
| handleSkipForward(); |
| return 0; |
| } |
| return prev + 1; |
| }); |
| }, 1000); |
| } |
| return () => clearInterval(interval); |
| }, [isPlaying, currentTrackIndex, songQueue]); |
|
|
| |
| const getSynth = () => { |
| if (!synthRef.current) { |
| synthRef.current = new WebMusicSynth(); |
| } |
| return synthRef.current; |
| }; |
|
|
| |
| const formatTime = (seconds: number) => { |
| const mins = Math.floor(seconds / 60); |
| const secs = seconds % 60; |
| return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; |
| }; |
|
|
| |
| const handleSkipForward = () => { |
| if (songQueue.length === 0) return; |
| setPlayProgress(0); |
| setCurrentTrackIndex(prev => (prev + 1) % songQueue.length); |
| }; |
|
|
| const handleSkipBack = () => { |
| setPlayProgress(0); |
| setCurrentTrackIndex(prev => (prev - 1 + songQueue.length) % songQueue.length); |
| }; |
|
|
| const handleTogglePlay = () => { |
| setIsPlaying(prev => !prev); |
| getSynth().playSynthPad(); |
| }; |
|
|
| const handleRemoveTrack = (id: string, idx: number) => { |
| if (songQueue.length <= 1) { |
| alert("¡Glizh debe mantener al menos 1 canción en la cola para no detener la música!"); |
| return; |
| } |
| setSongQueue(prev => prev.filter(t => t.id !== id)); |
| if (idx === currentTrackIndex) { |
| setCurrentTrackIndex(0); |
| setPlayProgress(0); |
| } else if (idx < currentTrackIndex) { |
| setCurrentTrackIndex(prev => Math.max(0, prev - 1)); |
| } |
| }; |
|
|
| |
| const handleAddManualTrack = () => { |
| if (!manualTitle.trim()) return; |
| const newT: SongTrack = { |
| id: `manual-${Date.now()}`, |
| title: manualTitle, |
| artist: manualArtist.trim() || 'Artista Desconocido', |
| duration: '03:15', |
| genre: 'Cola Manual', |
| requester: 'Tú' |
| }; |
| setSongQueue(prev => [...prev, newT]); |
| setManualTitle(''); |
| setManualArtist(''); |
| }; |
|
|
| |
| const handleGeminiPlaylistSuggest = async () => { |
| if (!geminiSearchQuery.trim()) { |
| alert("Por favor escribe un ambiente o género. E.g. 'reggaeton viejo para bailar' o 'bachata romantica'"); |
| return; |
| } |
| setIsAiSuggesting(true); |
| try { |
| const response = await fetch('/api/music/suggest', { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ query: geminiSearchQuery }) |
| }); |
| const data = await response.json(); |
| |
| if (data.error) throw new Error(data.error); |
|
|
| if (data.tracks && Array.isArray(data.tracks)) { |
| const parsedTracks: SongTrack[] = data.tracks.map((t: any, index: number) => ({ |
| id: `ai-suggest-${Date.now()}-${index}`, |
| title: t.title || 'Melodía Dinámica', |
| artist: t.artist || 'Glizh AI Studio', |
| duration: t.duration || '03:00', |
| genre: t.genre || 'Género sugerido', |
| requester: 'Asistente Glizh' |
| })); |
|
|
| setSongQueue(prev => [...prev, ...parsedTracks]); |
| |
| const discordEvent: MockDiscordMessage = { |
| id: `disc-event-${Date.now()}`, |
| author: { username: 'Glizh', avatar: '👑', isBot: true, colorClass: 'text-yellow-400 font-bold' }, |
| timestamp: 'Hoy a las ' + new Date().toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'}), |
| embed: { |
| title: `💿 Cola generada por IA cargada: "${geminiSearchQuery}"`, |
| description: `¡El motor inteligente analizó el prompt y añadió **${parsedTracks.length} canciones** a la cola de reproducción!`, |
| color: 'border-yellow-500', |
| fields: parsedTracks.map((t, i) => ({ |
| name: `${i+1}. ${t.title}`, |
| value: `de *${t.artist}* • Duración: ${t.duration}` |
| })), |
| footer: 'Recomendador Inteligente Glizh | Gemini API' |
| } |
| }; |
| setDiscordChat(prev => [...prev, discordEvent]); |
| |
| setGeminiSearchQuery(''); |
| alert(`¡Glizh cargó exitosamente ${parsedTracks.length} canciones en la cola! Compruébalo en la consola o el reproductor.`); |
| } |
| } catch (e: any) { |
| console.error(e); |
| alert(`No se pudo generar la recomendación: ${e.message || 'Verifica la clave de Gemini en el servidor.'}`); |
| } finally { |
| setIsAiSuggesting(false); |
| } |
| }; |
|
|
| |
| const loadPresetTheme = (theme: 'retro' | 'workout' | 'jazz') => { |
| let preset: SongTrack[] = []; |
| if (theme === 'retro') { |
| preset = [ |
| { id: `retro-1-${Date.now()}`, title: 'Provenza', artist: 'KAROL G', duration: '03:27', genre: 'Reggaeton', requester: 'reggaeton_lover' }, |
| { id: `retro-2-${Date.now()}`, title: 'La Bachata', artist: 'Manuel Turizo', duration: '02:42', genre: 'Bachata', requester: 'reggaeton_lover' }, |
| { id: `retro-3-${Date.now()}`, title: 'Un Preview', artist: 'Bad Bunny', duration: '02:44', genre: 'Reggaeton/Trap', requester: 'you' } |
| ]; |
| } else if (theme === 'workout') { |
| preset = [ |
| { id: `work-1-${Date.now()}`, title: 'Trap De La Calle', artist: 'Myke Towers', duration: '03:15', genre: 'Trap Latino', requester: 'shreed_33' }, |
| { id: `work-2-${Date.now()}`, title: 'La Jeepeta', artist: 'Anuel AA', duration: '03:45', genre: 'Trap Latino', requester: 'shreed_33' } |
| ]; |
| } else { |
| preset = [ |
| { id: `jazz-1-${Date.now()}`, title: 'Amargura', artist: 'KAROL G', duration: '02:50', genre: 'Reggaeton', requester: 'zesty_fans' }, |
| { id: `jazz-2-${Date.now()}`, title: 'Ella Baila Sola', artist: 'Eslabon Armado & Peso Pluma', duration: '02:44', genre: 'Regional Mexicano', requester: 'you' } |
| ]; |
| } |
| setSongQueue(prev => [...prev, ...preset]); |
| getSynth().playSynthPad(); |
| }; |
|
|
| |
| const triggerSFX = (type: 'airhorn' | 'laser' | 'pad') => { |
| const synth = getSynth(); |
| if (type === 'airhorn') synth.playAirhorn(); |
| if (type === 'laser') synth.playLaser(); |
| if (type === 'pad') synth.playSynthPad(); |
| }; |
|
|
| const handleToggleRainAmbient = () => { |
| const nextState = !isRainEnabled; |
| setIsRainEnabled(nextState); |
| getSynth().toggleRain(nextState); |
| }; |
|
|
| |
| const handleSendConsole = async (overrideMsg?: string) => { |
| const actualInput = overrideMsg || consoleInput; |
| if (!actualInput.trim()) return; |
|
|
| const userMessage: MockDiscordMessage = { |
| id: `discord-usr-${Date.now()}`, |
| author: { username: 'tú', avatar: '⚡', isBot: false, colorClass: 'text-amber-400 font-bold' }, |
| content: actualInput, |
| timestamp: 'Hoy a las ' + new Date().toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'}) |
| }; |
|
|
| setDiscordChat(prev => [...prev, userMessage]); |
| if (!overrideMsg) setConsoleInput(''); |
| setIsConsoleGenerating(true); |
|
|
| const cmdClean = actualInput.toLowerCase().trim(); |
| |
| if (cmdClean.startsWith('/play ')) { |
| const targetQuery = actualInput.substring(6); |
| setTimeout(() => { |
| const newTrack: SongTrack = { |
| id: `cmd-added-${Date.now()}`, |
| title: targetQuery.charAt(0).toUpperCase() + targetQuery.slice(1), |
| artist: 'Artista Encontrado', |
| duration: '03:10', |
| genre: 'Vía Terminal', |
| requester: 'tú' |
| }; |
| setSongQueue(prev => [...prev, newTrack]); |
| |
| setDiscordChat(prev => [...prev, { |
| id: `discord-bot-${Date.now()}`, |
| author: { username: 'Glizh', avatar: '👑', isBot: true, colorClass: 'text-yellow-400 font-bold' }, |
| timestamp: 'Hoy a las ' + new Date().toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'}), |
| embed: { |
| title: `🔊 Añadido a la cola`, |
| description: `Se añadió **${newTrack.title}** de *${newTrack.artist}* en la posición **#${songQueue.length + 1}**`, |
| color: 'border-yellow-500', |
| fields: [ |
| { name: 'Tamaño de cola', value: `${songQueue.length + 1} canciones`, inline: true }, |
| { name: 'Reproducción aproximada', value: `En aprox ${(songQueue.length) * 3} mins`, inline: true } |
| ], |
| footer: 'Glizh Music Engine | Activo' |
| } |
| }]); |
| setIsConsoleGenerating(false); |
| }, 500); |
|
|
| } else if (cmdClean === '/skip') { |
| setTimeout(() => { |
| handleSkipForward(); |
| setDiscordChat(prev => [...prev, { |
| id: `discord-bot-${Date.now()}`, |
| author: { username: 'Glizh', avatar: '👑', isBot: true, colorClass: 'text-yellow-400 font-bold' }, |
| timestamp: 'Hoy a las ' + new Date().toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'}), |
| embed: { |
| title: '⏭️ Canción Saltada', |
| description: `Ahora suena: **${songQueue[(currentTrackIndex + 1) % songQueue.length]?.title || 'Ninguna'}**`, |
| color: 'border-amber-500' |
| } |
| }]); |
| setIsConsoleGenerating(false); |
| }, 400); |
|
|
| } else if (cmdClean === '/queue') { |
| setTimeout(() => { |
| setDiscordChat(prev => [...prev, { |
| id: `discord-bot-${Date.now()}`, |
| author: { username: 'Glizh', avatar: '👑', isBot: true, colorClass: 'text-yellow-400 font-bold' }, |
| timestamp: 'Hoy a las ' + new Date().toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'}), |
| embed: { |
| title: '📜 Lista de Canciones en Espera', |
| description: songQueue.map((t, i) => `${i === currentTrackIndex ? '➔ 🔊' : `• [${i+1}]`} **${t.title}** - *${t.artist}* (${t.duration})`).join('\n'), |
| color: 'border-yellow-500', |
| footer: 'Glizh Bot | Usa /skip para saltar' |
| } |
| }]); |
| setIsConsoleGenerating(false); |
| }, 400); |
|
|
| } else if (cmdClean === '/lyrics') { |
| setTimeout(() => { |
| setDiscordChat(prev => [...prev, { |
| id: `discord-bot-${Date.now()}`, |
| author: { username: 'Glizh', avatar: '👑', isBot: true, colorClass: 'text-yellow-400 font-bold' }, |
| timestamp: 'Hoy a las ' + new Date().toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'}), |
| embed: { |
| title: `🎵 Letra de: "${activeTrack?.title || 'Nada Sonando'}"`, |
| description: `*(Pre-loaded lyrics excerpt)*\n\n[00:12] Baby, te vi en Provenza...\n[00:25] Hace rato que no sé de ti...\n[00:44] Aunque ande con otra, no te olvido...\n\n(Sincronización de letra en vivo activa)`, |
| color: 'border-yellow-500' |
| } |
| }]); |
| setIsConsoleGenerating(false); |
| }, 550); |
|
|
| } else if (cmdClean.startsWith('/volume ')) { |
| const volVal = parseInt(cmdClean.substring(8)) || 100; |
| setVolume(Math.min(150, Math.max(0, volVal))); |
| setTimeout(() => { |
| setDiscordChat(prev => [...prev, { |
| id: `discord-bot-${Date.now()}`, |
| author: { username: 'Glizh', avatar: '👑', isBot: true, colorClass: 'text-yellow-400 font-bold' }, |
| timestamp: 'Hoy a las ' + new Date().toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'}), |
| embed: { |
| title: `🔊 Volumen Ajustado`, |
| description: `Se modificó la escala del volumen maestro a **${volVal}%**.`, |
| color: 'border-yellow-500' |
| } |
| }]); |
| setIsConsoleGenerating(false); |
| }, 400); |
|
|
| } else if (cmdClean.startsWith('/bassboost ')) { |
| const boostVal = cmdClean.substring(11).toUpperCase(); |
| setTimeout(() => { |
| setDiscordChat(prev => [...prev, { |
| id: `discord-bot-${Date.now()}`, |
| author: { username: 'Glizh', avatar: '👑', isBot: true, colorClass: 'text-yellow-400 font-bold' }, |
| timestamp: 'Hoy a las ' + new Date().toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'}), |
| embed: { |
| title: `⚡ Filtro Bassboost Aplicado`, |
| description: `Umbral dinámico configurado a: **${boostVal}**`, |
| color: 'border-yellow-500' |
| } |
| }]); |
| setIsConsoleGenerating(false); |
| }, 400); |
|
|
| } else { |
| try { |
| const response = await fetch('/api/music/chat', { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ message: actualInput }) |
| }); |
| const data = await response.json(); |
| |
| if (data.error) throw new Error(data.error); |
|
|
| setDiscordChat(prev => [...prev, { |
| id: `discord-bot-${Date.now()}`, |
| author: { username: 'Glizh', avatar: '👑', isBot: true, colorClass: 'text-yellow-400 font-bold' }, |
| timestamp: 'Hoy a las ' + new Date().toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'}), |
| embed: { |
| title: 'Sala de Chat Interactiva Glizh', |
| description: data.text || '¡Alto y claro! El bot de música Glizh está listo.', |
| color: 'border-yellow-500' |
| } |
| }]); |
| } catch (err: any) { |
| setDiscordChat(prev => [...prev, { |
| id: `discord-bot-${Date.now()}`, |
| author: { username: 'Glizh', avatar: '👑', isBot: true, colorClass: 'text-yellow-400 font-bold' }, |
| timestamp: 'Hoy a las ' + new Date().toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'}), |
| content: `🎸 ¡Glizh está online! Prueba comandos como \`/play provenza\` o \`/queue\` en la consola del bot.` |
| }]); |
| } finally { |
| setIsConsoleGenerating(false); |
| } |
| } |
| }; |
|
|
| |
| const handleSimulateInviteSubmit = () => { |
| setInviteCompleted(true); |
| setTimeout(() => { |
| setShowInviteModal(false); |
| setGuildName(inviteServerName); |
| setInviteCompleted(false); |
| alert(`¡Éxito! Glizh Bot se ha enlazado al servidor de Discord "${inviteServerName}".`); |
| }, 1200); |
| }; |
|
|
| |
| const handleSendAssistant = async () => { |
| if (!assistantInput.trim()) return; |
| const userText = assistantInput; |
| setAssistantHistory(prev => [...prev, { sender: 'user', text: userText }]); |
| setAssistantInput(''); |
| setIsAssistantLoading(true); |
| try { |
| const response = await fetch('/api/music/chat', { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ message: userText }) |
| }); |
| const data = await response.json(); |
| if (data.error) throw new Error(data.error); |
| setAssistantHistory(prev => [...prev, { sender: 'bot', text: data.text || 'He recibido tu solicitud.' }]); |
| } catch (e) { |
| setAssistantHistory(prev => [...prev, { sender: 'bot', text: '¡Hola! Estoy listo para sonar. Activa tu GEMINI_API_KEY para respuestas inteligentes.' }]); |
| } finally { |
| setIsAssistantLoading(false); |
| } |
| }; |
|
|
| return ( |
| <div className="flex flex-col min-h-screen w-full bg-[#070709] text-[#f4f4f5] font-sans antialiased overflow-x-hidden relative select-none"> |
| |
| {/* Dynamic Background Ambient Gold Glow */} |
| <div className="absolute top-[-10%] left-[25%] w-[500px] h-[500px] rounded-full bg-amber-500/5 glow-ambient pointer-events-none z-0" /> |
| <div className="absolute bottom-[20%] right-[-10%] w-[450px] h-[450px] rounded-full bg-yellow-500/5 glow-ambient pointer-events-none z-0" /> |
| |
| {/* Website Top Header Navbar */} |
| <nav className="sticky top-0 w-full z-40 bg-[#070709]/85 backdrop-blur-md border-b border-white/5 px-4 sm:px-8 py-3.5 flex items-center justify-between"> |
| <div className="flex items-center gap-2.5"> |
| <div className="p-1.5 bg-gradient-to-tr from-amber-500 to-yellow-600 rounded-lg text-black shadow-lg shadow-amber-500/10 relative overflow-hidden"> |
| <Music className="h-5 w-5 animate-pulse" /> |
| <div className="absolute inset-0 bg-white/20 hover:scale-110 transition-transform" /> |
| </div> |
| <div> |
| <div className="flex items-center gap-1.5"> |
| <span className="font-extrabold tracking-wider text-transparent bg-clip-text bg-gradient-to-r from-amber-200 via-yellow-450 to-amber-500 text-lg sm:text-xl font-mono"> |
| GLIZH |
| </span> |
| <span className="h-2 w-2 rounded-full bg-amber-500 animate-ping inline-block" /> |
| </div> |
| <p className="text-[10px] uppercase tracking-widest text-[#a1a1aa] font-mono leading-none"> |
| Premium Discord Sound |
| </p> |
| </div> |
| </div> |
| |
| {/* Tab Selector Nav links */} |
| <div className="hidden md:flex items-center gap-1.5 px-1.5 py-1 rounded-full bg-white/5 border border-white/5"> |
| <button |
| onClick={() => setActiveTab('home')} |
| className={`px-4 py-1.5 rounded-full text-xs font-semibold tracking-wide transition-all cursor-pointer ${ |
| activeTab === 'home' ? 'bg-amber-500 text-black font-bold' : 'text-neutral-400 hover:text-white' |
| }`} |
| > |
| Inicio |
| </button> |
| <button |
| onClick={() => setActiveTab('dashboard')} |
| className={`px-4 py-1.5 rounded-full text-xs font-semibold tracking-wide transition-all cursor-pointer flex items-center gap-1 ${ |
| activeTab === 'dashboard' ? 'bg-amber-500 text-black font-bold' : 'text-neutral-400 hover:text-white' |
| }`} |
| > |
| <AudioLines className="h-3.2 w-3.2" /> |
| Reproductor en Vivo |
| </button> |
| <button |
| onClick={() => setActiveTab('console')} |
| className={`px-4 py-1.5 rounded-full text-xs font-semibold tracking-wide transition-all cursor-pointer flex items-center gap-1 ${ |
| activeTab === 'console' ? 'bg-amber-500 text-black font-bold' : 'text-neutral-400 hover:text-white' |
| }`} |
| > |
| <Terminal className="h-3.2 w-3.2" /> |
| Simulador de Chat |
| </button> |
| </div> |
| |
| {/* Gateway Launch Controls */} |
| <div className="flex items-center gap-3"> |
| <button |
| onClick={() => setShowInviteModal(true)} |
| className="px-4 py-1.5 text-xs font-bold rounded-lg shadow-lg bg-amber-500 hover:bg-amber-600 text-black transition-all cursor-pointer inline-flex items-center gap-1.5 gold-glow" |
| > |
| <Plus className="h-3.5 w-3.5" /> |
| Agregar a Discord |
| </button> |
| |
| <button |
| onClick={() => setActiveTab('dashboard')} |
| className="px-3.5 py-1.5 text-xs font-bold rounded-lg bg-neutral-900 hover:bg-neutral-800 text-[#f4f4f5] transition-all cursor-pointer border border-white/5 flex md:hidden items-center" |
| title="Reproductor en Vivo" |
| > |
| Reproductor |
| </button> |
| </div> |
| </nav> |
| |
| {/* Main Container Content */} |
| <main className="flex-1 w-full flex flex-col z-10"> |
| |
| {/* ======================================= */} |
| {/* TAB 1: LANDING OVERVIEW PAGE */} |
| {/* ======================================= */} |
| {activeTab === 'home' && ( |
| <div className="flex flex-col w-full max-w-7xl mx-auto px-4 sm:px-8 py-8 sm:py-16 space-y-16"> |
| |
| {/* Hero Splash Typography */} |
| <div className="text-center max-w-3xl mx-auto space-y-6"> |
| <div className="inline-flex items-center gap-1.5 bg-amber-500/10 border border-amber-500/30 rounded-full px-3 py-1 text-xs text-amber-300 font-mono tracking-wide"> |
| <Sparkles className="h-3 w-3 text-amber-400" /> |
| Motor de Audio de Alta Fidelidad para Discord |
| </div> |
| |
| <h1 className="text-4xl sm:text-6xl font-extrabold tracking-tight leading-tight text-white"> |
| Música Sin Límites en <br className="hidden sm:inline" /> |
| <span className="text-transparent bg-clip-text bg-gradient-to-r from-amber-300 via-yellow-450 to-amber-500 font-mono"> |
| tu Servidor de Discord |
| </span> |
| </h1> |
| |
| <p className="text-sm sm:text-base text-neutral-400 leading-relaxed max-w-2xl mx-auto"> |
| Conecta a tu comunidad con <strong>Glizh</strong>. Transmisión estéreo sin pérdidas (320kbps), ecualizadores premium, filtros dinámicos, control inteligente por comandos y listas curadas por IA con Gemini. |
| </p> |
| |
| <div className="flex flex-wrap items-center justify-center gap-4 pt-3"> |
| <button |
| onClick={() => setShowInviteModal(true)} |
| className="px-8 py-3 bg-amber-500 hover:bg-amber-600 text-black font-bold text-sm rounded-xl transform hover:scale-[1.03] transition-all shadow-lg shadow-amber-500/25 cursor-pointer flex items-center gap-2 gold-glow" |
| > |
| <Plus className="h-4 w-4" /> |
| Invitar a Glizh Bot |
| </button> |
| <button |
| onClick={() => setActiveTab('dashboard')} |
| className="px-8 py-3 bg-neutral-950 border border-white/5 hover:bg-neutral-900 text-[#efefe1] font-bold text-sm rounded-xl transition-all cursor-pointer flex items-center gap-2" |
| > |
| <AudioLines className="h-4 w-4 text-amber-400" /> |
| Abrir Reproductor |
| </button> |
| <button |
| onClick={() => setActiveTab('console')} |
| className="px-5 py-3 bg-neutral-950 hover:bg-neutral-900 border border-white/5 text-neutral-400 hover:text-white font-mono text-xs rounded-xl transition-all cursor-pointer flex items-center gap-1.5" |
| > |
| <Terminal className="h-3.5 w-3.5 text-amber-400" /> |
| Simulador de Comandos |
| </button> |
| </div> |
| </div> |
| |
| {/* Immersive Preview Showcase */} |
| <div className="w-full rounded-2xl border border-white/5 p-4 sm:p-5 bg-gradient-to-b from-[#101012] to-[#08080a] shadow-2xl relative group overflow-hidden gold-border-glow"> |
| <div className="absolute top-2 left-6 text-[10px] text-amber-500/60 font-mono tracking-widest flex items-center gap-1"> |
| <span className="h-2 w-2 rounded-full bg-amber-500 animate-ping inline-block" /> |
| VISTA PREVIA DEL REPRODUCTOR EN VIVO DE GLIZH |
| </div> |
| |
| <div className="grid grid-cols-1 lg:grid-cols-12 gap-5 mt-4"> |
| |
| {/* Embedded Mini Player panel */} |
| <div className="col-span-1 lg:col-span-5 bg-black/40 border border-white/5 rounded-xl p-5 flex flex-col justify-between space-y-4"> |
| <div className="flex items-center justify-between"> |
| <span className="text-[10px] bg-amber-500/10 text-amber-300 font-mono py-0.5 px-2.5 rounded-full border border-amber-500/20 uppercase tracking-widest"> |
| Sonando ahora |
| </span> |
| <div className="flex gap-1"> |
| <span className="h-3.5 w-1 bg-amber-450 rounded-full eq-bar-1" /> |
| <span className="h-3.5 w-1 bg-yellow-450 rounded-full eq-bar-2" /> |
| <span className="h-3.5 w-1 bg-amber-500 rounded-full eq-bar-3" /> |
| <span className="h-3.5 w-1 bg-yellow-500 rounded-full eq-bar-4" /> |
| </div> |
| </div> |
| |
| <div className="flex items-center gap-4"> |
| <div className="w-14 h-14 rounded-full bg-gradient-to-tr from-amber-400 via-yellow-500 to-amber-600 flex items-center justify-center animate-spin" style={{ animationDuration: '8s' }}> |
| <Disc className="h-7 w-7 text-black/90" /> |
| </div> |
| <div> |
| <h4 className="text-sm font-bold text-white tracking-wide">{activeTrack?.title || 'Provenza'}</h4> |
| <p className="text-xs text-neutral-400">{activeTrack?.artist || 'KAROL G'}</p> |
| <span className="text-[9px] uppercase font-mono tracking-wider text-neutral-500">Solicitado por: @cristophergamer</span> |
| </div> |
| </div> |
| |
| <div className="space-y-1"> |
| <div className="flex justify-between text-[10px] font-mono text-neutral-400"> |
| <span>{formatTime(playProgress)}</span> |
| <span>{activeTrack?.duration || '03:27'}</span> |
| </div> |
| <div className="w-full bg-white/5 rounded-full h-1 relative overflow-hidden"> |
| <div className="bg-amber-500 h-full rounded-full transition-all" style={{ width: '30%' }} /> |
| </div> |
| </div> |
| |
| <div className="flex justify-between items-center bg-white/2 p-2 px-3.5 rounded-lg border border-white/5"> |
| <div className="flex items-center gap-1.5"> |
| <Volume2 className="h-4 w-4 text-amber-400" /> |
| <span className="text-[10px] font-mono">Vol: {volume}%</span> |
| </div> |
| <div className="h-4 w-px bg-white/10" /> |
| <span className="text-[10px] font-mono uppercase text-amber-400">🟢 Link: Voice General</span> |
| </div> |
| </div> |
| |
| {/* Embedded Mini Queue listing */} |
| <div className="col-span-1 lg:col-span-7 bg-black/40 border border-white/5 rounded-xl p-5 flex flex-col justify-between space-y-4"> |
| <div> |
| <h4 className="text-xs font-semibold uppercase tracking-wider text-neutral-300 font-mono mb-2">Lista de reproducción entrante</h4> |
| <div className="space-y-1.5 max-h-[140px] overflow-y-auto pr-2"> |
| {songQueue.slice(0, 3).map((t, index) => ( |
| <div key={t.id} className="flex justify-between items-center text-xs p-2 rounded-lg bg-neutral-950 border border-white/5"> |
| <div className="flex items-center gap-2"> |
| <span className="text-[10px] uppercase font-mono text-amber-500">[{index + 1}]</span> |
| <span className="font-medium text-neutral-200 truncate">{t.title}</span> |
| </div> |
| <span className="text-[10px] font-mono text-neutral-500 whitespace-nowrap">{t.duration}</span> |
| </div> |
| ))} |
| </div> |
| </div> |
| |
| <div className="flex items-center justify-between border-t border-white/5 pt-3"> |
| <span className="text-[10px] text-neutral-400">¿Quieres ver la cola completa o cambiar de canción?</span> |
| <button |
| onClick={() => setActiveTab('dashboard')} |
| className="text-xs text-black font-bold py-1 px-3 bg-amber-500 hover:bg-amber-600 rounded-md border border-amber-500/20 cursor-pointer inline-flex items-center gap-1" |
| > |
| Reproductor Activo |
| <ChevronRight className="h-3 w-3" /> |
| </button> |
| </div> |
| </div> |
| |
| </div> |
| </div> |
| |
| {/* Simulated Live Statistics */} |
| <div className="grid grid-cols-2 md:grid-cols-4 gap-4 sm:gap-6"> |
| <div className="p-5 rounded-xl border border-white/5 bg-[#0e0e11] flex flex-col space-y-1 relative overflow-hidden group"> |
| <div className="absolute top-0 left-0 w-1 h-full bg-amber-500 opacity-0 group-hover:opacity-100 transition-all" /> |
| <span className="text-xs font-mono uppercase tracking-widest text-neutral-500">Servidores Activos</span> |
| <span className="text-2xl sm:text-3xl font-extrabold tracking-tight text-white">{botStats.guilds.toLocaleString()}</span> |
| <p className="text-[10px] text-neutral-400">Comunidades de Discord activas</p> |
| </div> |
| |
| <div className="p-5 rounded-xl border border-white/5 bg-[#0e0e11] flex flex-col space-y-1 relative overflow-hidden group"> |
| <div className="absolute top-0 left-0 w-1 h-full bg-yellow-500 opacity-0 group-hover:opacity-100 transition-all" /> |
| <span className="text-xs font-mono uppercase tracking-widest text-neutral-500">Oyentes</span> |
| <span className="text-2xl sm:text-3xl font-extrabold tracking-tight text-white">{(botStats.users / 1000).toFixed(0)}k+</span> |
| <p className="text-[10px] text-neutral-400">Usuarios únicos en streaming hoy</p> |
| </div> |
| |
| <div className="p-5 rounded-xl border border-white/5 bg-[#0e0e11] flex flex-col space-y-1 relative overflow-hidden group"> |
| <div className="absolute top-0 left-0 w-1 h-full bg-amber-600 opacity-0 group-hover:opacity-100 transition-all" /> |
| <span className="text-xs font-mono uppercase tracking-widest text-neutral-500">Latencia</span> |
| <span className="text-2xl sm:text-3xl font-extrabold tracking-tight text-amber-400">{botStats.ping}ms</span> |
| <p className="text-[10px] text-neutral-400">Excelente conexión y carga de buffer</p> |
| </div> |
| |
| <div className="p-5 rounded-xl border border-white/5 bg-[#0e0e11] flex flex-col space-y-1 relative overflow-hidden group"> |
| <div className="absolute top-0 left-0 w-1 h-full bg-yellow-600 opacity-0 group-hover:opacity-100 transition-all" /> |
| <span className="text-xs font-mono uppercase tracking-widest text-neutral-500">Uptime</span> |
| <span className="text-2xl sm:text-3xl font-extrabold tracking-tight text-white">{botStats.uptime}</span> |
| <p className="text-[10px] text-neutral-400">Estabilidad garantizada 24/7</p> |
| </div> |
| </div> |
| |
| {/* Highlight Feature Bento grid */} |
| <div className="space-y-6"> |
| <div className="text-center"> |
| <h3 className="text-xl sm:text-3xl font-bold tracking-tight text-white">Diseñado Para La Excelencia Auditiva</h3> |
| <p className="text-xs sm:text-sm text-neutral-400 max-w-lg mx-auto mt-1">Glizh elimina los saltos de audio y el lag para ofrecer una experiencia musical de primera clase.</p> |
| </div> |
| |
| <div className="grid grid-cols-1 md:grid-cols-3 gap-6"> |
| <div className="p-6 rounded-xl border border-white/5 bg-neutral-900/60 hover:bg-neutral-900 transition-all flex flex-col space-y-3"> |
| <div className="p-2.5 bg-amber-500/10 border border-amber-500/20 rounded-lg text-amber-400 w-fit"> |
| <AudioLines className="h-5 w-5" /> |
| </div> |
| <h4 className="text-sm font-bold text-white tracking-wide">Ecualizador Premium & Bassboost</h4> |
| <p className="text-xs text-neutral-400 leading-relaxed"> |
| Ajusta los bajos de forma masiva o aplica filtros dinámicos (Vaporwave, Nightcore, Lo-Fi) en tiempo real con comandos sencillos. |
| </p> |
| </div> |
| |
| <div className="p-6 rounded-xl border border-white/5 bg-neutral-900/60 hover:bg-neutral-900 transition-all flex flex-col space-y-3"> |
| <div className="p-2.5 bg-yellow-500/10 border border-yellow-500/20 rounded-lg text-yellow-405 w-fit"> |
| <Zap className="h-5 w-5" /> |
| </div> |
| <h4 className="text-sm font-bold text-white tracking-wide">Audio Lossless a 320kbps</h4> |
| <p className="text-xs text-neutral-400 leading-relaxed"> |
| Transmisión de sonido sin pérdidas directamente a tus canales de voz, esquivando los filtros normales de compresión de Discord. |
| </p> |
| </div> |
| |
| <div className="p-6 rounded-xl border border-white/5 bg-neutral-900/60 hover:bg-neutral-900 transition-all flex flex-col space-y-3"> |
| <div className="p-2.5 bg-amber-600/10 border border-amber-600/20 rounded-lg text-amber-400 w-fit"> |
| <Wand2 className="h-5 w-5" /> |
| </div> |
| <h4 className="text-sm font-bold text-white tracking-wide">Co-Piloto Inteligente Gemini</h4> |
| <p className="text-xs text-neutral-400 leading-relaxed"> |
| Usa lenguaje natural directamente en el simulador para que Gemini AI genere y ponga en cola playlists personalizadas según tu estado de ánimo. |
| </p> |
| </div> |
| </div> |
| </div> |
| |
| {/* Immersive Guild Switch Mock Selector */} |
| <div className="p-6 rounded-xl bg-gradient-to-tr from-[#0f0e11] to-[#08080a] border border-white/5 flex flex-col sm:flex-row items-center justify-between gap-4"> |
| <div className="space-y-1 text-center sm:text-left"> |
| <span className="text-[10px] uppercase tracking-widest text-amber-500 font-mono font-bold block">Conexión del simulador</span> |
| <h4 className="text-lg font-bold text-white">🔊 Servidor enlazado: <span className="text-amber-300 font-mono italic">"{guildName}"</span></h4> |
| <p className="text-xs text-neutral-400">¿Quieres enlazar el reproductor virtual a otro servidor de tu propiedad?</p> |
| </div> |
| |
| <div className="flex gap-2"> |
| <button |
| onClick={() => setShowInviteModal(true)} |
| className="px-5 py-2 rounded-lg bg-neutral-800 hover:bg-neutral-700 text-xs font-bold text-[#f4f4f5] border border-white/5 transition-all cursor-pointer inline-flex items-center gap-1" |
| > |
| Configurar Servidores |
| </button> |
| <button |
| onClick={() => { |
| const next = prompt("Ingresa el nombre del nuevo servidor para simular conexión:", guildName); |
| if (next) setGuildName(next); |
| }} |
| className="px-5 py-2 rounded-lg bg-amber-500 hover:bg-amber-600 text-xs font-bold text-black transition-all cursor-pointer font-bold" |
| > |
| Cambiar Nombre |
| </button> |
| </div> |
| </div> |
| |
| </div> |
| )} |
| |
| {/* ======================================= */} |
| {/* TAB 2: INTERACTIVE WEB MUSIC DASHBOARD */} |
| {/* ======================================= */} |
| {activeTab === 'dashboard' && ( |
| <div className="flex-1 w-full max-w-7xl mx-auto px-4 sm:px-8 py-6 grid grid-cols-1 lg:grid-cols-12 gap-6 relative"> |
| |
| {/* Left Column: Player controls & Active Visualizers */} |
| <div className="lg:col-span-7 flex flex-col space-y-6"> |
| |
| {/* High-Fi Music Master Player Card */} |
| <div className="rounded-2xl border border-white/5 p-6 bg-[#0e0e11] space-y-6 relative overflow-hidden gold-border-glow"> |
| <div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 w-48 h-48 rounded-full bg-amber-500/5 blur-3xl pointer-events-none" /> |
| |
| <div className="flex items-center justify-between relative z-10"> |
| <div className="flex items-center gap-2"> |
| <span className="h-2 w-2 rounded-full bg-amber-500 animate-ping" /> |
| <span className="text-[10px] font-mono uppercase tracking-widest text-neutral-400"> |
| Canal de Voz Conectado #01 |
| </span> |
| </div> |
| |
| <div className="flex items-center gap-2"> |
| <span className={`text-[9px] uppercase font-mono tracking-wider px-2 py-0.5 rounded border ${ |
| bassboostMode === 'Off' ? 'bg-white/5 text-neutral-500 border-white/5' : 'bg-amber-500/10 text-amber-400 border-amber-500/20' |
| }`}> |
| Bajos: {bassboostMode} |
| </span> |
| <span className={`text-[9px] uppercase font-mono tracking-wider px-2 py-0.5 rounded border ${ |
| activeFilter === 'None' ? 'bg-white/5 text-neutral-500 border-white/5' : 'bg-yellow-500/10 text-yellow-450 border-yellow-500/20' |
| }`}> |
| Filtro: {activeFilter} |
| </span> |
| </div> |
| </div> |
| |
| {/* Disc and track core info */} |
| <div className="flex flex-col sm:flex-row items-center gap-6 relative z-10"> |
| <div className="relative shrink-0"> |
| <div className={`w-28 h-28 rounded-full bg-gradient-to-tr from-amber-400 via-yellow-500 to-amber-600 flex items-center justify-center shadow-lg border-4 border-black/40 ${ |
| isPlaying ? 'animate-spin' : '' |
| }`} style={{ animationDuration: '6s' }}> |
| <Disc className="h-12 w-12 text-black/90" /> |
| </div> |
| {isPlaying && ( |
| <div className="absolute inset-0 rounded-full border-2 border-dashed border-amber-500/30 animate-ping pointer-events-none" style={{ animationDuration: '3s' }} /> |
| )} |
| </div> |
| |
| <div className="flex-1 text-center sm:text-left space-y-1 min-w-0"> |
| <span className="text-[10px] uppercase font-mono bg-amber-500/10 py-0.5 px-2.5 rounded text-amber-300 border border-amber-500/20 w-fit block mx-auto sm:mx-0"> |
| {activeTrack?.genre || 'Música Latina'} |
| </span> |
| <h3 className="text-xl sm:text-2xl font-black text-white tracking-wide truncate"> |
| {activeTrack ? activeTrack.title : 'Glizh Desconectado'} |
| </h3> |
| <p className="text-sm text-neutral-400"> |
| de <strong className="text-neutral-300 font-medium">{activeTrack ? activeTrack.artist : 'Carga pistas para empezar'}</strong> |
| </p> |
| {activeTrack?.requester && ( |
| <p className="text-[10px] text-neutral-500 font-mono uppercase"> |
| Solicitado por: <span className="text-neutral-400">@{activeTrack.requester}</span> |
| </p> |
| )} |
| </div> |
| </div> |
| |
| {/* Timeline Progress Bar controls */} |
| <div className="space-y-2 relative z-10"> |
| <div className="w-full relative"> |
| <input |
| type="range" |
| min="0" |
| max={activeTrack ? (Number(activeTrack.duration.split(':')[0]) * 60 + Number(activeTrack.duration.split(':')[1])) : 200} |
| value={playProgress} |
| onChange={(e) => setPlayProgress(Number(e.target.value))} |
| className="w-full h-1 bg-white/5 rounded-lg appearance-none cursor-pointer accent-amber-500 outline-none" |
| /> |
| </div> |
| <div className="flex justify-between text-xs font-mono text-neutral-400"> |
| <span>{formatTime(playProgress)}</span> |
| <span>{activeTrack ? activeTrack.duration : '00:00'}</span> |
| </div> |
| </div> |
| |
| {/* Primary Music Controls Buttons Row */} |
| <div className="flex flex-col sm:flex-row items-center justify-between gap-4 border-t border-white/5 pt-5 relative z-10"> |
| <div className="flex items-center gap-3"> |
| <button |
| onClick={handleSkipBack} |
| className="p-2.5 rounded-lg bg-neutral-900 hover:bg-neutral-800 text-white transition-all cursor-pointer" |
| title="Canción anterior" |
| > |
| <SkipBack className="h-4.5 w-4.5 text-amber-450" /> |
| </button> |
| |
| <button |
| onClick={handleTogglePlay} |
| className="p-4 rounded-full bg-amber-500 hover:bg-amber-600 text-black shadow-md transform hover:scale-[1.05] transition-all cursor-pointer gold-glow" |
| title={isPlaying ? 'Pausar' : 'Reanudar'} |
| > |
| {isPlaying ? <Pause className="h-6 w-6" /> : <Play className="h-6 w-6" />} |
| </button> |
| |
| <button |
| onClick={handleSkipForward} |
| className="p-2.5 rounded-lg bg-neutral-900 hover:bg-neutral-800 text-white transition-all cursor-pointer" |
| title="Siguiente canción" |
| > |
| <SkipForward className="h-4.5 w-4.5 text-amber-450" /> |
| </button> |
| </div> |
| |
| {/* Volume Slider scale controls */} |
| <div className="flex items-center gap-3 w-full sm:w-auto shrink-0 bg-white/3 p-2 px-3 rounded-lg border border-white/5"> |
| <button |
| onClick={() => setVolume(prev => prev === 0 ? 90 : 0)} |
| className="text-neutral-400 hover:text-white" |
| > |
| {volume === 0 ? <VolumeX className="h-4 w-4" /> : volume < 50 ? <Volume1 className="h-4 w-4" /> : <Volume2 className="h-4 w-4" />} |
| </button> |
| <span className="text-xs font-mono text-neutral-300 w-8 text-right shrink-0">{volume}%</span> |
| <input |
| type="range" |
| min="0" |
| max="150" |
| value={volume} |
| onChange={(e) => setVolume(Number(e.target.value))} |
| className="w-24 h-1 bg-white/10 rounded-lg appearance-none cursor-pointer accent-amber-500 outline-none" |
| /> |
| </div> |
| </div> |
| |
| {/* Animated EQ Bars Gold */} |
| <div className="flex items-center gap-1 absolute bottom-4 right-6 pointer-events-none opacity-40"> |
| <span className={`w-1 bg-amber-500 rounded-full ${isPlaying ? 'h-5 eq-bar-1' : 'h-1 transition-all'}`} /> |
| <span className={`w-1 bg-yellow-400 rounded-full ${isPlaying ? 'h-8 eq-bar-2' : 'h-1 transition-all'}`} style={{ animationDelay: '0.2s' }} /> |
| <span className={`w-1 bg-amber-600 rounded-full ${isPlaying ? 'h-6 eq-bar-3' : 'h-1 transition-all'}`} style={{ animationDelay: '0.4s' }} /> |
| <span className={`w-1 bg-yellow-500 rounded-full ${isPlaying ? 'h-7 eq-bar-4' : 'h-1 transition-all'}`} style={{ animationDelay: '0.1s' }} /> |
| <span className={`w-1 bg-amber-400 rounded-full ${isPlaying ? 'h-4 eq-bar-1' : 'h-1 transition-all'}`} style={{ animationDelay: '0.5s' }} /> |
| </div> |
| </div> |
| |
| {/* Stateful EQ Filter Tuner deck */} |
| <div className="rounded-2xl border border-white/5 p-5 bg-[#0e0e11] space-y-4"> |
| <div className="flex items-center justify-between"> |
| <h4 className="text-xs font-bold font-mono tracking-widest text-[#a1a1aa] uppercase flex items-center gap-1.5"> |
| <Sliders className="h-3.8 w-3.8 text-amber-400" /> |
| Filtros y Ecualización DSP del Canal |
| </h4> |
| <span className="text-[10px] text-neutral-500">Ajustes instantáneos</span> |
| </div> |
| |
| <div className="grid grid-cols-1 sm:grid-cols-2 gap-4"> |
| {/* Bassboost tuner */} |
| <div className="p-3 bg-black/30 rounded-xl border border-white/5 space-y-2"> |
| <span className="text-[10px] font-semibold text-neutral-400 uppercase tracking-wide block">Realce de Bajos (Equalizer)</span> |
| <div className="grid grid-cols-3 gap-1.5"> |
| {(['Off', 'Bass+ Low', 'Glizh Heavy'] as const).map(b => ( |
| <button |
| key={b} |
| onClick={() => { |
| setBassboostMode(b); |
| triggerSFX('pad'); |
| }} |
| className={`py-1 text-[10px] font-mono rounded border capitalize transition-all cursor-pointer ${ |
| bassboostMode === b ? 'bg-amber-500/10 text-amber-400 border-amber-500/20 font-bold' : 'bg-neutral-900 text-neutral-500 border-white/5 hover:text-white' |
| }`} |
| > |
| {b.split(' ').pop()} |
| </button> |
| ))} |
| </div> |
| </div> |
| |
| {/* High pass layout */} |
| <div className="p-3 bg-black/30 rounded-xl border border-white/5 space-y-2"> |
| <span className="text-[10px] font-semibold text-neutral-400 uppercase tracking-wide block">Filtros Activos DSP</span> |
| <div className="grid grid-cols-4 gap-1"> |
| {(['None', 'Nightcore', 'Vaporwave', 'Lo-Fi Chill'] as const).map(f => ( |
| <button |
| key={f} |
| onClick={() => { |
| setActiveFilter(f); |
| triggerSFX('laser'); |
| }} |
| className={`py-1 text-[9px] font-mono rounded border capitalize transition-all cursor-pointer truncate ${ |
| activeFilter === f ? 'bg-yellow-500/10 text-yellow-450 border-yellow-500/20 font-bold' : 'bg-neutral-900 text-neutral-500 border-white/5 hover:text-white' |
| }`} |
| title={`Aplicar filtro ${f}`} |
| > |
| {f.split(' ')[0]} |
| </button> |
| ))} |
| </div> |
| </div> |
| </div> |
| </div> |
| |
| {/* Browser Interactive Soundboard (Plays actual sounds via Synthesizer API) */} |
| <div className="rounded-2xl border border-white/5 p-5 bg-[#0e0e11] space-y-4"> |
| <div className="flex justify-between items-center"> |
| <div> |
| <h4 className="text-xs font-bold font-mono tracking-widest text-[#a1a1aa] uppercase flex items-center gap-1.5"> |
| <AudioLines className="h-3.8 w-3.8 text-amber-500" /> |
| Efectos de Sonido Directos |
| </h4> |
| <p className="text-[10px] text-neutral-500 mt-1">Efectos sintetizados directamente en tu navegador</p> |
| </div> |
| <span className="text-[10px] px-2.5 py-0.5 rounded-full bg-amber-500/10 text-amber-300 font-mono uppercase text-right">Sin Latencia</span> |
| </div> |
| |
| <div className="grid grid-cols-2 sm:grid-cols-4 gap-3"> |
| <button |
| onClick={() => triggerSFX('airhorn')} |
| className="p-3.5 rounded-xl border border-amber-500/10 bg-amber-500/5 hover:bg-amber-500/10 hover:scale-[1.02] active:scale-[0.98] transition-all cursor-pointer flex flex-col items-center justify-center space-y-1" |
| > |
| <span className="text-xl">📢</span> |
| <span className="text-xs font-bold text-amber-300 font-mono">BOCINA (AIRHORN)</span> |
| </button> |
| |
| <button |
| onClick={() => triggerSFX('laser')} |
| className="p-3.5 rounded-xl border border-yellow-500/10 bg-yellow-500/5 hover:bg-yellow-500/10 hover:scale-[1.02] active:scale-[0.98] transition-all cursor-pointer flex flex-col items-center justify-center space-y-1" |
| > |
| <span className="text-xl">👾</span> |
| <span className="text-xs font-bold text-yellow-450 font-mono">LASER CIBERNÉTICO</span> |
| </button> |
| |
| <button |
| onClick={() => triggerSFX('pad')} |
| className="p-3.5 rounded-xl border border-amber-600/10 bg-amber-600/5 hover:bg-amber-600/10 hover:scale-[1.02] active:scale-[0.98] transition-all cursor-pointer flex flex-col items-center justify-center space-y-1" |
| > |
| <span className="text-xl">🎼</span> |
| <span className="text-xs font-bold text-amber-300 font-mono">CHORD SWEEP</span> |
| </button> |
| |
| <button |
| onClick={handleToggleRainAmbient} |
| className={`p-3.5 rounded-xl border transition-all cursor-pointer flex flex-col items-center justify-center space-y-1 ${ |
| isRainEnabled |
| ? 'border-yellow-550 bg-yellow-500/10 hover:scale-[1.02] active:scale-[0.98]' |
| : 'border-yellow-500/10 bg-yellow-500/5 hover:bg-yellow-500/10 hover:scale-[1.02]' |
| }`} |
| > |
| <span className="text-xl">🌧️</span> |
| <span className="text-xs font-bold text-yellow-400 font-mono">{isRainEnabled ? 'PARAR LLUVIA' : 'LLUVIA AMBIENT'}</span> |
| </button> |
| </div> |
| </div> |
| |
| </div> |
| |
| {/* Right Column: Queue Manager, Manual Add, Gemini Recommender */} |
| <div className="lg:col-span-5 flex flex-col space-y-6"> |
| |
| {/* Queue List Panel */} |
| <div className="rounded-2xl border border-white/5 bg-[#0e0e11] flex flex-col max-h-[350px] sm:max-h-[380px] overflow-hidden"> |
| <div className="p-4 border-b border-white/5 bg-[#070709] flex justify-between items-center shrink-0"> |
| <div className="flex items-center gap-1.5"> |
| <ListMusic className="h-4 w-4 text-amber-500" /> |
| <h4 className="text-xs font-bold font-mono tracking-wider text-neutral-200 uppercase"> |
| Canciones en la Cola ({songQueue.length}) |
| </h4> |
| </div> |
| <button |
| onClick={() => { |
| if (window.confirm("¿Vaciar lista de reproducción?")) { |
| setSongQueue([songQueue[currentTrackIndex]]); |
| setCurrentTrackIndex(0); |
| } |
| }} |
| className="text-[10px] text-rose-450 hover:underline cursor-pointer" |
| > |
| Limpiar Cola |
| </button> |
| </div> |
| |
| <div className="flex-1 overflow-y-auto p-4 space-y-2"> |
| {songQueue.map((t, index) => { |
| const isActive = index === currentTrackIndex; |
| return ( |
| <div |
| key={t.id} |
| onClick={() => { |
| setCurrentTrackIndex(index); |
| setPlayProgress(0); |
| }} |
| className={`p-3.5 rounded-xl border flex items-center justify-between gap-3 text-xs opacity-90 hover:opacity-100 cursor-pointer transition-all ${ |
| isActive |
| ? 'border-amber-500 bg-amber-500/10 text-white font-semibold' |
| : 'border-white/5 bg-black/20 hover:border-neutral-700' |
| }`} |
| > |
| <div className="flex items-center gap-2.5 min-w-0"> |
| <span className="font-mono text-[10px] text-amber-500"> |
| {isActive ? '🔊' : `[${index + 1}]`} |
| </span> |
| <div className="min-w-0"> |
| <h5 className="text-neutral-200 truncate">{t.title}</h5> |
| <p className="text-[10px] text-neutral-400 truncate">de {t.artist}</p> |
| </div> |
| </div> |
| |
| <div className="flex items-center gap-2 shrink-0"> |
| {t.requester && ( |
| <span className="text-[8px] bg-neutral-900 text-amber-400 font-mono px-1.5 py-0.5 rounded tracking-wide uppercase border border-amber-500/10"> |
| @{t.requester} |
| </span> |
| )} |
| <span className="text-[10px] font-mono text-neutral-500">{t.duration}</span> |
| <button |
| onClick={(e) => { |
| e.stopPropagation(); |
| handleRemoveTrack(t.id, index); |
| }} |
| className="p-1 rounded text-neutral-500 hover:text-rose-400 hover:bg-neutral-800" |
| title="Eliminar de la cola" |
| > |
| <Trash2 className="h-3.5 w-3.5" /> |
| </button> |
| </div> |
| </div> |
| ); |
| })} |
| </div> |
| </div> |
| |
| {/* Gemini AI Smart Playlist Spark (Suggest tracks) */} |
| <div className="rounded-2xl border border-white/5 p-5 bg-[#0e0e11] space-y-4"> |
| <div className="space-y-1"> |
| <h4 className="text-xs font-bold font-mono tracking-widest text-transparent bg-clip-text bg-gradient-to-r from-amber-300 via-yellow-450 to-amber-500 uppercase flex items-center gap-1.5"> |
| <Wand2 className="h-4 w-4 text-amber-400 animate-pulse" /> |
| Co-Piloto Inteligente Gemini |
| </h4> |
| <p className="text-[11px] text-neutral-400 leading-relaxed"> |
| Escribe tu vibra o estilo musical en español (ej. "canciones de Karol G bailables" o "trap latino pesado") y deja que Gemini ponga temas realistas en la cola al instante. |
| </p> |
| </div> |
| |
| <div className="space-y-3"> |
| <div className="flex gap-2"> |
| <input |
| type="text" |
| value={geminiSearchQuery} |
| onChange={(e) => setGeminiSearchQuery(e.target.value)} |
| onKeyDown={(e) => e.key === 'Enter' && handleGeminiPlaylistSuggest()} |
| placeholder="Ej: reggaeton bailable del 2020..." |
| className="flex-1 bg-black/40 text-xs px-3 py-2.5 rounded-lg border border-white/5 outline-none focus:border-amber-500/50" |
| disabled={isAiSuggesting} |
| /> |
| |
| <button |
| onClick={handleGeminiPlaylistSuggest} |
| disabled={isAiSuggesting || !geminiSearchQuery.trim()} |
| className="px-4 bg-amber-500 hover:bg-amber-600 text-black rounded-lg text-xs font-bold transition-all disabled:opacity-40 flex items-center gap-1 justify-center whitespace-nowrap cursor-pointer font-bold" |
| > |
| {isAiSuggesting ? 'Pensando...' : 'Generar Mix'} |
| </button> |
| </div> |
| |
| {isAiSuggesting && ( |
| <div className="flex items-center gap-2 p-2 bg-amber-500/5 border border-amber-500/10 rounded-lg text-[10px] text-amber-300 font-mono"> |
| <span className="h-2 w-2 rounded-full bg-amber-450 animate-ping" /> |
| <span>Generando lista de canciones mediante Gemini 3.5...</span> |
| </div> |
| )} |
| </div> |
| </div> |
| |
| {/* Queue Preset Accelerators */} |
| <div className="rounded-2xl border border-white/5 p-4 bg-[#0e0e11] space-y-3"> |
| <span className="text-[10px] font-mono uppercase tracking-widest text-neutral-400 block"> |
| Paquetes de Estilo Musical Rápido |
| </span> |
| <div className="grid grid-cols-3 gap-2"> |
| <button |
| onClick={() => loadPresetTheme('retro')} |
| className="p-2 rounded-lg border border-white/5 bg-neutral-900/60 text-amber-300 font-bold hover:text-white hover:border-amber-500 text-xs text-center cursor-pointer font-bold" |
| > |
| 🔥 Reggaeton |
| </button> |
| <button |
| onClick={() => loadPresetTheme('workout')} |
| className="p-2 rounded-lg border border-white/5 bg-neutral-900/60 text-yellow-405 font-bold hover:text-white hover:border-yellow-500 text-xs text-center cursor-pointer font-bold" |
| > |
| 💪 Trap Latino |
| </button> |
| <button |
| onClick={() => loadPresetTheme('jazz')} |
| className="p-2 rounded-lg border border-white/5 bg-neutral-900/60 text-amber-400 font-bold hover:text-white hover:border-amber-600 text-xs text-center cursor-pointer font-bold" |
| > |
| 🎷 Bachatas/Clásicos |
| </button> |
| </div> |
| </div> |
| |
| {/* Manual Add Item Form */} |
| <div className="rounded-2xl border border-white/5 p-4 bg-[#0e0e11] space-y-3"> |
| <span className="text-[10px] font-mono uppercase tracking-widest text-neutral-400 block"> |
| Agregar Canción Manualmente |
| </span> |
| <div className="grid grid-cols-2 gap-2"> |
| <input |
| type="text" |
| value={manualTitle} |
| onChange={(e) => setManualTitle(e.target.value)} |
| placeholder="Título de la canción..." |
| className="bg-black/30 text-xs px-3 py-2 rounded-md border border-white/5 outline-none focus:border-amber-400 text-white" |
| /> |
| <input |
| type="text" |
| value={manualArtist} |
| onChange={(e) => setManualArtist(e.target.value)} |
| placeholder="Nombre del artista..." |
| className="bg-black/30 text-xs px-3 py-2 rounded-md border border-white/5 outline-none focus:border-amber-400 text-white" |
| /> |
| </div> |
| <button |
| onClick={handleAddManualTrack} |
| disabled={!manualTitle.trim()} |
| className="w-full py-1.5 text-xs text-black bg-amber-500 hover:bg-amber-600 font-bold rounded-lg cursor-pointer transition-all disabled:opacity-40" |
| > |
| Cargar a la Cola |
| </button> |
| </div> |
| |
| </div> |
| </div> |
| )} |
| |
| {/* ======================================= */} |
| {/* TAB 3: INTERACTIVE DISCORD CHAT CONSOLE */} |
| {/* ======================================= */} |
| {activeTab === 'console' && ( |
| <div className="flex-1 w-full max-w-7xl mx-auto px-4 sm:px-8 py-6 grid grid-cols-1 lg:grid-cols-12 gap-6 items-stretch"> |
| |
| {/* Left Box: Active Command References */} |
| <div className="lg:col-span-4 rounded-xl border border-white/5 bg-[#0e0e11] p-5 flex flex-col space-y-4"> |
| <div> |
| <h4 className="text-xs font-bold font-mono tracking-widest text-[#a1a1aa] uppercase flex items-center gap-1"> |
| <Terminal className="h-4 w-4 text-amber-500" /> |
| Referencia de Comandos |
| </h4> |
| <p className="text-[10px] text-neutral-500 mt-1"> |
| Escríbelos en el simulador o haz clic en ellos para ejecutarlos automáticamente. |
| </p> |
| </div> |
| |
| <div className="flex-1 overflow-y-auto space-y-2.5 pr-1"> |
| {BOT_COMMANDS.map(cmd => ( |
| <div |
| key={cmd.name} |
| onClick={() => { |
| setConsoleInput(cmd.syntax); |
| triggerSFX('laser'); |
| }} |
| className="p-3 bg-neutral-950 hover:bg-neutral-900 border border-white/5 hover:border-amber-550 rounded-lg cursor-pointer transition-all space-y-1 block text-left" |
| title="Clic para pegar sintaxis" |
| > |
| <div className="flex justify-between items-center"> |
| <span className="text-xs font-mono font-bold text-white"> |
| /{cmd.name} |
| </span> |
| <span className="text-[8px] bg-amber-500/10 text-amber-300 font-mono uppercase px-1.5 rounded"> |
| {cmd.category} |
| </span> |
| </div> |
| <p className="text-[11px] text-neutral-400 leading-relaxed"> |
| {cmd.description} |
| </p> |
| <span className="text-[9px] font-mono text-neutral-500 block"> |
| Sintaxis: <code className="text-neutral-300">{cmd.syntax}</code> |
| </span> |
| </div> |
| ))} |
| </div> |
| </div> |
| |
| {/* Right Box: Elegant Immersive Discord Text Client Simulator */} |
| <div className="lg:col-span-8 rounded-xl border border-white/5 bg-[#131416] flex flex-col h-[500px] lg:h-auto overflow-hidden"> |
| |
| {/* Discord Top Header simulation bar */} |
| <div className="p-3.5 bg-[#18191c] border-b border-black/80 flex items-center justify-between"> |
| <div className="flex items-center gap-2"> |
| <span className="text-xl text-neutral-500">#</span> |
| <div> |
| <h4 className="text-xs font-bold text-white tracking-wide font-mono"> |
| glizh-chat-lounge |
| </h4> |
| <p className="text-[10px] text-neutral-400"> |
| Canal de texto del simulador conectado a **"{guildName}"** |
| </p> |
| </div> |
| </div> |
| |
| <div className="flex items-center gap-2 px-2.5 py-1 rounded bg-[#09090b]/40 text-[10px] font-mono text-amber-450 border border-white/5"> |
| <span className="h-2 w-2 rounded-full bg-amber-500 inline-block animate-pulse" /> |
| BOT EN: VOICE GENERAL |
| </div> |
| </div> |
| |
| {/* Chat archives scroll panel */} |
| <div className="flex-1 overflow-y-auto p-4 space-y-4 bg-[#18191c] text-xs leading-relaxed select-text"> |
| {discordChat.map((msg) => { |
| return ( |
| <div key={msg.id} className="flex items-start gap-3 border-b border-white/2 pb-3 last:border-0"> |
| <div className="w-9 h-9 rounded-full bg-neutral-800 flex items-center justify-center text-base shrink-0 select-none shadow"> |
| {msg.author.avatar} |
| </div> |
| |
| <div className="min-w-0 flex-1 space-y-1"> |
| <div className="flex items-center gap-2"> |
| <span className={msg.author.colorClass}> |
| {msg.author.username} |
| </span> |
| {msg.author.isBot && ( |
| <span className="text-[8px] tracking-wider uppercase font-extrabold bg-amber-500 text-black px-1 relative -top-[1.2px] rounded-sm select-none"> |
| BOT |
| </span> |
| )} |
| <span className="text-[9px] text-neutral-500"> |
| {msg.timestamp} |
| </span> |
| </div> |
| |
| {msg.content && ( |
| <p className="text-neutral-300 leading-relaxed text-[12.5px] whitespace-pre-wrap select-text"> |
| {msg.content} |
| </p> |
| )} |
| |
| {/* Discord Embed design rendering */} |
| {msg.embed && ( |
| <div className={`mt-2 p-3.5 rounded bg-[#0f0f12]/80 border-l-4 ${msg.embed.color || 'border-amber-500'} max-w-lg space-y-2 shadow-md relative group select-text`}> |
| {msg.embed.title && ( |
| <h5 className="font-bold text-white text-[13px] tracking-wide"> |
| {msg.embed.title} |
| </h5> |
| )} |
| {msg.embed.description && ( |
| <p className="text-neutral-300 text-[11.5px] leading-relaxed whitespace-pre-wrap"> |
| {msg.embed.description} |
| </p> |
| )} |
| |
| {/* Embed Fields */} |
| {msg.embed.fields && msg.embed.fields.length > 0 && ( |
| <div className="grid grid-cols-2 gap-3.5 pt-1.5 select-text"> |
| {msg.embed.fields.map((f, i) => ( |
| <div key={i} className="space-y-0.5"> |
| <span className="text-[10px] text-neutral-450 font-bold uppercase tracking-wider block font-mono"> |
| {f.name} |
| </span> |
| <span className="text-[11px] text-neutral-200"> |
| {f.value} |
| </span> |
| </div> |
| ))} |
| </div> |
| )} |
| |
| {msg.embed.footer && ( |
| <div className="border-t border-white/5 pt-2 mt-2 text-[8.5px] font-mono text-neutral-500 uppercase tracking-wider"> |
| {msg.embed.footer} |
| </div> |
| )} |
| </div> |
| )} |
| </div> |
| </div> |
| ); |
| })} |
| |
| {/* Loading typing indicator */} |
| {isConsoleGenerating && ( |
| <div className="flex items-center gap-2 text-[10px] font-mono text-neutral-400"> |
| <span className="h-1.5 w-1.5 rounded-full bg-amber-400 animate-bounce" style={{ animationDelay: '0s' }} /> |
| <span className="h-1.5 w-1.5 rounded-full bg-amber-400 animate-bounce" style={{ animationDelay: '0.2s' }} /> |
| <span className="h-1.5 w-1.5 rounded-full bg-amber-400 animate-bounce" style={{ animationDelay: '0.4s' }} /> |
| <span className="animate-pulse">Glizh está procesando el flujo de audio...</span> |
| </div> |
| )} |
| </div> |
| |
| {/* Command preset rapid triggers bar */} |
| <div className="p-2 border-t border-[#101012] bg-[#1a1b1e] flex flex-wrap gap-1.5 shrink-0 select-none"> |
| <span className="text-[9px] font-mono text-neutral-500 uppercase py-1 mr-1"> |
| Acciones rápidas: |
| </span> |
| <button |
| onClick={() => handleSendConsole('/play provenza')} |
| className="px-2.5 py-1 rounded bg-[#2f3136] hover:bg-[#393c43] text-[10px] font-bold text-neutral-250 cursor-pointer text-left font-semibold text-white" |
| > |
| ⚡ /play provenza |
| </button> |
| <button |
| onClick={() => handleSendConsole('/skip')} |
| className="px-2.5 py-1 rounded bg-[#2f3136] hover:bg-[#393c43] text-[10px] font-bold text-neutral-250 cursor-pointer text-left font-semibold text-white" |
| > |
| ⏭️ /skip |
| </button> |
| <button |
| onClick={() => handleSendConsole('/queue')} |
| className="px-2.5 py-1 rounded bg-[#2f3136] hover:bg-[#393c43] text-[10px] font-bold text-neutral-250 cursor-pointer text-left font-semibold text-white" |
| > |
| 📜 /queue |
| </button> |
| <button |
| onClick={() => handleSendConsole('/lyrics')} |
| className="px-2.5 py-1 rounded bg-[#2f3136] hover:bg-[#393c43] text-[10px] font-bold text-neutral-250 cursor-pointer text-left font-semibold text-white" |
| > |
| 🎤 /lyrics |
| </button> |
| <button |
| onClick={() => handleSendConsole('/bassboost heavy')} |
| className="px-2.5 py-1 rounded bg-[#2f3136] hover:bg-[#393c43] text-[10px] font-bold text-neutral-250 cursor-pointer text-left font-mono text-white" |
| > |
| 🔋 /bassboost heavy |
| </button> |
| </div> |
| |
| {/* AI helper questions presets */} |
| <div className="px-3.5 py-2 border-t border-white/5 bg-[#141517] flex flex-wrap gap-1.5 shrink-0 select-none items-center"> |
| <span className="text-[9px] font-mono text-amber-400 font-bold uppercase tracking-wider flex items-center gap-1 mr-1"> |
| <Sparkles className="h-3 w-3 text-amber-400" /> |
| Preguntas sugeridas: |
| </span> |
| <button |
| onClick={() => handleSendConsole('¿Qué filtros de audio puedo aplicar en el canal de voz?')} |
| className="px-2 py-1 rounded bg-amber-500/10 hover:bg-amber-500/20 text-amber-300 text-[10px] font-medium border border-amber-500/20 cursor-pointer transition-all" |
| > |
| ✨ "¿Qué filtros puedo aplicar?" |
| </button> |
| <button |
| onClick={() => handleSendConsole('¿Cómo configuro la rotación de bloque IPv6 en Lavalink?')} |
| className="px-2 py-1 rounded bg-amber-500/10 hover:bg-amber-500/20 text-amber-300 text-[10px] font-medium border border-amber-500/20 cursor-pointer transition-all" |
| > |
| 📡 "Configurar IPv6 en Lavalink?" |
| </button> |
| <button |
| onClick={() => handleSendConsole('¿Por qué tengo problemas de SocketTimeoutException con YouTube?')} |
| className="px-2 py-1 rounded bg-amber-500/10 hover:bg-amber-500/20 text-amber-300 text-[10px] font-medium border border-amber-500/20 cursor-pointer transition-all" |
| > |
| 🔗 "Resolver errores de YouTube?" |
| </button> |
| </div> |
| |
| {/* Terminal simulated Input box */} |
| <div className="p-4 bg-[#1e1f22] shrink-0"> |
| <div className="flex gap-2"> |
| <div className="flex-1 bg-[#313338] text-xs px-4 py-3 rounded-lg flex items-center justify-between border border-transparent focus-within:border-amber-500/30"> |
| <input |
| type="text" |
| value={consoleInput} |
| onChange={(e) => setConsoleInput(e.target.value)} |
| onKeyDown={(e) => e.key === 'Enter' && handleSendConsole()} |
| placeholder={`Enviar mensaje a #glizh-chat-lounge (o consulta al bot)...`} |
| className="w-full bg-transparent border-none text-[#dbdee1] outline-none placeholder-neutral-500 text-xs" |
| /> |
| </div> |
| |
| <button |
| onClick={() => handleSendConsole()} |
| disabled={!consoleInput.trim() || isConsoleGenerating} |
| className="px-4 bg-amber-500 hover:bg-amber-600 font-bold text-xs text-black rounded-lg flex items-center justify-center transition-all cursor-pointer disabled:opacity-40 font-bold" |
| > |
| <Send className="h-4 w-4" /> |
| </button> |
| </div> |
| </div> |
| |
| </div> |
| |
| </div> |
| )} |
| |
| </main> |
| |
| {/* ======================================= */} |
| {/* IMMERSIVE GATEWAY AUTHORIZATION DIALOG */} |
| {/* ======================================= */} |
| {showInviteModal && ( |
| <div className="fixed inset-0 bg-black/85 backdrop-blur-sm flex items-center justify-center p-4 z-50"> |
| <div className="w-full max-w-md rounded-xl bg-[#1e1f22] text-[#f4f4f5] border border-white/5 overflow-hidden shadow-2xl relative gold-border-glow"> |
| |
| {/* Top auth aesthetic header strip */} |
| <div className="p-5 text-center bg-gradient-to-r from-amber-600 to-yellow-600 relative"> |
| <button |
| onClick={() => setShowInviteModal(false)} |
| className="absolute top-4 right-4 p-1 rounded-md text-black/50 hover:text-black" |
| > |
| <LogOut className="h-4.5 w-4.5" /> |
| </button> |
| |
| <div className="w-14 h-14 rounded-full bg-black/60 mx-auto flex items-center justify-center text-2xl border-2 border-white/20 select-none shadow shadow-amber-500/10"> |
| 👑 |
| </div> |
| <h3 className="text-sm font-bold uppercase tracking-wider text-black mt-2 font-bold"> |
| Glizh Bot Shard Gateway |
| </h3> |
| <p className="text-[10px] text-neutral-800 font-medium"> |
| Autoriza la conexión de Glizh a tus servidores |
| </p> |
| </div> |
| |
| {/* Simulated authorization checklists */} |
| <div className="p-6 space-y-4"> |
| <div className="rounded-lg bg-[#2b2d31] p-3 text-xs space-y-2"> |
| <span className="text-[9px] font-mono text-[#a1a1aa] uppercase font-bold block"> |
| Seleccionar Servidor de Destino |
| </span> |
| |
| <input |
| type="text" |
| value={inviteServerName} |
| onChange={(e) => setInviteServerName(e.target.value)} |
| placeholder="e.g. Mi Servidor Dorado" |
| className="w-full bg-[#1e1f22] border border-white/5 p-2 px-3 text-xs text-[#efefe1] outline-none rounded focus:border-amber-500" |
| /> |
| </div> |
| |
| <div className="space-y-2.5 text-xs"> |
| <span className="text-[9px] font-mono text-[#a1a1aa] font-bold uppercase block"> |
| Permisos Requeridos |
| </span> |
| |
| <div className="space-y-2 divide-y divide-white/5"> |
| <div className="flex items-center gap-2 pt-2 first:pt-0"> |
| <ShieldCheck className="h-4 w-4 text-amber-400 shrink-0" /> |
| <div> |
| <span className="font-bold text-white block">Crear comandos de barra /</span> |
| <p className="text-[10px] text-neutral-400">Permite controlar colas de reproducción desde texto</p> |
| </div> |
| </div> |
| |
| <div className="flex items-center gap-2 pt-2"> |
| <CheckCircle className="h-4 w-4 text-amber-400 shrink-0" /> |
| <div> |
| <span className="font-bold text-white block">Unirse a canales de voz</span> |
| <p className="text-[10px] text-neutral-400">Transmisión lossless directa de 320kbps</p> |
| </div> |
| </div> |
| |
| <div className="flex items-center gap-2 pt-2"> |
| <CheckCircle className="h-4 w-4 text-amber-400 shrink-0" /> |
| <div> |
| <span className="font-bold text-white block">Ecualización de audio</span> |
| <p className="text-[10px] text-neutral-400">Modifica filtros graves o efectos Nightcore en vivo</p> |
| </div> |
| </div> |
| </div> |
| </div> |
| |
| {/* Submit simulation */} |
| <div className="pt-4 flex gap-3"> |
| <button |
| onClick={() => setShowInviteModal(false)} |
| className="flex-1 py-2.5 rounded-lg bg-neutral-800 hover:bg-neutral-750 font-bold text-xs text-[#a1a1aa] hover:text-white" |
| > |
| Cancelar |
| </button> |
| <button |
| onClick={handleSimulateInviteSubmit} |
| disabled={inviteCompleted} |
| className="flex-1 py-2.5 rounded-lg bg-amber-500 hover:bg-amber-600 font-bold text-xs text-black" |
| > |
| {inviteCompleted ? 'Enlazando Shards...' : 'Autorizar Enlace'} |
| </button> |
| </div> |
| |
| </div> |
| |
| </div> |
| </div> |
| )} |
| |
| {/* Botón flotante del Asistente de IA Glizh */} |
| <div className="fixed bottom-6 right-6 z-50 flex flex-col items-end"> |
| {isAssistantOpen && ( |
| <div className="mb-4 w-80 sm:w-96 h-[450px] bg-[#0c0c0e]/95 backdrop-blur-md rounded-2xl border border-amber-500/35 shadow-2xl flex flex-col overflow-hidden gold-glow animate-in fade-in slide-in-from-bottom-5 duration-200"> |
| {/* Cabecera */} |
| <div className="p-4 bg-gradient-to-r from-amber-500 to-yellow-600 flex justify-between items-center text-black shrink-0 font-bold"> |
| <div className="flex items-center gap-2"> |
| <div className="p-1.5 bg-black/10 rounded-lg"> |
| <Sparkles className="h-4 w-4 text-black animate-pulse" /> |
| </div> |
| <div> |
| <h4 className="font-bold text-xs uppercase tracking-wider text-black">Asistente Glizh AI</h4> |
| <span className="text-[9px] text-neutral-800 flex items-center gap-1 font-semibold"> |
| <span className="h-1.5 w-1.5 rounded-full bg-emerald-500 animate-ping inline-block" /> En línea |
| </span> |
| </div> |
| </div> |
| <button |
| onClick={() => setIsAssistantOpen(false)} |
| className="p-1 rounded-md hover:bg-black/10 text-black transition-all cursor-pointer" |
| > |
| <X className="h-4 w-4" /> |
| </button> |
| </div> |
| |
| {/* Mensajes */} |
| <div className="flex-1 overflow-y-auto p-4 space-y-3"> |
| {assistantHistory.map((msg, index) => ( |
| <div |
| key={index} |
| className={`flex ${msg.sender === 'user' ? 'justify-end' : 'justify-start'}`} |
| > |
| <div |
| className={`max-w-[80%] rounded-xl p-3 text-xs leading-relaxed ${ |
| msg.sender === 'user' |
| ? 'bg-amber-500 text-black font-semibold rounded-tr-none' |
| : 'bg-neutral-900 text-neutral-200 rounded-tl-none border border-white/5' |
| }`} |
| > |
| {msg.text} |
| </div> |
| </div> |
| ))} |
| {isAssistantLoading && ( |
| <div className="flex justify-start"> |
| <div className="bg-neutral-900 border border-white/5 rounded-xl rounded-tl-none p-3 text-xs flex items-center gap-1.5 text-neutral-400"> |
| <span className="h-1.5 w-1.5 rounded-full bg-amber-450 animate-bounce" style={{ animationDelay: '0s' }} /> |
| <span className="h-1.5 w-1.5 rounded-full bg-amber-450 animate-bounce" style={{ animationDelay: '0.2s' }} /> |
| <span className="h-1.5 w-1.5 rounded-full bg-amber-450 animate-bounce" style={{ animationDelay: '0.4s' }} /> |
| <span>Glizh está pensando...</span> |
| </div> |
| </div> |
| )} |
| <div ref={assistantEndRef} /> |
| </div> |
| |
| {/* Input */} |
| <div className="p-3 bg-[#121214] border-t border-white/5 shrink-0"> |
| <div className="flex gap-2"> |
| <input |
| type="text" |
| value={assistantInput} |
| onChange={(e) => setAssistantInput(e.target.value)} |
| onKeyDown={(e) => e.key === 'Enter' && handleSendAssistant()} |
| placeholder="Pregúntale a Glizh AI..." |
| className="flex-1 bg-black/40 text-xs px-3 py-2.5 rounded-lg border border-white/5 outline-none focus:border-amber-500/50 text-white placeholder-neutral-500" |
| /> |
| <button |
| onClick={handleSendAssistant} |
| disabled={!assistantInput.trim() || isAssistantLoading} |
| className="p-2.5 bg-amber-500 hover:bg-amber-600 disabled:opacity-40 text-black rounded-lg transition-all cursor-pointer flex items-center justify-center shrink-0" |
| > |
| <Send className="h-3.5 w-3.5 text-black font-bold" /> |
| </button> |
| </div> |
| </div> |
| </div> |
| )} |
| |
| <button |
| onClick={() => setIsAssistantOpen(!isAssistantOpen)} |
| className="p-3.5 rounded-full bg-gradient-to-tr from-amber-500 to-yellow-600 hover:from-amber-600 hover:to-yellow-700 text-black shadow-lg hover:shadow-amber-500/20 transform hover:scale-105 active:scale-95 transition-all cursor-pointer flex items-center justify-center relative gold-glow" |
| > |
| {isAssistantOpen ? <X className="h-6 w-6 text-black font-bold" /> : <MessageSquare className="h-6 w-6 text-black font-bold" />} |
| {!isAssistantOpen && ( |
| <span className="absolute -top-1 -right-1 flex h-3 w-3"> |
| <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-amber-400 opacity-75"></span> |
| <span className="relative inline-flex rounded-full h-3 w-3 bg-amber-500"></span> |
| </span> |
| )} |
| </button> |
| </div> |
| |
| {/* Futuristic clean footer */} |
| <footer className="w-full py-8 border-t border-white/5 bg-[#070709] text-center text-xs text-neutral-500 font-mono tracking-wide"> |
| <div className="max-w-7xl mx-auto px-4 space-y-2"> |
| <div className="flex items-center justify-center gap-2 text-neutral-400"> |
| <span>Glizh Music Inc</span> |
| <span>•</span> |
| <span className="text-amber-500 font-semibold">Socio Certificado de Discord</span> |
| </div> |
| <p className="text-[10px] text-neutral-600"> |
| Esta página de inicio y simulador del bot está enlazada con Gemini 3.5. |
| </p> |
| </div> |
| </footer> |
| |
| </div> |
| ); |
| } |
|
|