CVision-assitant's picture
Update modules/interface.py
7e06093 verified
Raw History Blame Contribute Delete
69.9 kB
# =====================================================================
# SECCIΓ“N 1: IMPORTACIONES ESTÁNDAR Y DE SISTEMA
# =====================================================================
import gradio as gr
import re
import os
# =====================================================================
# SECCIΓ“N 2: MΓ“DULOS DE AUDIO Y TRANSCRIPCIΓ“N (CVISION)
# =====================================================================
from modules.listen import transcribe_audio_file
from modules.talker.talk import speak
# =====================================================================
# SECCIΓ“N 3: ACOPLAMIENTO DEL MΓ“DULO DE VOZ MODULAR (DESDE 0)
# =====================================================================
from modules.langchain_assistant.voice_module import create_voice_tab
# ════════════════════════════════════════════════════════
# FALLBACK CHAT β€” MΓ³dulos de respaldo si falla la importaciΓ³n
# ════════════════════════════════════════════════════════
def _fallback_chat(history, message, *args, **kwargs):
yield (history or []) + [[message, "⚠️ Módulo no disponible. Revisa los logs del servidor."]], None
try:
from modules.utils.chat_utils import (
productivity_chat,
browser_chat,
scientist_chat,
regular_chat,
document_chat,
)
print("βœ… MΓ³dulos de chat importados correctamente")
except Exception as e:
print(f"❌ Error importando módulos de chat: {e}")
regular_chat = _fallback_chat
browser_chat = _fallback_chat
productivity_chat = _fallback_chat
scientist_chat = _fallback_chat
document_chat = _fallback_chat
# ════════════════════════════════════════════════════════
# CONSTANTES GLOBALES
# ════════════════════════════════════════════════════════
PLACEHOLDER = "Escribe tu mensaje para CVision..."
AVATAR_URL = "https://huggingface.co/spaces/CVision-assitant/asistente-virtual-CVision/resolve/main/assets/robot-bckound-black.png"
# ════════════════════════════════════════════════════════
# CSS GLOBAL β€” Estilos de toda la interfaz
# ════════════════════════════════════════════════════════
CUSTOM_CSS = """
@import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Exo+2:wght@300;400;500&family=Share+Tech+Mono&display=swap');
/* ══ VARIABLES CSS GLOBALES ══
Modifica aquΓ­ los colores principales de toda la app */
:root {
--cyan:#00d4ff; /* Color principal cian */
--cyan2:#00a8e8; /* Cian secundario */
--cg:#00ff88; /* Color verde neΓ³n (online/activo) */
--bg-main:#020408; /* Fondo principal oscuro */
--border:rgba(0,212,255,0.16);
--font-d:'Orbitron',monospace; /* Fuente display/tΓ­tulos */
--font-b:'Exo 2',sans-serif; /* Fuente body */
--font-m:'Share Tech Mono',monospace; /* Fuente monospace/datos */
}
/* ══ RESET Y BASE ══ */
html,body { overflow-x:hidden !important; }
body,.gradio-container {
background:var(--bg-main) !important;
color:white !important;
font-family:'Exo 2','Segoe UI',sans-serif;
}
/* ══ LÍNEA DE ESCANEO ANIMADA (scan line global) ══
Desactivar: comentar este bloque */
.gradio-container::after {
content:''; position:fixed; top:0; left:0; width:100%; height:2px;
background:linear-gradient(90deg,transparent,rgba(0,212,255,0.30),transparent);
animation:cvScanDown 7s ease-in-out infinite;
z-index:9999; pointer-events:none;
}
/* ══ KEYFRAMES β€” Todas las animaciones CSS ══
Modifica aquΓ­ velocidades/efectos de animaciΓ³n */
@keyframes cvScanDown { 0%{top:-2px;opacity:0} 8%{opacity:1} 92%{opacity:.4} 100%{top:100vh;opacity:0} }
@keyframes cvSpin { to{transform:rotate(360deg)} }
@keyframes cvBlink { 0%,100%{opacity:.25} 50%{opacity:1} }
@keyframes cvOrbit { to{transform:rotate(360deg)} }
@keyframes cvFloat { 0%,100%{transform:translateY(0)} 50%{transform:translateY(-4px)} }
@keyframes pulse { 0%,100%{transform:scale(1);opacity:1} 50%{transform:scale(1.3);opacity:.7} }
@keyframes bglow { 0%,100%{transform:scale(1);opacity:.5} 50%{transform:scale(1.2);opacity:1} }
@keyframes radarSpin { to{transform:rotate(360deg)} }
@keyframes headerShimmer { 0%,100%{opacity:0} 50%{opacity:1} }
@keyframes btnGlow { 0%,100%{box-shadow:0 0 12px rgba(0,212,255,.3),0 0 24px rgba(0,212,255,.1)} 50%{box-shadow:0 0 22px rgba(0,212,255,.7),0 0 44px rgba(0,212,255,.25)} }
@keyframes avatar-pulse {
0% { border-color:rgba(0,212,255,0.3); box-shadow:0 0 5px rgba(0,212,255,0.2); }
50% { border-color:rgba(0,212,255,1); box-shadow:0 0 18px rgba(0,212,255,0.6); }
100%{ border-color:rgba(0,212,255,0.3); box-shadow:0 0 5px rgba(0,212,255,0.2); }
}
/* ══ HEADER β€” Barra superior con logo y tΓ­tulo ══
Modificar padding/height aquΓ­ para cambiar tamaΓ±o del header */
#cvision-header {
background:rgba(1,4,12,0.98);
border-bottom:1px solid rgba(0,212,255,0.18);
padding:12px 28px; display:flex; align-items:center; gap:16px;
backdrop-filter:blur(24px); position:relative; z-index:10; flex-shrink:0;
box-shadow:0 4px 40px rgba(0,0,0,.8),0 0 30px rgba(0,212,255,.04);
}
/* LΓ­nea inferior animada del header */
#cvision-header::after {
content:''; position:absolute; bottom:0; left:0; right:0; height:1px;
background:linear-gradient(90deg,transparent,rgba(0,212,255,.5) 30%,rgba(0,255,200,.7) 50%,rgba(0,212,255,.5) 70%,transparent);
animation:headerShimmer 3s ease-in-out infinite;
}
/* ══ NEBULA SECTION β€” Contenedor de la secciΓ³n hero (nebula + info + panel derecho) ══
Modificar padding/gap aquΓ­ para espaciado de la secciΓ³n */
#cvision-nebula-section {
position:relative; display:flex; align-items:center; gap:24px;
padding:28px 32px; border-bottom:1px solid rgba(0,212,255,0.10);
background:linear-gradient(180deg,rgba(0,8,20,0.97),rgba(1,6,16,0.99));
flex-shrink:0; overflow:hidden;
}
/* ══ GRID CANVAS β€” Canvas de fondo con cuadrΓ­cula y puntos ══
Es el fondo animado de la secciΓ³n nebula */
#cv-grid-canvas {
position:absolute; inset:0;
width:100% !important; height:100% !important;
pointer-events:none; z-index:0;
}
/* ══ NEBULA CANVAS WRAPPER β€” Contenedor del canvas de la esfera nebula ══
Modificar width/height aquΓ­ para cambiar tamaΓ±o de la nebula */
#cv-nebula-wrap {
position:relative; width:420px; height:420px;
flex-shrink:0; z-index:2;
}
/* TamaΓ±o del canvas de la nebula (debe coincidir con el wrapper) */
#nebulaCanvas { width:420px !important; height:420px !important; display:block; position:relative; z-index:2; }
/* ══ HERO INFO β€” Bloque de texto a la derecha de la nebula ══
TΓ­tulo, descripciΓ³n y chips de capacidades */
#cv-hero-info {
position:relative; z-index:2;
display:flex; flex-direction:column; gap:13px;
flex:1; min-width:0;
}
/* Texto de descripciΓ³n del hero β€” modificar font-size para tamaΓ±o */
#cv-hero-info .cv-desc {
font-family:'Exo 2',sans-serif;
font-size:1.08rem !important;
color:rgba(90,160,190,.85) !important;
line-height:1.85 !important;
font-weight:300;
max-width:460px;
}
/* ══ RIGHT PANEL β€” Panel lateral derecho (NΓΊcleo Neural + Conectividad) ══
Modificar width para cambiar ancho del panel */
#cv-right-panel {
position:relative; z-index:2;
display:flex; flex-direction:column; gap:10px;
width:210px; flex-shrink:0;
}
/* ══ RCARD β€” Tarjetas del panel derecho ══
Estilos compartidos por NΓΊcleo Neural y Conectividad */
.rcard {
background:rgba(2,7,18,.92); border:1px solid rgba(0,212,255,.12);
border-radius:12px; overflow:hidden; position:relative;
box-shadow:0 0 30px rgba(0,0,0,.5);
}
.rcard::before { content:''; position:absolute; top:0; left:0; right:0; height:1px; background:linear-gradient(90deg,transparent,rgba(0,212,255,.4),rgba(0,255,200,.2),transparent); }
.rcard::after { content:''; position:absolute; bottom:0; left:0; right:0; height:1px; background:linear-gradient(90deg,transparent,rgba(0,212,255,.1),transparent); }
/* ══ RCARD HEADER β€” Cabecera de las tarjetas del panel derecho ══ */
.rh { display:flex; align-items:center; gap:6px; padding:8px 12px; border-bottom:1px solid rgba(0,212,255,.06); }
.rh-dot { width:5px; height:5px; border-radius:50%; background:var(--cyan); box-shadow:0 0 6px var(--cyan); flex-shrink:0; animation:pulse 2s infinite; }
.rh-t { font-family:'Orbitron',monospace; font-size:8px; font-weight:700; color:rgba(0,190,240,.6); letter-spacing:.15em; }
.rh-s { margin-left:auto; font-family:'Share Tech Mono',monospace; font-size:7.5px; color:rgba(0,150,200,.35); letter-spacing:.1em; }
/* ══ BRAIN BOX β€” Contenedor del canvas del cerebro neural ══
Modificar height para cambiar altura del Γ‘rea del cerebro */
.brain-box { padding:6px; display:flex; align-items:center; justify-content:center; height:180px; position:relative; }
/* Glow del fondo del cerebro */
.bglow { position:absolute; width:150px; height:150px; border-radius:50%; background:radial-gradient(circle,rgba(0,100,255,.12),transparent 70%); animation:bglow 2s ease-in-out infinite; }
/* TamaΓ±o del canvas del cerebro β€” modificar width/height */
#brainCvs { width:198px; height:168px; }
/* Anillos de radar decorativos */
.rring { position:absolute; border-radius:50%; border:1px solid rgba(0,212,255,.08); }
.rring1 { inset:10px; animation:radarSpin 8s linear infinite; }
.rring2 { inset:25px; animation:radarSpin 12s linear infinite reverse; }
/* ══ CONNECTIVITY CARD β€” Tarjeta de conectividad/mapa mundial ══ */
.conn-body { padding:9px 12px; }
.cstat { display:flex; align-items:center; gap:7px; margin-bottom:5px; }
.cdot { width:8px; height:8px; border-radius:50%; background:var(--cg); box-shadow:0 0 12px var(--cg); animation:pulse 1.5s infinite; }
.cst { font-family:'Share Tech Mono',monospace; font-size:9px; color:var(--cg); letter-spacing:.12em; }
.clat { font-family:'Share Tech Mono',monospace; font-size:8px; color:rgba(0,150,200,.4); letter-spacing:.1em; margin-bottom:7px; }
/* Canvas del mapa mundial β€” modificar width/height */
#worldC { width:100%; height:52px; }
/* ══ STATUS BAR β€” Barra de estado inferior fija ══
Modificar bottom para subir/bajar, font-size para tamaΓ±o */
#cvision-statusbar {
position:fixed;
bottom:4px;
left:50%; transform:translateX(-50%);
font-family:var(--font-m); font-size:0.75rem; letter-spacing:0.14em;
color:rgba(0,212,255,0.75); display:flex; align-items:center; gap:14px;
pointer-events:none; z-index:500; white-space:nowrap;
padding:6px 20px;
background:rgba(0,6,16,0.80); backdrop-filter:blur(12px);
border:1px solid rgba(0,212,255,0.22); border-radius:8px;
box-shadow:0 0 16px rgba(0,212,255,0.10);
}
/* Puntos separadores de la status bar */
.cv-sdot { width:4px; height:4px; border-radius:50%; background:rgba(0,212,255,0.5); animation:cvBlink 2s ease-in-out infinite; display:inline-block; }
/* ══════════════════════════════════════════════════════
CHATBOT β€” Contenedor principal y burbujas de mensajes
══════════════════════════════════════════════════════ */
/* ── Contenedor externo del chatbot ── */
.chatbot-container {
border-radius:16px !important;
border:1px solid rgba(0,210,255,0.25) !important;
background:rgba(2,10,20,0.98) !important;
box-shadow:0 12px 50px rgba(0,0,0,.8) !important;
overflow:hidden !important;
}
/* ── Wrapper interno (lista de mensajes) ── */
.chatbot-container > div.wrapper {
display:flex !important;
flex-direction:column !important;
gap:14px !important;
padding:16px !important;
}
/* ── BASE de todos los mensajes ──
NO poner display:block ni width fijos aquΓ­ */
.chatbot-container .message {
max-width:82% !important;
padding:12px 16px !important;
font-size:14px !important;
line-height:1.65 !important;
position:relative !important;
box-sizing:border-box !important;
overflow-wrap:break-word !important;
word-break:break-word !important;
white-space:normal !important;
hyphens:auto !important;
}
/* ── BURBUJA USUARIO (derecha) ──
Modificar background/border para cambiar color del usuario */
.chatbot-container .message.user,
.chatbot-container [data-testid="user"] {
align-self:flex-end !important;
margin-left:auto !important;
margin-right:6px !important;
width:fit-content !important;
min-width:60px !important;
max-width:75% !important;
background:linear-gradient(145deg,rgba(0,80,160,0.82),rgba(0,40,95,0.96)) !important;
border:1px solid rgba(0,180,255,0.45) !important;
border-radius:18px 18px 4px 18px !important;
color:#ffffff !important;
box-shadow:0 4px 18px rgba(0,0,0,.55), 0 0 8px rgba(0,120,255,0.12) !important;
text-align:left !important;
display:block !important;
}
/* PΓ‘rrafos dentro de burbuja usuario */
.chatbot-container .message.user p,
.chatbot-container [data-testid="user"] p {
margin:0 0 6px 0 !important;
white-space:normal !important;
word-break:break-word !important;
overflow-wrap:break-word !important;
color:#ffffff !important;
}
.chatbot-container .message.user p:last-child,
.chatbot-container [data-testid="user"] p:last-child { margin-bottom:0 !important; }
/* Todos los hijos del mensaje de usuario */
.chatbot-container .message.user *,
.chatbot-container [data-testid="user"] * {
color:#ffffff !important;
overflow-wrap:break-word !important;
word-break:break-word !important;
white-space:normal !important;
max-width:100% !important;
}
/* ── BURBUJA ASISTENTE (izquierda) ──
Modificar background/border-left para cambiar color del bot */
.chatbot-container .message.bot,
.chatbot-container [data-testid="bot"] {
align-self:flex-start !important;
margin-right:auto !important;
margin-left:6px !important;
width:fit-content !important;
min-width:80px !important;
max-width:82% !important;
background:linear-gradient(145deg,rgba(10,25,45,0.92),rgba(5,15,30,0.96)) !important;
border:1px solid rgba(0,212,255,0.15) !important;
border-radius:18px 18px 18px 4px !important;
border-left:3px solid #00d4ff !important; /* ← lΓ­nea izquierda cian */
color:#c8e8f8 !important;
box-shadow:0 4px 18px rgba(0,0,0,.6) !important;
display:block !important;
}
/* PΓ‘rrafos dentro de burbuja bot */
.chatbot-container .message.bot p,
.chatbot-container [data-testid="bot"] p {
margin-bottom:8px !important;
white-space:pre-wrap !important;
color:#c8e8f8 !important;
}
.chatbot-container .message.bot p:last-child,
.chatbot-container [data-testid="bot"] p:last-child { margin-bottom:0 !important; }
/* ── BLOQUES DE CΓ“DIGO dentro del chatbot ── */
.chatbot-container pre {
background:rgba(0,5,15,0.85) !important;
border:1px solid rgba(0,212,255,0.3) !important;
border-radius:10px !important;
padding:12px !important; margin:8px 0 !important;
overflow-x:auto !important; white-space:pre !important;
}
.chatbot-container code {
font-family:'Share Tech Mono',monospace !important;
color:#00ffcc !important;
background:rgba(0,255,200,0.1) !important;
padding:2px 5px !important; border-radius:4px !important;
white-space:pre-wrap !important;
}
/* ── SCROLLBAR del chatbot ── */
.chatbot-container > div::-webkit-scrollbar { width:5px !important; }
.chatbot-container > div::-webkit-scrollbar-thumb {
background:rgba(0,212,255,0.2) !important; border-radius:10px !important;
}
/* ══ AVATAR DEL ASISTENTE β€” Esfera con logo CVision ══
Modificar width/height para cambiar tamaΓ±o del avatar (actualmente 42x42) */
.chatbot-container .avatar-container {
width:48px !important; height:48px !important;
border-radius:50% !important;
border:2px solid rgba(0,212,255,0.6) !important;
box-shadow:0 0 10px rgba(0,212,255,0.35) !important;
overflow:hidden !important;
background:rgba(2,10,20,0.9) !important;
display:flex !important; align-items:center !important;
justify-content:center !important; flex-shrink:0 !important;
}
/* TamaΓ±o del logo dentro del avatar β€” scale(1.18) para zoom */
.chatbot-container .avatar-container img {
width:100% !important; height:100% !important;
object-fit:cover !important;
transform:scale(1.18) !important; /* ← aumentar para mΓ‘s zoom */
}
/* AnimaciΓ³n de pulso en el avatar del bot */
.chatbot-container [data-testid="bot"] .avatar-container {
animation:avatar-pulse 3s infinite ease-in-out;
}
/* ══ TEXTBOX β€” Campo de entrada de texto ══
Modificar border-radius/font-size para cambiar estilo del input */
.gradio-textbox input, .gradio-textbox textarea {
background:rgba(0,8,22,.92) !important;
border:1px solid rgba(0,180,255,.22) !important;
border-radius:22px !important;
color:#e0f4ff !important;
font-family:'Exo 2',sans-serif !important;
font-size:13.5px !important;
padding:9px 18px !important;
transition:all .3s !important;
}
.gradio-textbox input:focus, .gradio-textbox textarea:focus {
border-color:var(--cyan) !important;
box-shadow:0 0 0 1px rgba(0,212,255,.15), 0 0 20px rgba(0,212,255,.12) !important;
outline:none !important;
}
.gradio-textbox input::placeholder, .gradio-textbox textarea::placeholder { color:rgba(0,140,190,.35) !important; }
/* ══ BOTΓ“N ENVIAR β€” BotΓ³n principal de envΓ­o de mensaje ══
Modificar background gradient para cambiar color del botΓ³n */
.btn-send-cv button, .btn-send-cv {
background:linear-gradient(135deg, #001a3a 0%, #003670 40%, #0068b8 75%, #00aadd 100%) !important;
border:1px solid rgba(0,212,255,0.5) !important;
color:#e8f8ff !important;
border-radius:20px !important;
font-weight:700 !important; font-size:0.72rem !important;
font-family:'Orbitron',monospace !important; letter-spacing:.08em !important;
padding:8px 18px !important; height:38px !important;
min-width:110px !important; max-width:160px !important;
box-shadow:0 0 14px rgba(0,168,232,.35), 0 0 1px rgba(0,212,255,.6) inset !important;
transition:all 0.2s ease !important;
animation:btnGlow 3s ease-in-out infinite !important;
position:relative !important; overflow:hidden !important;
white-space:nowrap !important; text-overflow:ellipsis !important;
}
.btn-send-cv button::after, .btn-send-cv::after {
content:''; position:absolute; top:0; left:-100%; width:100%; height:100%;
background:linear-gradient(90deg,transparent,rgba(255,255,255,.12),transparent);
transition:.4s;
}
.btn-send-cv button:hover, .btn-send-cv:hover {
box-shadow:0 0 28px rgba(0,212,255,.65), 0 0 55px rgba(0,212,255,.2) !important;
transform:translateY(-1px) !important;
border-color:rgba(0,255,220,.7) !important;
}
.btn-send-cv button:hover::after, .btn-send-cv:hover::after { left:100%; }
/* ══ BOTΓ“N VOZ β€” BotΓ³n de activar respuesta por voz del asistente ══ */
.btn-voice-cv button, .btn-voice-cv {
background:rgba(0,20,50,.6) !important;
border:1px solid rgba(0,212,255,.35) !important;
color:#00d4ff !important;
border-radius:20px !important; font-size:0.72rem !important;
font-family:'Share Tech Mono',monospace !important; letter-spacing:.1em !important;
padding:8px 14px !important; height:38px !important;
min-width:70px !important; max-width:110px !important;
transition:all 0.2s !important; white-space:nowrap !important;
}
.btn-voice-cv button:hover, .btn-voice-cv:hover {
background:rgba(0,80,160,.3) !important;
box-shadow:0 0 16px rgba(0,212,255,.4) !important;
border-color:rgba(0,212,255,.7) !important;
}
/* ══ BOTΓ“N LIMPIAR β€” BotΓ³n para limpiar conversaciΓ³n ══ */
.btn-clear-cv button, .btn-clear-cv {
background:rgba(30,0,0,.5) !important;
border:1px solid rgba(255,50,80,.25) !important;
color:rgba(255,100,120,.7) !important;
border-radius:20px !important; font-size:0.68rem !important;
font-family:'Share Tech Mono',monospace !important; letter-spacing:.08em !important;
padding:8px 14px !important; height:38px !important;
min-width:90px !important; max-width:160px !important;
transition:all 0.2s !important; white-space:nowrap !important;
}
.btn-clear-cv button:hover, .btn-clear-cv:hover {
background:rgba(60,0,0,.6) !important;
border-color:rgba(255,80,80,.5) !important;
box-shadow:0 0 12px rgba(255,50,50,.25) !important;
}
/* ══ BOTΓ“N EJECUTAR (PDF) β€” BotΓ³n de envΓ­o en el mΓ³dulo PDF ══ */
.btn-exec-cv button, .btn-exec-cv {
background:linear-gradient(135deg,#001a3a,#00306a,#0055a0) !important;
border:1px solid rgba(0,200,255,.45) !important; color:#e0f8ff !important;
border-radius:20px !important; font-family:'Orbitron',monospace !important;
font-size:0.68rem !important; font-weight:700 !important; letter-spacing:.08em !important;
padding:8px 18px !important; height:38px !important;
box-shadow:0 0 12px rgba(0,150,255,.3) !important;
transition:all .2s !important; white-space:nowrap !important;
}
.btn-exec-cv button:hover, .btn-exec-cv:hover {
box-shadow:0 0 24px rgba(0,212,255,.55) !important; transform:translateY(-1px) !important;
}
/* ══ MIC / AUDIO COMPONENT β€” Componente de grabaciΓ³n de voz (vozβ†’texto) ══
Modificar border/background para cambiar apariencia del micrΓ³fono */
.mic-tool-cv {
background:rgba(0,6,18,.9) !important;
border:1px solid rgba(0,180,255,.18) !important;
border-radius:12px !important; padding:4px 8px !important;
}
.mic-tool-cv label {
font-family:'Share Tech Mono',monospace !important;
font-size:0.65rem !important; color:rgba(0,180,255,.5) !important; letter-spacing:.12em !important;
}
.mic-tool-cv button {
background:rgba(0,50,120,.4) !important;
border:1px solid rgba(0,212,255,.3) !important;
border-radius:8px !important; color:#00d4ff !important;
font-size:0.72rem !important; padding:4px 10px !important; transition:.2s !important;
}
.mic-tool-cv button:hover {
background:rgba(0,80,180,.5) !important;
box-shadow:0 0 10px rgba(0,212,255,.3) !important;
}
/* ══ FILAS DE INPUT β€” Espaciado de las filas de controles ══ */
.input-row-cv { gap:8px !important; align-items:center !important; }
.mic-row-cv { gap:8px !important; align-items:center !important; margin-top:4px !important; }
/* ══ FOOTER DE GRADIO β€” Ocultar footer por defecto ══ */
footer { display:none !important; }
"""
# ════════════════════════════════════════════════════════
# HEADER HTML β€” Barra superior: logo CV, tΓ­tulo, imagen logo, badges
# ════════════════════════════════════════════════════════
HEADER_HTML = """
<div id="cvision-header">
<!-- Badge circular CV (izquierda) -->
<div style="width:56px;height:56px;border-radius:50%;
border:1.5px solid rgba(0,212,255,0.5);
background:radial-gradient(circle,rgba(0,212,255,0.18) 0%,#020a18 100%);
display:flex;align-items:center;justify-content:center;
font-family:'Orbitron',monospace;font-size:0.55rem;font-weight:700;
color:#00d4ff;letter-spacing:0.04em;
box-shadow:0 0 22px rgba(0,212,255,0.45),0 0 50px rgba(0,212,255,0.12);
position:relative;overflow:hidden;flex-shrink:0;">
<span style="position:relative;z-index:1;">CV</span>
<div style="position:absolute;inset:-50%;width:200%;height:200%;
background:conic-gradient(from 0deg,transparent 55%,rgba(0,212,255,0.28) 78%,transparent 100%);
animation:cvSpin 3.5s linear infinite;"></div>
</div>
<!-- TΓ­tulo principal + subtΓ­tulo -->
<div style="flex:1;display:flex;flex-direction:column;gap:3px;">
<div style="font-family:'Orbitron',monospace;font-size:2.8rem;font-weight:900;
color:#fff;letter-spacing:0.08em;line-height:1;
text-shadow:0 0 22px rgba(0,212,255,0.65),0 0 55px rgba(0,212,255,0.2);">
C<span style="color:#00d4ff;">Vision</span> Intelligence
</div>
<div style="font-family:'Share Tech Mono',monospace;font-size:0.75rem;
letter-spacing:0.22em;color:rgba(74,122,150,.75);">
ASISTENTE T&Eacute;CNICO MULTIMODAL &nbsp;&middot;&nbsp; CRISTIAN VELASCO
</div>
</div>
<!-- Logo CVision animado (centro-derecha) -->
<div style="display:flex;align-items:center;justify-content:center;margin:0 24px;position:relative;">
<div style="position:absolute;width:103px;height:103px;border-radius:50%;
border:1px solid rgba(0,212,255,0.35);
box-shadow:0 0 14px rgba(0,212,255,0.25),inset 0 0 14px rgba(0,212,255,0.1);
animation:cvFloat 3s ease-in-out infinite;"></div>
<div style="position:absolute;width:130px;height:130px;border-radius:50%;
border:0.5px dashed rgba(0,212,255,0.15);
animation:cvOrbit 12s linear infinite;"></div>
<img src="https://huggingface.co/spaces/CVision-assitant/asistente-virtual-CVision/resolve/main/assets/CVision_logo-trprnte.png"
onerror="this.style.display='none'"
style="width:130px;height:130px;object-fit:contain;border-radius:50%;
filter:drop-shadow(0 0 10px rgba(0,212,255,0.7)) drop-shadow(0 0 24px rgba(0,180,255,0.4));
animation:cvFloat 3s ease-in-out infinite;position:relative;z-index:1;">
</div>
<!-- Badges de estado (derecha) -->
<div style="display:flex;gap:7px;align-items:center;flex-shrink:0;">
<div style="width:7px;height:7px;border-radius:50%;background:#00ff96;
box-shadow:0 0 8px #00ff96;animation:cvBlink 2s ease-in-out infinite;"></div>
<span style="font-family:'Share Tech Mono',monospace;font-size:0.82rem;
letter-spacing:0.14em;padding:4px 11px;border-radius:20px;
border:1px solid rgba(0,255,150,0.3);background:rgba(0,255,150,0.07);
color:#00ff96;">ONLINE</span>
<span style="font-family:'Share Tech Mono',monospace;font-size:0.82rem;
letter-spacing:0.14em;padding:4px 11px;border-radius:20px;
border:1px solid rgba(0,212,255,0.28);background:rgba(0,212,255,0.07);
color:#00d4ff;">v3.1.0</span>
<span style="font-family:'Share Tech Mono',monospace;font-size:0.82rem;
letter-spacing:0.14em;padding:4px 11px;border-radius:20px;
border:1px solid rgba(0,212,255,0.28);background:rgba(0,212,255,0.07);
color:#00d4ff;">NEURAL &middot; ACTIVE</span>
</div>
</div>
"""
# ════════════════════════════════════════════════════════
# NEBULA SECTION HTML β€” Hero con nebula, info y panel derecho
# ════════════════════════════════════════════════════════
NEBULA_SECTION_HTML = """
<div id="cvision-nebula-section">
<!-- Canvas de cuadrΓ­cula de fondo -->
<canvas id="cv-grid-canvas"></canvas>
<!-- Canvas de la esfera nebula animada -->
<div id="cv-nebula-wrap">
<canvas id="nebulaCanvas"></canvas>
</div>
<!-- Bloque de informaciΓ³n hero (tΓ­tulo + descripciΓ³n + chips) -->
<div id="cv-hero-info">
<div style="font-family:'Share Tech Mono',monospace;font-size:0.6rem;
letter-spacing:0.3em;color:rgba(0,180,220,.5);text-transform:uppercase;
display:flex;align-items:center;gap:8px;">
<span style="display:inline-block;width:6px;height:6px;border-radius:50%;
background:#00d4ff;box-shadow:0 0 8px #00d4ff;"></span>
SISTEMA ACTIVADO &nbsp;&middot;&nbsp; CORE OPERATIVO
</div>
<!-- TΓ­tulo grande del hero -->
<div style="font-family:'Orbitron',monospace;font-size:2.15rem;
font-weight:900;line-height:1.12;color:#dff0ff;
text-shadow:0 0 30px rgba(0,212,255,0.55),0 0 70px rgba(0,180,255,0.2);">
Asistente Virtual<br>
<span style="color:transparent;
background:linear-gradient(135deg,#00eeff 0%,#ffffff 45%,#00aaff 100%);
-webkit-background-clip:text;background-clip:text;
filter:drop-shadow(0 0 10px rgba(0,220,255,0.7));">CVision</span>
</div>
<!-- DescripciΓ³n β€” clase cv-desc (ver CSS #cv-hero-info .cv-desc) -->
<p class="cv-desc">
Interfaz de <strong style="color:rgba(0,220,255,.95);font-weight:600;">inteligencia artificial</strong> multimodal.
Combina procesamiento de lenguaje natural, an&aacute;lisis de documentos
y b&uacute;squeda web en un solo n&uacute;cleo cognitivo.
</p>
<!-- Chips de capacidades -->
<div style="display:flex;flex-wrap:wrap;gap:7px;margin-top:4px;">
<span style="font-family:'Share Tech Mono',monospace;font-size:0.58rem;letter-spacing:0.1em;padding:5px 13px;border-radius:20px;border:1px solid rgba(0,212,255,0.22);background:rgba(0,80,160,0.08);color:rgba(0,200,255,.5);">PROCESAMIENTO NLP</span>
<span style="font-family:'Share Tech Mono',monospace;font-size:0.58rem;letter-spacing:0.1em;padding:5px 13px;border-radius:20px;border:1px solid rgba(0,212,255,0.22);background:rgba(0,80,160,0.08);color:rgba(0,200,255,.5);">VISI&Oacute;N COMPUTACIONAL</span>
<span style="font-family:'Share Tech Mono',monospace;font-size:0.58rem;letter-spacing:0.1em;padding:5px 13px;border-radius:20px;border:1px solid rgba(0,212,255,0.22);background:rgba(0,80,160,0.08);color:rgba(0,200,255,.5);">B&Uacute;SQUEDA WEB</span>
<span style="font-family:'Share Tech Mono',monospace;font-size:0.58rem;letter-spacing:0.1em;padding:5px 13px;border-radius:20px;border:1px solid rgba(0,212,255,0.22);background:rgba(0,80,160,0.08);color:rgba(0,200,255,.5);">AN&Aacute;LISIS PDF</span>
<span style="font-family:'Share Tech Mono',monospace;font-size:0.58rem;letter-spacing:0.1em;padding:5px 13px;border-radius:20px;border:1px solid rgba(0,212,255,0.22);background:rgba(0,80,160,0.08);color:rgba(0,200,255,.5);">VOZ &middot; WHISPER</span>
<span style="font-family:'Share Tech Mono',monospace;font-size:0.58rem;letter-spacing:0.1em;padding:5px 13px;border-radius:20px;border:1px solid rgba(0,212,255,0.22);background:rgba(0,80,160,0.08);color:rgba(0,200,255,.5);">RAZONAMIENTO IA</span>
</div>
</div>
<!-- Panel derecho: NΓΊcleo Neural + Conectividad -->
<div id="cv-right-panel">
<!-- Tarjeta NΓΊcleo Neural (canvas del cerebro) -->
<div class="rcard">
<div class="rh" style="padding:10px 12px 8px;">
<span style="color:#00d4ff;font-size:14px;margin-right:6px;">&#10022;</span>
<div style="display:flex;flex-direction:column;">
<span class="rh-t" style="font-size:8.5px;font-weight:900;letter-spacing:.18em;color:rgba(160,210,255,.95);">N&Uacute;CLEO NEURAL</span>
<span style="font-family:'Share Tech Mono',monospace;font-size:6.5px;color:rgba(0,150,200,.4);letter-spacing:.18em;margin-top:2px;">ACTIVIDAD EN TIEMPO REAL</span>
</div>
</div>
<div class="brain-box">
<div class="bglow"></div>
<div class="rring rring1"></div>
<div class="rring rring2"></div>
<canvas id="brainCvs" width="198" height="168"></canvas>
</div>
</div>
<!-- Tarjeta Conectividad (canvas mapa mundial) -->
<div class="rcard">
<div class="rh">
<div class="rh-dot" style="background:#00ff88;box-shadow:0 0 8px #00ff88;"></div>
<span class="rh-t">CONECTIVIDAD</span>
<span class="rh-s" style="color:#00ff88;">&#9679; ONLINE</span>
</div>
<div class="conn-body">
<div class="cstat"><div class="cdot"></div><span class="cst">ONLINE</span></div>
<div class="clat">Latencia: <span style="color:#00d4ff;">18ms</span> &middot; P&eacute;rdida: <span style="color:#00ffcc;">0.0%</span></div>
<canvas id="worldC" width="190" height="52"></canvas>
</div>
</div>
</div>
</div>
"""
# ════════════════════════════════════════════════════════
# STATUS BAR + JAVASCRIPT β€” Barra de estado y animaciones canvas
# ════════════════════════════════════════════════════════
STATUS_AND_JS_HTML = """
<!-- ══ STATUS BAR HTML ══ -->
<div id="cvision-statusbar">
<span>CORE ACTIVE</span><span class="cv-sdot"></span>
<span>ALL SYSTEMS NOMINAL</span><span class="cv-sdot" style="animation-delay:.6s"></span>
<span id="cv-clock">00:00:00</span><span class="cv-sdot" style="animation-delay:1.2s"></span>
<span>SCANNING</span>
</div>
<script>
'use strict';
// ═══════════════════════════════════════════════════════
// RELOJ β€” Actualiza el reloj de la status bar cada segundo
// Para cambiar formato: modificar el string de textContent
// ═══════════════════════════════════════════════════════
(function clockTick(){
var el = document.getElementById('cv-clock');
if(el){
var n = new Date();
var p = function(x){ return String(x).padStart(2,'0'); };
el.textContent = p(n.getHours()) + ':' + p(n.getMinutes()) + ':' + p(n.getSeconds());
}
setTimeout(clockTick, 1000);
})();
// ═══════════════════════════════════════════════════════
// GRID CANVAS β€” CuadrΓ­cula + puntos flotantes de fondo
// OPTIMIZACIΓ“N: DOT_COUNT reducido (30β†’18), GRID mΓ‘s amplia
// Para mΓ‘s/menos puntos: cambiar DOT_COUNT
// Para mΓ‘s/menos lΓ­neas: cambiar GRID (mayor = menos lΓ­neas)
// ═══════════════════════════════════════════════════════
(function initGrid(){
function setup(){
var section = document.getElementById('cvision-nebula-section');
var gc = document.getElementById('cv-grid-canvas');
if(!section || !gc){ setTimeout(setup, 200); return; }
var ctx = gc.getContext('2d');
var W, H, dots = [];
var GRID = 52; /* ← Mayor = cuadrΓ­cula mΓ‘s espaciada (menos lΓ­neas) */
var DOT_COUNT = 18; /* ← Reducir para mejorar rendimiento */
function resize(){
W = gc.width = section.offsetWidth;
H = gc.height = section.offsetHeight;
buildDots();
}
function buildDots(){
dots = [];
for(var i = 0; i < DOT_COUNT; i++){
dots.push({ x:Math.random()*W, y:Math.random()*H,
vx:(Math.random()-.5)*.35, vy:(Math.random()-.5)*.35,
r:.8+Math.random()*1.4, a:.10+Math.random()*.28 });
}
}
function drawGrid(){
ctx.strokeStyle='rgba(0,200,255,0.035)'; ctx.lineWidth=.5;
for(var x=0;x<W;x+=GRID){ctx.beginPath();ctx.moveTo(x,0);ctx.lineTo(x,H);ctx.stroke();}
for(var y=0;y<H;y+=GRID){ctx.beginPath();ctx.moveTo(0,y);ctx.lineTo(W,y);ctx.stroke();}
ctx.fillStyle='rgba(0,200,255,0.08)';
for(var x2=0;x2<W;x2+=GRID) for(var y2=0;y2<H;y2+=GRID){ctx.beginPath();ctx.arc(x2,y2,1,0,Math.PI*2);ctx.fill();}
}
function drawDots(){
for(var i=0;i<dots.length;i++){
var d=dots[i]; d.x+=d.vx; d.y+=d.vy;
if(d.x<0)d.x=W; if(d.x>W)d.x=0; if(d.y<0)d.y=H; if(d.y>H)d.y=0;
ctx.globalAlpha=d.a; ctx.fillStyle='#00d4ff'; ctx.beginPath(); ctx.arc(d.x,d.y,d.r,0,Math.PI*2); ctx.fill();
}
ctx.globalAlpha=1;
}
function drawVig(){
var g=ctx.createRadialGradient(W*.5,H*.5,0,W*.5,H*.5,Math.max(W,H)*.65);
g.addColorStop(0,'rgba(0,8,22,0)'); g.addColorStop(1,'rgba(0,4,12,0.65)');
ctx.fillStyle=g; ctx.fillRect(0,0,W,H);
}
/* OPTIMIZACIΓ“N: throttle a ~40fps (25ms) en vez de requestAnimationFrame puro */
var lastGrid = 0;
function loop(ts){
requestAnimationFrame(loop);
if(ts - lastGrid < 25) return;
lastGrid = ts;
ctx.clearRect(0,0,W,H); drawGrid(); drawDots(); drawVig();
}
window.addEventListener('resize',resize,{passive:true}); resize();
requestAnimationFrame(loop);
}
setup();
})();
// ═══════════════════════════════════════════════════════
// NEBULA β€” Esfera 3D con partΓ­culas, Γ³rbitas y texto CVision
// OPTIMIZACIΓ“N: partΓ­culas reducidas (280β†’180), rings (16β†’10),
// clouds (7β†’5), orbits (5β†’4), meridianos (10β†’7)
// t += .018 (era .022), throttle a ~50fps
// Para mΓ‘s calidad: aumentar N, rings, clouds
// Para mΓ‘s rendimiento: reducirlos
// ═══════════════════════════════════════════════════════
(function(){
var cv=document.getElementById('nebulaCanvas'); if(!cv) return;
var ctx=cv.getContext('2d');
var W=420,H=420,cx=210,cy=210,R=165; cv.width=W; cv.height=H;
var t=0;
/* PartΓ­culas 3D β€” N=180 (era 280) */
var N=180, parts=[];
for(var i=0;i<N;i++){
var theta=Math.random()*Math.PI*2, phi=Math.acos(2*Math.random()-1), shell=Math.random();
var orbR=R*(shell<.15?.25+Math.random()*.20:shell<.50?.45+Math.random()*.35:.65+Math.random()*.35);
parts.push({theta:theta,phi:phi,r:orbR,
dT:(Math.random()-.5)*.025,dP:(Math.random()-.5)*.016,
sz:2.0+Math.random()*4.0,hue:160+Math.random()*120,
bright:.5+Math.random()*.5,tw:Math.random()*Math.PI*2,twS:.05+Math.random()*.09});
}
/* Rings decorativos β€” 10 (era 16) */
var rings=[];
for(var i=0;i<10;i++) rings.push({tiltX:Math.random()*Math.PI,spin:Math.random()*Math.PI*2,
spinS:(Math.random()-.5)*.026,r:35+i*13,op:.04+Math.random()*.18,
dOn:1.5+Math.random()*5,dOff:2+Math.random()*7,
color:Math.random()>.5?[0,200,255]:Math.random()>.5?[0,255,200]:[180,100,255]});
/* Nubes de nebulosa β€” 5 (era 7) */
var clouds=[];
for(var i=0;i<5;i++) clouds.push({angle:Math.random()*Math.PI*2,dist:.22+Math.random()*.28,
speed:(Math.random()-.5)*.014,size:.32+Math.random()*.20,hue:175+i*36});
/* Γ“rbitas de Γ‘tomo β€” 4 (era 5) */
var orbits=[];
for(var i=0;i<4;i++) orbits.push({tilt:i*(Math.PI/4),spin:Math.random()*Math.PI*2,
spinSpeed:(Math.random()>.5?1:-1)*(.012+Math.random()*.018),
r:R*.55+i*R*.07,opacity:.18+Math.random()*.16});
var lastNeb=0;
function draw(ts){
requestAnimationFrame(draw);
/* OPTIMIZACIΓ“N: throttle a ~50fps */
if(ts - lastNeb < 20) return;
lastNeb = ts;
ctx.clearRect(0,0,W,H); t+=.018;
var atm=ctx.createRadialGradient(cx,cy,R*.5,cx,cy,R*1.55);
atm.addColorStop(0,'rgba(0,100,255,.09)');atm.addColorStop(.5,'rgba(0,60,180,.04)');atm.addColorStop(1,'rgba(0,0,0,0)');
ctx.fillStyle=atm; ctx.fillRect(0,0,W,H);
var sg=ctx.createRadialGradient(cx-R*.28,cy-R*.28,0,cx,cy,R);
sg.addColorStop(0,'rgba(80,180,255,.22)');sg.addColorStop(.25,'rgba(0,100,220,.14)');sg.addColorStop(.65,'rgba(0,50,140,.08)');sg.addColorStop(1,'rgba(0,15,50,.02)');
ctx.fillStyle=sg; ctx.beginPath(); ctx.arc(cx,cy,R,0,Math.PI*2); ctx.fill();
var eg=ctx.createRadialGradient(cx,cy,R*.78,cx,cy,R);
eg.addColorStop(0,'rgba(0,0,0,0)');eg.addColorStop(.55,'rgba(0,180,255,.12)');eg.addColorStop(1,'rgba(0,100,255,.26)');
ctx.fillStyle=eg; ctx.beginPath(); ctx.arc(cx,cy,R,0,Math.PI*2); ctx.fill();
ctx.beginPath(); ctx.arc(cx,cy,R,0,Math.PI*2);
ctx.strokeStyle='rgba(0,210,255,'+(.35+Math.sin(t*.3)*.12)+')'; ctx.lineWidth=2; ctx.stroke();
/* Γ“rbitas de Γ‘tomo */
for(var oi=0;oi<orbits.length;oi++){
var orb=orbits[oi]; orb.spin+=orb.spinSpeed;
ctx.save(); ctx.translate(cx,cy); ctx.rotate(orb.tilt); ctx.scale(1,.40); ctx.rotate(orb.spin);
ctx.beginPath(); ctx.arc(0,0,orb.r,0,Math.PI*2);
var pulse=.65+Math.abs(Math.sin(t*.65+orb.tilt))*.5;
ctx.strokeStyle='rgba(0,215,255,'+(orb.opacity*pulse)+')'; ctx.lineWidth=1.3; ctx.stroke();
var da=orb.spin*2.8,dx=Math.cos(da)*orb.r,dy=Math.sin(da)*orb.r*.40;
ctx.fillStyle='rgba(140,238,255,'+(orb.opacity*pulse*2.1)+')';
ctx.beginPath(); ctx.arc(dx,dy,2.6,0,Math.PI*2); ctx.fill(); ctx.restore();
}
/* Nubes */
for(var ci=0;ci<clouds.length;ci++){
var cl=clouds[ci]; cl.angle+=cl.speed;
var nx=cx+Math.cos(cl.angle)*R*cl.dist, ny=cy+Math.sin(cl.angle)*R*cl.dist*.50;
var cg=ctx.createRadialGradient(nx,ny,0,nx,ny,R*cl.size);
var h=cl.hue+Math.sin(t*.22)*28;
cg.addColorStop(0,'hsla('+h+',100%,58%,.10)'); cg.addColorStop(1,'rgba(0,0,0,0)');
ctx.fillStyle=cg; ctx.fillRect(cx-R,cy-R,R*2,R*2);
}
/* Rings */
for(var ri=0;ri<rings.length;ri++){
var rg=rings[ri]; rg.spin+=rg.spinS;
ctx.save(); ctx.translate(cx,cy); ctx.rotate(rg.spin);
var tiltY=Math.sin(rg.tiltX+t*.16)*.52+.12; ctx.scale(1,Math.abs(tiltY));
ctx.beginPath(); ctx.arc(0,0,rg.r,0,Math.PI*2);
ctx.setLineDash([rg.dOn,rg.dOff]);
var pls=.55+Math.abs(Math.sin(t*.58+rg.r*.025))*.55, rc=rg.color;
ctx.strokeStyle='rgba('+rc[0]+','+rc[1]+','+rc[2]+','+(rg.op*pls)+')';
ctx.lineWidth=1; ctx.stroke(); ctx.setLineDash([]); ctx.restore();
}
/* Wireframe β€” 7 meridianos (era 10) */
for(var li=1;li<6;li++){
var yl=cy-R+li*(R*2/6),rL=Math.sqrt(Math.max(0,R*R-(yl-cy)*(yl-cy)));
if(rL>3){ctx.beginPath();ctx.ellipse(cx,yl,rL,rL*.32,0,0,Math.PI*2);ctx.strokeStyle='rgba(0,185,255,.05)';ctx.lineWidth=.5;ctx.stroke();}
}
for(var mi=0;mi<7;mi++){
var a2=(mi/7)*Math.PI*2+t*.022, ex=cx+R*Math.cos(a2)*Math.abs(Math.cos(t*.07+mi*.35));
ctx.beginPath(); ctx.moveTo(cx,cy-R); ctx.bezierCurveTo(ex,cy-R*.55,ex,cy+R*.55,cx,cy+R);
ctx.strokeStyle='rgba(0,200,255,'+(.02+Math.abs(Math.cos(a2+t*.12))*.038)+')'; ctx.lineWidth=.5; ctx.stroke();
}
/* PartΓ­culas (ordenadas por profundidad) */
var visible=[];
for(var pi=0;pi<parts.length;pi++){
var p=parts[pi]; p.theta+=p.dT; p.phi+=p.dP; p.tw+=p.twS;
var sP=Math.sin(p.phi),cP=Math.cos(p.phi),sT=Math.sin(p.theta),cT=Math.cos(p.theta);
var px2=cx+p.r*sP*cT, py2=cy+p.r*sP*sT*.56, pz=p.r*cP, depth=(pz+R)/(2*R);
visible.push({px:px2,py:py2,pz:pz,depth:depth,p:p});
}
visible.sort(function(a,b){return a.pz-b.pz;});
for(var vi=0;vi<visible.length;vi++){
var v=visible[vi],dep=v.depth,vp=v.p;
var tw=.5+Math.abs(Math.sin(vp.tw))*.55;
var alpha=Math.max(.18,dep*.96*vp.bright*tw), sz=vp.sz*(.42+dep*.82);
var h2=vp.hue+Math.sin(t*.3+vp.theta)*38;
if(dep>.32){ctx.beginPath();ctx.arc(v.px,v.py,sz*2.6,0,Math.PI*2);ctx.fillStyle='hsla('+h2+',100%,68%,'+Math.min(alpha*.30,.36)+')';ctx.fill();}
ctx.beginPath(); ctx.arc(v.px,v.py,sz,0,Math.PI*2);
if(dep>.56){ctx.shadowBlur=sz*5;ctx.shadowColor='hsla('+h2+',100%,78%,1)';}
ctx.fillStyle='hsla('+h2+',100%,84%,'+Math.min(alpha,.97)+')'; ctx.fill(); ctx.shadowBlur=0;
if(dep>.72&&tw>.72){ctx.beginPath();ctx.arc(v.px,v.py,sz*.42,0,Math.PI*2);ctx.fillStyle='rgba(255,255,255,'+Math.min(alpha*.88,.93)+')';ctx.fill();}
}
/* Anillos de datos externos */
for(var ti=0;ti<2;ti++){
var rad=R+25+ti*18;
ctx.save(); ctx.translate(cx,cy); ctx.rotate(Date.now()*.00028*(ti%2===0?1:-.68));
ctx.beginPath(); ctx.arc(0,0,rad,0,Math.PI*2);
ctx.strokeStyle='rgba(0,155,255,'+(.08-ti*.022)+')'; ctx.lineWidth=.9; ctx.stroke();
var tks=20+ti*10;
for(var tk=0;tk<tks;tk++){
if(tk%3!==0) continue;
var a3=(tk/tks)*Math.PI*2, ll=(tk%8===0)?9:(tk%4===0)?5:2.5, glow=(tk%8===0);
ctx.beginPath(); ctx.moveTo(Math.cos(a3)*rad,Math.sin(a3)*rad); ctx.lineTo(Math.cos(a3)*(rad+ll),Math.sin(a3)*(rad+ll));
ctx.strokeStyle=glow?'rgba(0,238,255,.68)':'rgba(0,195,255,.26)'; ctx.lineWidth=glow?1.5:.75; ctx.stroke();
}
ctx.restore();
}
/* Texto CVision central */
var lg=ctx.createRadialGradient(cx,cy,0,cx,cy,80);
lg.addColorStop(0,'rgba(0,235,255,.38)');lg.addColorStop(.5,'rgba(0,115,255,.14)');lg.addColorStop(1,'rgba(0,0,0,0)');
ctx.fillStyle=lg; ctx.beginPath(); ctx.arc(cx,cy,80,0,Math.PI*2); ctx.fill();
ctx.save();
var flicker=.93+Math.sin(t*.8)*.07; ctx.globalAlpha=flicker;
ctx.fillStyle='rgba(0,5,22,.52)'; ctx.beginPath(); ctx.ellipse(cx,cy,78,32,0,0,Math.PI*2); ctx.fill();
ctx.font="bold 40px 'Orbitron',monospace"; ctx.textAlign='center'; ctx.textBaseline='middle';
ctx.shadowBlur=40; ctx.shadowColor='rgba(0,248,255,1)';
var tg2=ctx.createLinearGradient(cx-68,0,cx+68,0);
tg2.addColorStop(0,'#ccffff');tg2.addColorStop(.35,'#ffffff');tg2.addColorStop(.65,'#00ffff');tg2.addColorStop(1,'#ccffff');
ctx.fillStyle=tg2; ctx.fillText('CVision',cx,cy-2);
ctx.font="bold 12px 'Share Tech Mono',monospace"; ctx.globalAlpha=.72+Math.abs(Math.sin(t*.4))*.40;
ctx.shadowBlur=14; ctx.shadowColor='rgba(0,235,255,.92)';
ctx.fillStyle='rgba(0,242,255,.97)'; ctx.fillText('NEURAL CORE',cx,cy+27);
ctx.shadowBlur=0; ctx.restore();
}
requestAnimationFrame(draw);
})();
// ═══════════════════════════════════════════════════════
// BRAIN CANVAS β€” Cerebro neural animado (panel derecho)
// OPTIMIZACIΓ“N: nodos reducidos (60β†’40), distancia conexiΓ³n
// reducida (30β†’24), throttle a ~40fps
// Para mΓ‘s detalle: aumentar el 40 del while(brainNodes.length<40)
// ═══════════════════════════════════════════════════════
(function initBrain(){
var cv=document.getElementById('brainCvs'); if(!cv) return;
var ctx=cv.getContext('2d');
var W=198,H=168; cv.width=W; cv.height=H;
var t=0, brainNodes=[];
function inBrain(nx,ny){
var bcx=W/2,bcy=H*.42,dx=(nx-bcx)/(W*.44),dy=(ny-bcy)/(H*.36);
if(dx*dx+dy*dy<1) return true;
var ccx=bcx+W*.1,ccy=bcy+H*.3,ddx=(nx-ccx)/(W*.2),ddy=(ny-ccy)/(H*.13);
if(ddx*ddx+ddy*ddy<1) return true;
return false;
}
/* OPTIMIZACIΓ“N: 40 nodos (era 60) */
var attempts=0;
while(brainNodes.length<40 && attempts<1500){
attempts++;
var nx=W*.1+Math.random()*(W*.8), ny=H*.06+Math.random()*(H*.82);
if(inBrain(nx,ny)){
var dep=Math.random();
brainNodes.push({x:nx,y:ny,ox:nx,oy:ny,
wobbleX:(Math.random()-.5)*2.5,wobbleY:(Math.random()-.5)*1.8,
wobbleSpeed:.006+Math.random()*.014,wobblePhase:Math.random()*Math.PI*2,
r:1.4+Math.random()*2.4,depth:dep,fire:0,
fireTimer:Math.floor(Math.random()*120),
hue:195+Math.random()*20+(dep>.5?0:40),alpha:.4+Math.random()*.6});
}
}
/* OPTIMIZACIΓ“N: conexiΓ³n mΓ‘x a 24px (era 30) */
var edges=[];
for(var i=0;i<brainNodes.length;i++) for(var j=i+1;j<brainNodes.length;j++){
var dx2=brainNodes[i].x-brainNodes[j].x,dy2=brainNodes[i].y-brainNodes[j].y,d=Math.sqrt(dx2*dx2+dy2*dy2);
if(d<24&&Math.random()>.4) edges.push({a:i,b:j,len:d});
}
var pulses=[];
function BP(a,b){this.a=a;this.b=b;this.p=0;this.speed=.015+Math.random()*.022;this.done=false;}
BP.prototype.tick=function(){
this.p+=this.speed;
if(this.p>=1){this.done=true;brainNodes[this.b].fire=1;brainNodes[this.b].fireTimer=60+Math.random()*100;}
};
BP.prototype.draw=function(){
var na=brainNodes[this.a],nb=brainNodes[this.b];
var x=na.x+(nb.x-na.x)*this.p,y=na.y+(nb.y-na.y)*this.p;
var tx=na.x+(nb.x-na.x)*Math.max(0,this.p-.1),ty=na.y+(nb.y-na.y)*Math.max(0,this.p-.1);
ctx.save(); ctx.globalAlpha=.9-this.p*.35;
ctx.strokeStyle='rgba(120,210,255,.85)'; ctx.lineWidth=1.5;
ctx.shadowBlur=7; ctx.shadowColor='rgba(80,190,255,.9)';
ctx.beginPath(); ctx.moveTo(tx,ty); ctx.lineTo(x,y); ctx.stroke();
ctx.fillStyle='rgba(255,255,255,.95)'; ctx.shadowBlur=12; ctx.shadowColor='rgba(180,230,255,1)';
ctx.beginPath(); ctx.arc(x,y,2.2,0,Math.PI*2); ctx.fill();
ctx.shadowBlur=0; ctx.restore();
};
function drawBase(){
var bx=W*.5,by=H*.88;
for(var r=0;r<3;r++){ /* OPTIMIZACIΓ“N: 3 anillos (era 4) */
var rad=14+r*12,al=.22-r*.04,p=.4+Math.abs(Math.sin(t*.8+r*.6))*.4;
ctx.save(); ctx.globalAlpha=al*p;
ctx.strokeStyle='rgba(0,180,255,1)'; ctx.lineWidth=1.1-r*.18;
ctx.shadowBlur=5; ctx.shadowColor='rgba(0,180,255,.7)';
ctx.beginPath(); ctx.ellipse(bx,by,rad,rad*.2,0,0,Math.PI*2); ctx.stroke();
ctx.shadowBlur=0; ctx.restore();
}
}
function drawMesh(){
var cx2=W/2,cy2=H*.42;
var g=ctx.createRadialGradient(cx2,cy2,H*.1,cx2,cy2,H*.42);
g.addColorStop(0,'rgba(0,80,200,.1)'); g.addColorStop(1,'rgba(0,0,0,0)');
ctx.fillStyle=g; ctx.fillRect(0,0,W,H);
for(var ei=0;ei<edges.length;ei++){
var e=edges[ei],na=brainNodes[e.a],nb=brainNodes[e.b];
var fs=(na.fire+nb.fire)*.5,da=(na.depth+nb.depth)*.5,ba2=.04+da*.08+fs*.22;
ctx.save(); ctx.globalAlpha=Math.min(ba2,.55);
if(fs>.3){ctx.strokeStyle='rgba(170,225,255,'+(.35+fs*.5)+')';ctx.lineWidth=.7+fs*1.1;ctx.shadowBlur=3;ctx.shadowColor='rgba(100,200,255,.5)';}
else{ctx.strokeStyle='hsla('+(200+da*30)+',100%,62%,1)';ctx.lineWidth=.45+da*.4;}
ctx.beginPath();ctx.moveTo(na.x,na.y);ctx.lineTo(nb.x,nb.y);ctx.stroke();
ctx.shadowBlur=0; ctx.restore();
}
}
var lastBrain=0;
function draw(ts){
requestAnimationFrame(draw);
/* OPTIMIZACIΓ“N: throttle a ~40fps */
if(ts - lastBrain < 25) return;
lastBrain = ts;
ctx.clearRect(0,0,W,H); t+=.016;
var bg=ctx.createRadialGradient(W*.5,H*.38,0,W*.5,H*.38,H*.5);
bg.addColorStop(0,'rgba(0,55,180,'+(.04+Math.sin(t*.4)*.02)+')');bg.addColorStop(1,'rgba(0,0,0,0)');
ctx.fillStyle=bg; ctx.fillRect(0,0,W,H);
drawBase(); drawMesh();
for(var ni=0;ni<brainNodes.length;ni++){
var n=brainNodes[ni];
n.wobblePhase+=n.wobbleSpeed;
n.x=n.ox+Math.sin(n.wobblePhase)*n.wobbleX;
n.y=n.oy+Math.cos(n.wobblePhase*.7)*n.wobbleY;
n.fireTimer--;
if(n.fireTimer<=0){
n.fire=1; n.fireTimer=90+Math.random()*200;
var conn=[];
for(var ei2=0;ei2<edges.length;ei2++){if(edges[ei2].a===ni||edges[ei2].b===ni)conn.push(edges[ei2]);}
if(conn.length&&Math.random()>.45){var pk=conn[Math.floor(Math.random()*conn.length)];pulses.push(new BP(pk.a,pk.b));}
}
if(n.fire>0) n.fire=Math.max(0,n.fire-.014);
var sz2=n.r*(.6+n.depth*.45);
ctx.save(); ctx.globalAlpha=n.alpha*(.3+n.depth*.38+n.fire*.48);
if(n.fire>.1){ctx.beginPath();ctx.arc(n.x,n.y,sz2*3.2,0,Math.PI*2);ctx.fillStyle='hsla('+n.hue+',100%,68%,'+n.fire*.22+')';ctx.fill();ctx.shadowBlur=sz2*4.5;ctx.shadowColor='hsla('+n.hue+',100%,78%,1)';}
ctx.fillStyle=n.fire>.5?'rgba(255,255,255,'+(.7+n.fire*.3)+')':'hsla('+n.hue+',100%,74%,1)';
ctx.beginPath();ctx.arc(n.x,n.y,sz2,0,Math.PI*2);ctx.fill();ctx.shadowBlur=0;ctx.restore();
}
for(var pi2=pulses.length-1;pi2>=0;pi2--){pulses[pi2].tick();pulses[pi2].draw();if(pulses[pi2].done)pulses.splice(pi2,1);}
}
requestAnimationFrame(draw);
})();
// ═══════════════════════════════════════════════════════
// WORLD MAP β€” Mapa de conectividad mundial animado
// OPTIMIZACIΓ“N: puntos reducidos (~75β†’~55), throttle a ~30fps
// Para mΓ‘s/menos puntos del mapa: cambiar el 5to valor en specs
// ═══════════════════════════════════════════════════════
(function initWorld(){
var cv=document.getElementById('worldC'); if(!cv) return;
var ctx=cv.getContext('2d'); var W=190,H=52,t=0;
var cont=[];
/* OPTIMIZACIΓ“N: puntos reducidos ~35% */
var specs=[[10,42,4,22,10],[24,18,28,18,5],[72,26,3,44,13],[102,58,4,32,15],[148,22,32,14,5]];
for(var si=0;si<specs.length;si++){var s=specs[si];for(var sj=0;sj<s[4];sj++)cont.push([s[0]+Math.random()*s[1],s[2]+Math.random()*s[3]]);}
var hubs=[[30,14],[80,11],[130,9],[160,38],[55,40]];
var lastWorld=0;
function draw(ts){
requestAnimationFrame(draw);
/* OPTIMIZACIΓ“N: throttle a ~30fps */
if(ts - lastWorld < 33) return;
lastWorld = ts;
ctx.clearRect(0,0,W,H);
for(var ci=0;ci<cont.length;ci++){ctx.beginPath();ctx.arc(cont[ci][0],cont[ci][1],.9,0,Math.PI*2);ctx.fillStyle='rgba(0,120,200,.28)';ctx.fill();}
for(var i=0;i<hubs.length;i++) for(var j=i+1;j<hubs.length;j++){
var x1=hubs[i][0],y1=hubs[i][1],x2=hubs[j][0],y2=hubs[j][1],mid=[(x1+x2)/2,(y1+y2)/2-10];
ctx.beginPath();ctx.moveTo(x1,y1);ctx.quadraticCurveTo(mid[0],mid[1],x2,y2);ctx.strokeStyle='rgba(0,180,255,.07)';ctx.lineWidth=.7;ctx.stroke();
var pr=(Math.sin(t*1.5+i*1.2+j*.7)+1)/2,px=x1+(x2-x1)*pr,py=y1+(y2-y1)*pr-Math.sin(pr*Math.PI)*10;
ctx.beginPath();ctx.arc(px,py,1.8,0,Math.PI*2);ctx.fillStyle='rgba(0,230,255,.75)';ctx.fill();
}
for(var hi=0;hi<hubs.length;hi++){
var a=.5+Math.sin(t*2+hubs[hi][0])*.4;
ctx.beginPath();ctx.arc(hubs[hi][0],hubs[hi][1],3.5,0,Math.PI*2);ctx.fillStyle='rgba(0,255,150,'+a+')';ctx.shadowBlur=6;ctx.shadowColor='rgba(0,255,150,.8)';ctx.fill();ctx.shadowBlur=0;
ctx.beginPath();ctx.arc(hubs[hi][0],hubs[hi][1],6+Math.sin(t*3+hubs[hi][1])*2,0,Math.PI*2);ctx.strokeStyle='rgba(0,255,150,'+(a*.28)+')';ctx.lineWidth=.7;ctx.stroke();
}
t+=.016;
}
requestAnimationFrame(draw);
})();
</script>
"""
# ════════════════════════════════════════════════════════
# CHAT HANDLER β€” Procesamiento de mensajes y selecciΓ³n de motor
# ════════════════════════════════════════════════════════
def chat_engine_handler(history, message, mode, voice_enabled=False, pdf_file=None):
if not message or message.strip() == "":
return history, "", None
if history is None:
history = []
# ── Mapa de modos a motores de chat ──
engines = {
"general": regular_chat,
"web": browser_chat,
"productividad": productivity_chat,
"cientifico": scientist_chat,
"pdf": document_chat,
}
selected_engine = engines.get(mode, regular_chat)
try:
if mode == "pdf" and pdf_file:
gen = selected_engine(history, message, pdf_file)
else:
gen = selected_engine(history, message)
last_history, last_audio = None, None
for yielded in gen:
if isinstance(yielded, tuple) and len(yielded) == 2:
last_history, last_audio = yielded
else:
last_history = yielded
if last_history is None:
last_history = history + [[message, "Sin respuesta del sistema."]]
# ── Limpieza e imΓ‘genes markdown ──
clean_history = []
for entry in last_history:
if isinstance(entry, (list, tuple)) and len(entry) == 2:
user_msg = entry[0] if entry[0] is not None else ""
bot_msg = entry[1] if entry[1] is not None else ""
if bot_msg and "![" in str(bot_msg):
bot_msg = re.sub(
r'!\[([^\]]*)\]\(([^)]+)\)',
r'<img src="\2" alt="\1" style="max-width:100%;border-radius:10px;margin-top:8px;">',
str(bot_msg)
)
clean_history.append((str(user_msg), str(bot_msg)))
if not clean_history:
clean_history = list(history) + [(message, "No se recibio respuesta.")]
# ── SΓ­ntesis de voz opcional ──
audio_path = None
if voice_enabled and clean_history:
last_bot = clean_history[-1][1]
voice_text = re.sub(r'<[^>]+>', '', last_bot).replace("*","").replace("#","").replace("`","")
try:
audio_path = speak(voice_text[:450])
except Exception:
pass
return clean_history, "", audio_path
except Exception as e:
print("CRITICAL ERROR EN CVISION: " + str(e))
safe = list(history) if history else []
safe.append((message, "Error del sistema: " + str(e)))
return safe, "", None
# ════════════════════════════════════════════════════════
# TRANSCRIBE PROCESSOR β€” Convierte audio grabado a texto
# ════════════════════════════════════════════════════════
def transcribe_processor(audio):
if audio is None:
return ""
text = transcribe_audio_file(audio)
return text if text else ""
# ════════════════════════════════════════════════════════
# CREATE CHAT INTERFACE β€” Helper que crea el layout de chat
# Reutilizado por todos los tabs excepto PDF
# Layout:
# [Chatbot]
# Fila 1: [Textbox──────────] [Enviar] [Voz]
# Fila 2: [Mic/Audio────────────────] [Limpiar]
# ════════════════════════════════════════════════════════
def create_chat_interface(mode_id, placeholder_text=None):
ph = placeholder_text or PLACEHOLDER
with gr.Column():
# ── Ventana de chat ──
chat_window = gr.Chatbot(
height=490,
bubble_full_width=False,
elem_classes="chatbot-container",
show_label=False,
render_markdown=True,
show_copy_button=True,
avatar_images=(None, AVATAR_URL),
)
out_audio = gr.Audio(visible=False, autoplay=True)
# ── Fila 1: Textbox + Enviar + Voz ──
with gr.Row(elem_classes="input-row-cv"):
input_msg = gr.Textbox(
placeholder=ph,
scale=10,
show_label=False,
container=False,
)
btn_send = gr.Button(
"Enviar β•°β”ˆβž€",
variant="primary",
scale=2,
min_width=110,
elem_classes="btn-send-cv",
)
btn_voice = gr.Button(
"Voz πŸ€–πŸ”ŠαŠαŠ||၊",
variant="secondary",
scale=2,
min_width=80,
elem_classes="btn-voice-cv",
)
# ── Fila 2: MicrΓ³fono (vozβ†’texto) + Limpiar ──
with gr.Row(elem_classes="mic-row-cv"):
mic_tool = gr.Audio(
sources=["microphone"],
type="filepath",
label="TranscripciΓ³n de Voz",
scale=8,
elem_classes="mic-tool-cv",
)
btn_clear = gr.Button(
"⊠ Limpiar Conversación",
variant="stop",
scale=3,
min_width=160,
elem_classes="btn-clear-cv",
)
# ── Estado del modo activo ──
st_mode = gr.State(mode_id)
# ── Mic β†’ transcripciΓ³n automΓ‘tica en el textbox ──
mic_tool.stop_recording(
transcribe_processor,
inputs=[mic_tool],
outputs=[input_msg],
)
# ── Enviar mensaje ──
btn_send.click(
chat_engine_handler,
inputs=[chat_window, input_msg, st_mode, gr.State(False)],
outputs=[chat_window, input_msg, out_audio],
)
input_msg.submit(
chat_engine_handler,
inputs=[chat_window, input_msg, st_mode, gr.State(False)],
outputs=[chat_window, input_msg, out_audio],
)
# ── Enviar con sΓ­ntesis de voz ──
btn_voice.click(
chat_engine_handler,
inputs=[chat_window, input_msg, st_mode, gr.State(True)],
outputs=[chat_window, input_msg, out_audio],
)
# ── Limpiar conversaciΓ³n ──
btn_clear.click(
lambda: ([], "", None),
inputs=None,
outputs=[chat_window, input_msg, out_audio],
queue=False,
)
# ════════════════════════════════════════════════════════
# GRADIO INTERFACE β€” ConstrucciΓ³n de la interfaz principal
# ════════════════════════════════════════════════════════
with gr.Blocks(css=CUSTOM_CSS, title="CVision Intelligence | Neural OS") as assistant:
# ── SecciΓ³n visual superior ──
gr.HTML(HEADER_HTML)
gr.HTML(NEBULA_SECTION_HTML)
gr.HTML(STATUS_AND_JS_HTML)
# ── Tabs de mΓ³dulos ──
with gr.Tabs():
# ── Tab: NΓΊcleo Central (chat general) ──
with gr.Tab("πŸ’ πŸ’¬ CHAT GENERAL"):
create_chat_interface("general")
# ── Tab: Red Global (navegaciΓ³n web) ──
with gr.Tab("πŸŒπŸ›œ BÚSQUEDA WEB"):
create_chat_interface("web")
# ── Tab: Algoritmos (productividad) ──
with gr.Tab("πŸ“ˆπŸŽ― PRODUCTIVIDAD"):
create_chat_interface("productividad")
# ── Tab: Laboratorio (anΓ‘lisis cientΓ­fico) ──
with gr.Tab("βš—οΈπŸ”¬ CIENTIFICO"):
create_chat_interface("cientifico")
# ── Tab: AnΓ‘lisis de Matriz (PDF) ──
with gr.Tab("πŸ“‚πŸ”— ANÁLISIS DE PDF"):
with gr.Row():
# Columna de carga de archivo PDF
with gr.Column(scale=2):
gr.Markdown("### πŸ“₯ Ingesta de Archivos")
file_input = gr.File(
label="Documento PDF de Origen",
file_types=[".pdf"],
)
# Columna de chat PDF
with gr.Column(scale=5):
chat_pdf = gr.Chatbot(
height=450,
bubble_full_width=False,
elem_classes="chatbot-container",
show_label=False,
render_markdown=True,
avatar_images=(None, AVATAR_URL),
)
pdf_audio = gr.Audio(visible=False, autoplay=True)
# Fila 1: textbox + Ejecutar + Voz
with gr.Row(elem_classes="input-row-cv"):
txt_pdf = gr.Textbox(
placeholder="Consultar parΓ‘metros del documento...",
scale=10,
container=False,
)
btn_send_pdf = gr.Button(
"Ejecutar β•°β”ˆβž€",
variant="primary",
scale=2,
min_width=110,
elem_classes="btn-exec-cv",
)
btn_voz_pdf = gr.Button(
"Voz πŸ€–πŸ”ŠαŠαŠ||၊",
variant="secondary",
scale=2,
min_width=80,
elem_classes="btn-voice-cv",
)
# Fila 2: mic + limpiar
with gr.Row(elem_classes="mic-row-cv"):
mic_pdf = gr.Audio(
sources=["microphone"],
type="filepath",
label="TranscripciΓ³n de Voz",
scale=8,
elem_classes="mic-tool-cv",
)
btn_clr_pdf = gr.Button(
"βŒπŸ—‘οΈ Limpiar",
variant="stop",
scale=3,
min_width=100,
elem_classes="btn-clear-cv",
)
# Mic PDF β†’ textbox
mic_pdf.stop_recording(
transcribe_processor,
inputs=[mic_pdf],
outputs=[txt_pdf],
)
pdf_in = [chat_pdf, txt_pdf, gr.State("pdf"), gr.State(False), file_input]
pdf_inv = [chat_pdf, txt_pdf, gr.State("pdf"), gr.State(True), file_input]
pdf_out = [chat_pdf, txt_pdf, pdf_audio]
btn_send_pdf.click(chat_engine_handler, pdf_in, pdf_out)
txt_pdf.submit( chat_engine_handler, pdf_in, pdf_out)
btn_voz_pdf.click( chat_engine_handler, pdf_inv, pdf_out)
btn_clr_pdf.click(lambda: ([], "", None), None, pdf_out, queue=False)
# ── Tab 6: Modo Voz (JARVIS) ──
with gr.Tab("πŸŽ™οΈπŸŒŒ MODO VOZ"):
create_voice_tab()
# ════════════════════════════════════════════════════════
# SECCIΓ“N 07: EXPOSICIΓ“N ADAPTATIVA DE LA INTERFAZ
# Expone la interfaz pura de Gradio para que el orquestador controle el flujo.
# ════════════════════════════════════════════════════════
# Simplemente asignamos el bloque puro de Gradio sin envolturas pesadas
assistant_app = assistant