Download ui.py from AndresMV/AndyBot: direct link, hf CLI and curl.
- Browser
- Download file 21.2 kB
-
https://huggingface.co/spaces/AndresMV/AndyBot/resolve/main/ui.py
- Command line
-
hf download hf://spaces/AndresMV/AndyBot/ui.py
-
curl -L -o ui.py https://huggingface.co/spaces/AndresMV/AndyBot/resolve/main/ui.py
21.2 kB
| # ============================================================================= | |
| # ui.py β Interfaz Gradio para CV Interactivo Andres Marcelo Villarreal (VersiΓ³n Avatar) | |
| # ============================================================================= | |
| import gradio as gr | |
| from gradio.themes import Base | |
| import os | |
| import json | |
| with open("me/profile.json") as f: | |
| profile = json.load(f) | |
| # ββ ConfiguraciΓ³n de Preguntas y Rutas ββββββββββββββββββββββββββββββββββββββββββ | |
| # SUGGESTED_QUESTIONS = [ | |
| # "ΒΏCuΓ‘l es tu experiencia con sistemas crΓticos?", | |
| # "ΒΏQuΓ© stack tecnolΓ³gico dominas actualmente?", | |
| # "ΒΏCΓ³mo gestionas un equipo Γ‘gil como Scrum Master?", | |
| # "CuΓ©ntame sobre tus proyectos de IA y automatizaciΓ³n.", | |
| # "ΒΏQuΓ© certificaciones tienes en Cloud e IA?", | |
| # ] | |
| # Definimos la ruta del avatar (usando ruta absoluta para asegurar la carga en Gradio) | |
| AVATAR_PATH = os.path.abspath(os.path.join("assets", "avatar.png")) | |
| # Generic user avatar URL (Mystery Person de Gravatar) | |
| AVATAR_USER = "https://www.gravatar.com/avatar/00000000000000000000000000000000?d=mp&f=y" | |
| # Tu avatar local para el Bot | |
| AVATAR_BOT = os.path.join("assets", "avatar.png") | |
| # ββ CSS Maestro (Responsivo, Estilo Matrix + Avatar) βββββββββββββββββββββββ | |
| CUSTOM_CSS = """ | |
| /* Variables de Estilo */ | |
| :root { | |
| --bg-primary: #0f172a; /* azul oscuro elegante */ | |
| --bg-secondary: #111827; /* panel */ | |
| --border: #1f2937; | |
| --text-primary: #f9fafb; | |
| --text-secondary: #9ca3af; | |
| --accent: #3b82f6; /* azul moderno */ | |
| --accent-soft: rgba(59,130,246,0.1); | |
| --success: #10b981; | |
| } | |
| /* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| FIX CRΓTICO MOBILE #1 β Scroll global en iframe de HF Spaces | |
| El iframe de Hugging Face bloquea el scroll tΓ‘ctil si html/body | |
| tienen overflow implΓcito. Forzamos el modelo correcto. | |
| βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| html { | |
| height: 100%; | |
| overflow-y: auto !important; | |
| -webkit-overflow-scrolling: touch !important; | |
| background-color: #0f172a !important; | |
| } | |
| body { | |
| min-height: 100%; | |
| overflow-x: hidden !important; | |
| overflow-y: auto !important; | |
| -webkit-overflow-scrolling: touch !important; | |
| color: #f9fafb !important; | |
| background-color: #0f172a !important; | |
| } | |
| /* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| FIX CRΓTICO MOBILE #2 β Scroll tΓ‘ctil en el chatbot | |
| Gradio envuelve el historial en varios divs anidados. | |
| Cubrimos todos los selectores conocidos de Gradio 3.x / 4.x. | |
| βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| #chatbot-container, | |
| #chatbot-container > div, | |
| #chatbot-container .wrap, | |
| #chatbot-container .scroll-hide, | |
| #chatbot-container [data-testid="bot"], | |
| #chatbot-container .chatbot { | |
| overflow-y: auto !important; | |
| overflow-x: hidden !important; | |
| -webkit-overflow-scrolling: touch !important; /* iOS momentum scroll */ | |
| touch-action: pan-y !important; /* Permite scroll vertical tΓ‘ctil */ | |
| overscroll-behavior: contain !important; /* Evita que el scroll se propague al iframe padre */ | |
| } | |
| /* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| FIX CRΓTICO MOBILE #3 β Tablas en modo "espagueti" | |
| El problema: en mΓ³vil real, el contenedor del mensaje NO tiene | |
| overflow:auto, por lo que la tabla empuja el layout y se rompe. | |
| SoluciΓ³n en dos capas: | |
| a) El mensaje burbuja del bot se convierte en contenedor | |
| scrollable horizontal. | |
| b) La tabla se fuerza a bloque con overflow-x:auto propio. | |
| Usamos selectores amplios porque Gradio puede generar: | |
| .prose > table, .message > table, .bot > table, etc. | |
| βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| /* Capa a: burbuja del bot como contenedor scrollable */ | |
| #chatbot-container .message.bot, | |
| #chatbot-container .bot-row .message, | |
| #chatbot-container [data-testid="bot"] .message, | |
| #chatbot-container .bubble-wrap, | |
| #chatbot-container .prose { | |
| overflow-x: auto !important; | |
| -webkit-overflow-scrolling: touch !important; | |
| max-width: 100% !important; | |
| /* Evita que el texto del bot empuje el layout */ | |
| word-break: break-word !important; | |
| overflow-wrap: break-word !important; | |
| } | |
| /* Capa b: la tabla en sΓ, scrollable y con ancho mΓnimo por columna */ | |
| #chatbot-container table, | |
| #chatbot-container .prose table, | |
| #chatbot-container .message table, | |
| #chatbot-container .bot table { | |
| display: block !important; | |
| width: max-content !important; /* Se expande al ancho real de su contenido */ | |
| max-width: 100% !important; | |
| overflow-x: auto !important; | |
| -webkit-overflow-scrolling: touch !important; | |
| touch-action: pan-x pan-y !important; | |
| border-collapse: collapse !important; | |
| margin: 12px 0 !important; | |
| /* Scrollbar Matrix-style */ | |
| scrollbar-width: thin; | |
| scrollbar-color: var(--accent) transparent; | |
| } | |
| #chatbot-container table::-webkit-scrollbar { height: 4px; } | |
| #chatbot-container table::-webkit-scrollbar-thumb { | |
| background: var(--accent-dim); | |
| border-radius: 10px; | |
| } | |
| /* Celdas: ancho mΓnimo para que el contenido respire */ | |
| #chatbot-container table th, | |
| #chatbot-container table td { | |
| min-width: 120px !important; | |
| padding: 8px 12px !important; | |
| white-space: normal !important; | |
| word-break: normal !important; | |
| border: 1px solid var(--border) !important; | |
| vertical-align: top !important; | |
| font-size: 0.82rem !important; | |
| line-height: 1.4 !important; | |
| } | |
| #chatbot-container table th { | |
| background: rgba(0, 255, 65, 0.1) !important; | |
| color: var(--accent) !important; | |
| text-transform: uppercase; | |
| letter-spacing: 1px; | |
| white-space: nowrap !important; /* Cabeceras en una lΓnea para anclar el layout */ | |
| } | |
| /* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| FIX MOBILE #4 β Input area y teclado virtual (iOS / Android) | |
| En mΓ³vil, el teclado virtual reduce el viewport y el input | |
| queda oculto. position:sticky lo mantiene visible. | |
| βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| #input-area { | |
| background: var(--bg-secondary); | |
| border: 1px solid var(--border); | |
| border-radius: 12px; | |
| padding: 12px; | |
| } | |
| /* Evitar zoom automΓ‘tico en iOS al hacer focus (font-size < 16px dispara zoom) */ | |
| #msg-input textarea { | |
| background: #020617 !important; | |
| border: 1px solid var(--border) !important; | |
| color: var(--text-primary) !important; | |
| font-size: 16px !important; | |
| } | |
| /* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| ESTILOS BASE (sin cambios respecto a la versiΓ³n original) | |
| βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| .gradio-container { | |
| background: var(--bg-primary) !important; | |
| width: 100% !important; | |
| max-width: 1000px !important; | |
| margin: 0 auto !important; | |
| font-family: 'Inter', sans-serif !important; | |
| color: var(--text-primary) !important; | |
| } | |
| .contain { padding: 0 !important; } | |
| .gap { gap: 15px !important; } | |
| #cv-header { | |
| background: var(--bg-secondary); | |
| border: 1px solid var(--border); | |
| border-radius: 16px; | |
| padding: 32px; | |
| text-align: left; | |
| } | |
| #header-avatar { | |
| width: var(--avatar-size); | |
| height: var(--avatar-size); | |
| border-radius: 50%; | |
| object-fit: cover; | |
| border: 3px solid var(--accent); | |
| box-shadow: 0 0 25px rgba(0,255,65,0.5); | |
| margin-bottom: 20px; | |
| } | |
| #header-name { | |
| font-size: 2rem; | |
| font-weight: 700; | |
| color: var(--text-primary); | |
| } | |
| #header-title { | |
| color: var(--text-secondary); | |
| margin-top: 6px; | |
| } | |
| .badge-container { | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 8px; | |
| justify-content: center; | |
| } | |
| .skill-badge { | |
| background: var(--accent-soft); | |
| color: var(--accent); | |
| border-radius: 999px; | |
| padding: 6px 12px; | |
| font-size: 0.8rem; | |
| border: none; | |
| } | |
| .message.user { | |
| background: var(--accent); | |
| color: white; | |
| border-radius: 12px; | |
| } | |
| .message.bot { | |
| background: #1e293b; | |
| color: var(--text-primary); | |
| border-radius: 12px; | |
| } | |
| #chatbot-container { | |
| background: var(--bg-secondary) !important; | |
| border-radius: 16px !important; | |
| border: 1px solid var(--border) !important; | |
| padding: 10px; | |
| } | |
| #chatbot-container .message { | |
| font-size: 0.95rem !important; | |
| max-width: 100% !important; /* Burbujas no se salen del contenedor */ | |
| box-sizing: border-box !important; | |
| color: #f9fafb !important; | |
| } | |
| #suggestions-row { | |
| display: flex !important; | |
| flex-wrap: nowrap !important; | |
| overflow-x: auto !important; | |
| gap: 10px !important; | |
| padding: 5px 0 !important; | |
| scrollbar-width: none; | |
| } | |
| #suggestions-row::-webkit-scrollbar { display: none; } | |
| .suggestion-btn { | |
| flex: 0 0 auto !important; | |
| background: #1a1a1a !important; | |
| border: 1px solid var(--border) !important; | |
| color: var(--text-secondary) !important; | |
| border-radius: 20px !important; | |
| padding: 5px 15px !important; | |
| transition: all 0.3s ease !important; | |
| } | |
| .suggestion-btn:hover { | |
| border-color: var(--accent) !important; | |
| color: var(--accent) !important; | |
| box-shadow: var(--accent-glow); | |
| } | |
| #send-btn { | |
| background: var(--accent) !important; | |
| color: white !important; | |
| border-radius: 10px !important; | |
| font-weight: 500; | |
| } | |
| #send-btn:hover { | |
| background: #2563eb !important; | |
| } | |
| #clear-btn { | |
| background: transparent !important; | |
| color: #ff5555 !important; | |
| border: 1px solid #442222 !important; | |
| border-radius: 8px !important; | |
| } | |
| /* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| RESPONSIVE BREAKPOINTS | |
| βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| @media (max-width: 600px) { | |
| :root { --avatar-size: 80px; } | |
| .gradio-container { width: 100% !important; padding: 0 !important; } | |
| #cv-header { padding: 20px 10px; border-radius: 0; } | |
| #input-area { border-radius: 0; padding: 12px; } | |
| /* En mΓ³vil las burbujas ocupan casi todo el ancho */ | |
| #chatbot-container .message { | |
| max-width: 92% !important; | |
| } | |
| /* Reducir altura del chatbot en pantallas pequeΓ±as | |
| para que el input quede siempre visible sin scroll de pΓ‘gina */ | |
| #chatbot-container { | |
| height: 55dvh !important; /* dvh: dynamic viewport height, descuenta el teclado */ | |
| min-height: 280px !important; | |
| border-radius: 0 !important; | |
| } | |
| /* Celdas aΓΊn mΓ‘s compactas en mΓ³vil */ | |
| #chatbot-container table th, | |
| #chatbot-container table td { | |
| min-width: 90px !important; | |
| font-size: 0.75rem !important; | |
| padding: 6px 8px !important; | |
| } | |
| } | |
| @media (max-width: 380px) { | |
| :root { --avatar-size: 68px; } | |
| #header-name { letter-spacing: 2px; } | |
| } | |
| """ | |
| # ββ HTML de Componentes (Ahora con la imagen) βββββββββββββββββββββββββββ | |
| # Usamos file/ prefixed path para Gradio local. | |
| def get_header_html(profile): | |
| return f""" | |
| <div id="cv-header"> | |
| <h1 id="header-name">{profile['name']}</h1> | |
| <p id="header-title">{profile['title']}</p> | |
| <div class="badge-container"> | |
| {''.join(f'<span class="skill-badge">{skill}</span>' for skill in profile['skills'])} | |
| </div> | |
| </div> | |
| """ | |
| def get_footer_html(profile): | |
| return f""" | |
| <div style="text-align: center; color: #333; font-size: 0.7rem; margin-top: 15px; font-family: sans-serif;"> | |
| CV INTERACTIVO β’ {profile['name']} β’ AI Agent | |
| </div> | |
| """ | |
| # ββ ConstrucciΓ³n de la UI ββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def build_ui(chat_fn): | |
| # ββ JS: Viewport meta + table-wrapper inyectados en runtime ββββββββββββ | |
| # Gradio en HF Spaces no expone el <head> directamente. | |
| # gr.HTML con <script> se ejecuta dentro del body pero es suficiente | |
| # para aΓ±adir el viewport meta (si no existe) y envolver tablas. | |
| MOBILE_FIX_JS = """ | |
| <script> | |
| (function() { | |
| /* 1. Viewport meta β crΓtico para escala correcta en mΓ³vil real */ | |
| if (!document.querySelector('meta[name="viewport"]')) { | |
| var m = document.createElement('meta'); | |
| m.name = 'viewport'; | |
| m.content = 'width=device-width, initial-scale=1.0'; | |
| document.head.appendChild(m); | |
| } | |
| /* 2. Envolver tablas en un div scrollable. | |
| CSS puede hacer overflow:auto en la tabla, pero en algunos builds | |
| de Gradio el elemento padre tiene overflow:hidden implΓcito. | |
| Envolvemos cada tabla con un div dedicado que hace scroll. */ | |
| function wrapTables() { | |
| var chatbot = document.getElementById('chatbot-container'); | |
| if (!chatbot) return; | |
| chatbot.querySelectorAll('table').forEach(function(table) { | |
| if (table.parentElement.classList.contains('tbl-scroll-wrap')) return; | |
| var wrap = document.createElement('div'); | |
| wrap.className = 'tbl-scroll-wrap'; | |
| wrap.style.cssText = [ | |
| 'overflow-x:auto', | |
| '-webkit-overflow-scrolling:touch', | |
| 'touch-action:pan-x pan-y', | |
| 'max-width:100%', | |
| 'margin:12px 0', | |
| 'border-radius:4px' | |
| ].join(';'); | |
| table.parentNode.insertBefore(wrap, table); | |
| wrap.appendChild(table); | |
| table.style.margin = '0'; | |
| }); | |
| } | |
| /* 3. Observer: detecta nuevas burbujas del bot y envuelve sus tablas */ | |
| var observer = new MutationObserver(function(mutations) { | |
| mutations.forEach(function(m) { | |
| if (m.addedNodes.length) wrapTables(); | |
| }); | |
| }); | |
| /* Esperamos a que Gradio monte el DOM */ | |
| document.addEventListener('DOMContentLoaded', function() { | |
| var target = document.getElementById('chatbot-container') || document.body; | |
| observer.observe(target, { childList: true, subtree: true }); | |
| wrapTables(); | |
| }); | |
| /* Fallback por si DOMContentLoaded ya paso */ | |
| if (document.readyState !== 'loading') { | |
| setTimeout(function() { | |
| var target = document.getElementById('chatbot-container') || document.body; | |
| observer.observe(target, { childList: true, subtree: true }); | |
| wrapTables(); | |
| }, 500); | |
| } | |
| })(); | |
| </script> | |
| """ | |
| with gr.Blocks( | |
| css=CUSTOM_CSS, | |
| title=f"{profile['name']} β CV Interactivo", | |
| theme=Base() | |
| ) as app: | |
| # 0. JS de correcciones mobile (se inyecta primero, antes del DOM visible) | |
| gr.HTML(MOBILE_FIX_JS) | |
| # 1. Encabezado (con Avatar) | |
| gr.HTML(get_header_html(profile)) | |
| # 2. Chatbot | |
| chatbot = gr.Chatbot( | |
| value=[{"role": "assistant", "content": f"Β‘Hola! Soy un asistente basado en el perfil de {profile['name']}. ΒΏQuΓ© te gustarΓa saber?"}], | |
| elem_id="chatbot-container", | |
| height=450, | |
| show_label=False, | |
| # avatar_images=( | |
| # None, # usuario β sin avatar | |
| # # None, # bot β sin avatar (usamos ::before CSS) | |
| # # Para habilitar tu foto real descomenta: | |
| # "assets/avatar.jpg", | |
| # ), | |
| avatar_images=(AVATAR_USER, AVATAR_BOT), # Orden: (Usuario, Bot) | |
| # type="messages", | |
| ) | |
| # # 3. Sugerencias | |
| # with gr.Row(elem_id="suggestions-row"): | |
| # for q in SUGGESTED_QUESTIONS: | |
| # btn = gr.Button(q, elem_classes=["suggestion-btn"], size="sm") | |
| # # LΓ³gica para autocompletar el textbox | |
| # btn.click(fn=lambda x=q: x, outputs=[gr.State(q)]).then( | |
| # fn=None, js=f"(q) => {{ document.querySelector('#msg-input textarea').value = '{q}'; }}" | |
| # ) | |
| # 4. Γrea de Entrada | |
| with gr.Column(elem_id="input-area"): | |
| msg = gr.Textbox( | |
| placeholder="Escribe tu pregunta aquΓ (ej. ΒΏCuΓ‘l es su experiencia en Java?)...", | |
| show_label=False, | |
| lines=1, | |
| elem_id="msg-input", | |
| max_lines=3, | |
| autofocus=True | |
| ) | |
| with gr.Row(): | |
| # clear_btn = gr.Button("βΊ LIMPIAR", elem_id="clear-btn", scale=1) | |
| send_btn = gr.Button("ENVIAR", elem_id="send-btn", scale=4) | |
| gr.HTML(get_footer_html(profile)) | |
| # ββ LΓ³gica de InteracciΓ³n ββ | |
| # def respond(message, history): | |
| # if not message or message.strip() == "": | |
| # return "", history | |
| # # Obtener respuesta del agente | |
| # reply = chat_fn(message, history) | |
| # # Actualizar historial | |
| # history.append({"role": "user", "content": message}) | |
| # history.append({"role": "assistant", "content": reply}) | |
| # return "", history | |
| def user_message(message, history): | |
| # Agrega el mensaje del usuario a la historia inmediatamente | |
| # y devuelve un string vacΓo para limpiar el input | |
| return "", history + [{"role": "user", "content": message}] | |
| def bot_message(history, chat_fn): | |
| # Toma el ΓΊltimo mensaje del usuario (ya presente en history) | |
| user_query = history[-1]["content"] | |
| # Llama a tu funciΓ³n de IA | |
| bot_response = chat_fn(user_query, history[:-1]) | |
| # Agrega la respuesta del bot a la historia | |
| history.append({"role": "assistant", "content": bot_response}) | |
| return history | |
| # Eventos de envΓo | |
| # --- LΓ³gica de InteracciΓ³n Fluida --- | |
| # 1. Definimos quΓ© pasa cuando el usuario envΓa | |
| submit_event = msg.submit( | |
| fn=user_message, | |
| inputs=[msg, chatbot], | |
| outputs=[msg, chatbot], | |
| queue=False # Importante para que sea instantΓ‘neo | |
| ).then( | |
| fn=lambda h: bot_message(h, chat_fn), | |
| inputs=[chatbot], | |
| outputs=[chatbot] | |
| ) | |
| # 2. Lo mismo para el botΓ³n de enviar | |
| send_btn.click( | |
| fn=user_message, | |
| inputs=[msg, chatbot], | |
| outputs=[msg, chatbot], | |
| queue=False | |
| ).then( | |
| fn=lambda h: bot_message(h, chat_fn), | |
| inputs=[chatbot], | |
| outputs=[chatbot] | |
| ) | |
| # msg.submit(respond, [msg, chatbot], [msg, chatbot]) | |
| # send_btn.click(respond, [msg, chatbot], [msg, chatbot]) | |
| # Evento de limpieza | |
| # clear_btn.click( | |
| # lambda: (None, [{"role": "assistant", "content": "ConversaciΓ³n reiniciada. ΒΏEn quΓ© mΓ‘s puedo ayudarte?"}]), | |
| # outputs=[msg, chatbot] | |
| # ) | |
| return app | |
| # Testing Standalone | |
| if __name__ == "__main__": | |
| def mock_chat(msg, hist): return f"Has preguntado: **{msg}**. Esta es una respuesta de prueba simulando al agente." | |
| # # IMPORTANTE: AVISAR A GRADIO QUE PERMITA EL ACCESO A LA CARPETA 'assets' | |
| # build_ui(mock_chat).launch(allowed_paths=[os.path.dirname(AVATAR_PATH)]) |