Novedades · Changelog

Cada semana, un poco mejor.

Mejoramos GarBotGPT casi a diario. Estas son las novedades más recientes: nuevas herramientas, mejoras de velocidad y arreglos, contadas en claro.

  1. v7.38.0

    Carpetas avanzadas: color, favorito, anidación, compartir y plantillas (DB + UI)

    Completados los items de v7.37.0 que necesitaban migración de schema. Ahora cada carpeta puede tener un color de acento (13 paletas), marcarse como favorita (sube arriba en el sidebar), anidarse bajo otra carpeta (parent_folder_id), tener un banner image, y existir como "smart folder" con filtros dinámicos. Además se añaden dos tablas nuevas con RLS completo: `folder_shares` para compartir carpetas con otros usuarios (roles viewer/editor/admin) y `folder_templates` para guardar plantillas reutilizables (públicas o privadas). UI integrada en CreateFolderModal y FolderSettingsModal con color picker y toggle de favorito.

    Nuevo

    • Favoritas suben al principio del sidebar automáticamente.

    Mejorado

    • 🎨 13 colores de acento por carpeta (sky, violet, blue, emerald, amber, rose, indigo, teal, orange, pink, cyan, lime, red).
    • 🪺 Anidación: cada carpeta puede tener un `parent_folder_id` (DB lista, UI próxima).
    • 🖼️ Banner image opcional por carpeta (DB lista, UI próxima).
    • 🧠 Smart folders con filtros JSON (DB lista, UI próxima).

    Seguridad

    • 🔗 Compartir carpeta con otro usuario con roles viewer/editor/admin (DB + RLS lista).
    • 📦 Templates de carpeta — guardar y reutilizar configuración (DB + RLS lista).
    • 🛡️ RLS completo en folder_shares y folder_templates con función helper folder_shared_role().

    Schema (Supabase)

    • folders: + parent_folder_id (FK self-ref ON DELETE SET NULL), accent_color, is_favorite, banner_url, smart_filters_json.
    • CHECK constraint folders_no_self_parent (carpeta no puede ser su propia padre).
    • Indices: idx_folders_parent_id (partial WHERE NOT NULL), idx_folders_user_favorite (partial WHERE is_favorite).
    • Tabla folder_shares (folder_id, user_id, role, invited_by, UNIQUE(folder_id, user_id)).
    • Tabla folder_templates con use_count, is_public, todos los campos de carpeta.
    • Función SECURITY DEFINER folder_shared_role(folder_id, user_id) → text.
    • Policies RLS completas en folder_shares (owner+admin gestionan, invitados ven y pueden salirse).
    • Policies RLS completas en folder_templates (own + public read).
    • Policies adicionales en folders: folders_shared_select, folders_shared_update.
    • Trigger touch_updated_at en folder_templates.

    Types (src/lib/types.ts)

    • export type FolderAccentColor (13 valores).
    • Folder ampliada con parent_folder_id, accent_color, is_favorite, banner_url, smart_filters_json.
    • export type FolderShareRole, interface FolderShare.
    • export interface FolderTemplate.

    folder-visuals.tsx — paleta de colores

    • Nuevo FOLDER_ACCENTS record con 13 paletas (icon/soft/dot/gradient/ring por color).
    • export FOLDER_ACCENT_LIST y normalizeFolderAccent() helper.
    • FolderIconBadge ahora acepta prop `accent` para colorear dinámicamente.
    • Backwards-compat: folderAccent legacy ahora apunta a la paleta sky.

    CreateFolderModal — color + favorite

    • Nuevo state accent (sky default) y isFavorite.
    • Color picker con 13 swatches circulares + Check overlay.
    • Toggle de favorito con icono Star + dot indicador.
    • Preview superior muestra el badge con el color elegido + estrella si favorito.
    • onSubmit acepta payload {name, icon, instructions, accent_color, is_favorite} o la firma 3-args legacy (compat).
    • Validación de nombre duplicado se mantiene.

    FolderSettingsModal — color + favorite

    • Hidrata accent + isFavorite del folder existente.
    • Sección nueva "Color de acento" con 13 swatches grandes.
    • Sección nueva "Carpeta favorita" con toggle Star.
    • saveFolder envía accent_color e is_favorite a supabase.
    • Evento geia-folder-updated incluye accent_color e is_favorite para que el sidebar pueda re-render sin recargar.

    Sidebar (Sidebar.tsx + SidebarSleek.tsx)

    • createFolder typed con CreateFolderFn (union: payload | 3-args legacy) usando useCallback<T>.
    • Carpetas se cargan ordenadas por is_favorite DESC primero.
    • FolderIconBadge en cada fila usa folder.accent_color.
    • Estrella amber al lado del nombre si is_favorite.

    Pendiente (UI para próximas releases)

    • Anidación: árbol jerárquico en el sidebar (parent_folder_id ya existe en DB).
    • Compartir carpeta: modal de invitar usuarios (folder_shares ya existe en DB).
    • Templates: galería de plantillas en CreateFolderModal (folder_templates ya existe en DB).
    • Smart folders: editor de filtros JSON.
    • Banner image: upload UI.
  2. v7.37.0

    Carpetas: paquete masivo (búsqueda, drag-drop, export, activity log, fixes…)

    Repaso a fondo del sistema de carpetas con 11 mejoras y arreglos en una sola release. Resumen: pinned arriba, buscador con ⌘F, drag-drop para reordenar, export a JSON/Markdown, sparkline de actividad de los últimos 7 días, chips de tools por defecto en el header, validación inline de nombre duplicado al crear, "Indexar ahora" para ficheros sin indexar, cache scoped por usuario para evitar fugas entre cuentas, atajos de teclado, stats con tamaño total de documentos. Las funciones que requieren cambio de schema (anidación, compartir carpeta, templates, color, banner, smart folders, favorito) quedan documentadas para la siguiente release tras confirmar la migración.

    Nuevo

    • 🔍 Búsqueda inline de chats dentro de la carpeta (⌘F).
    • 🤚 Drag-drop para reordenar chats — persiste folder_sort_order automáticamente.
    • 📌 Pinned chats agrupados arriba con header propio "Fijados".
    • 📥 Export a JSON (full backup con mensajes) o Markdown (legible).
    • 📊 Activity log: sparkline 7 días + count "este mes".
    • 🔧 Chips de "Activas:" mostrando las tools por defecto de la carpeta en el hero.
    • ⚠️ Validación inline de nombre duplicado al crear carpeta — antes el toast desde el padre te hacía perder lo escrito.
    • 🚀 Botón "Indexar ahora" (gradient violeta) para ficheros con ingest_status=none.
    • ⌨️ Atajos: ⌘F enfoca búsqueda, ⌘, abre Settings, Esc limpia búsqueda.
    • 💾 Stats: tamaño total de documentos (KB/MB/GB), último chat, count este mes.

    Seguridad

    • 🔒 Cache de useFolderContext scoped por user_id (antes era global, fuga entre cuentas).

    FolderWelcomeScreen.tsx — rewrite

    • Buscador con focus-ring sky, atajo ⌘F, cierre con Esc, kbd visible cuando vacío.
    • Drag-drop nativo HTML5 con persist en lote: reorder dispara N updates a `conversations.folder_sort_order`. La fila arrastrada se opaca, la dropZone se resalta sky.
    • Pinned chats van a sección propia "Fijados" arriba con divider y header amber. Las pinned no se reordenan (siempre van arriba), solo las "Recientes" son arrastrables.
    • Stats hero ahora muestran chats + último activo, documentos + tamaño total, memoria, knowledge.
    • Sparkline de últimos 7 días (gradient sky → bars proporcional al máximo) + count "X chats este mes" con icono trending.
    • Chips de default_tools en el hero ("Activas: Web · Code · Imagen…") con sparkle violeta.
    • Botón "Exportar" con menú JSON/Markdown.
    • - JSON: backup completo con folder + stats + conversaciones + mensajes.
    • - Markdown: legible, con prompt de carpeta arriba y cada chat formateado.
    • Atajos ⌘F (search), ⌘, (settings), Esc (clear search).

    CreateFolderModal.tsx — validación

    • Nuevo prop `existingNames: string[]`.
    • Validación case-insensitive contra duplicados.
    • Input se pone con border rose + ring rose cuando hay duplicado.
    • Mensaje inline "Ya tienes una carpeta con ese nombre. Elige otro."
    • Botón submit cambia su label a "Nombre duplicado" y se deshabilita.

    FolderSettingsModal.tsx — Indexar ahora

    • El botón "Reindexar" cambia a "Indexar ahora" cuando `ingest_status === "none"` (o vacío).
    • Style gradient violet→fuchsia con sombra y scale al hover, para destacar que ese fichero NO está indexado todavía.
    • Fichero con status `done`/`failed`/`queued` mantiene el botón neutro como antes.

    useFolderContext.ts — cache por usuario

    • folderCache pasa de `Map<id, {name, icon}>` a `Map<id, {userId, name, icon}>`.
    • Lookup invalida la entrada si el slot.userId no coincide con el currentUserId actual.
    • currentUserId se obtiene de supabase.auth.getUser() en el primer fetch.
    • Listener `geia-folder-updated` también escribe el userId actual.
    • Resultado: si cambias de cuenta sin recargar, no ves los nombres/iconos del usuario anterior.

    Sidebar.tsx — pasa existingNames

    • CreateFolderModal recibe `existingNames={folders.map(f => f.name)}`.
    • La validación duplicado del sidebar (toast al crear) sigue funcionando, pero ahora el modal valida ANTES de enviar — no se pierde lo escrito.

    Pendiente (requiere migración de schema)

    • F1 — Carpetas anidadas (parent_folder_id en `folders`).
    • F2 — Compartir carpeta (tabla `folder_shares` + permisos).
    • F3 — Templates (tabla `folder_templates`).
    • F9 — Smart folders / vistas dinámicas (tabla o JSON de filtros).
    • M2/B7 — Color de acento por carpeta (`accent_color` en `folders`).
    • M3 — Banner image (`banner_url` en `folders`).
    • M6 — Favorito (`is_favorite` en `folders`).
    • B1 — Unificar carpetas personales y de proyecto (refactor profundo).
    • F6 — Auto-classification (no requiere schema, pero sí endpoint AI).
    • F11 — Herencia de padre (depende de F1).
  3. v7.36.0

    Draft del chat IA persiste también en chat nuevo (sin id)

    ChatInput ya guardaba el draft por conversación en localStorage, pero sólo cuando había `activeConversationId`. Si abrías un chat NUEVO (sin enviar nada todavía → no hay id) y refrescabas la página, lo escrito se perdía. Fix: usamos el slot `__new__` cuando no hay conversación activa, así el draft sobrevive a refrescos antes del primer envío. Al mandar el primer mensaje (que crea la conversación), limpiamos tanto el slot del nuevo id como el slot `__new__` para que no quede basura.

    Nuevo

    • 💾 Chat nuevo: lo escrito se conserva tras refrescar.
    • 💾 Slot dedicado `geia:chat-draft:__new__` para chats sin id.
    • 🧹 Al enviar desde un chat nuevo, se limpian ambos slots (el `__new__` y el del nuevo id) para evitar drafts huérfanos.
    • El comportamiento existente (draft por conversación abierta) se mantiene intacto.

    Cambios en ChatInput.tsx

    • draftStorageKey: `${PREFIX}${activeConversationId || "__new__"}` (antes devolvía null cuando no había id).
    • Los useEffect de restore y persist ya no early-return con null — siempre operan.
    • handleSubmit captura `wasNewChat = !convId` antes de createConversation.
    • Tras éxito en chat nuevo, además de limpiar `draftStorageKey`, también limpia explícitamente `${PREFIX}__new__` para cubrir el race entre createConversation y el re-render del useMemo.
    • Misma limpieza en la rama de message queue (cuando se mete en cola).
  4. v7.35.0

    Draft de canal persiste al refrescar + Voz: vuelve la transcripción del usuario

    Dos fixes: (1) En canales, lo que escribes en el composer ahora persiste en localStorage por canal — al refrescar la página, recuperas el texto donde lo dejaste. Antes se perdía. (2) En el modo voz, la transcripción de TUS mensajes había dejado de aparecer en el chat. Causa: el modelo `gpt-4o-mini-transcribe` con prompt (introducido en v7.19) era rechazado silenciosamente por OpenAI en algunos casos, dejando de enviar el evento `conversation.item.input_audio_transcription.completed`. Vuelta a `whisper-1` (modelo estable de toda la vida) sin prompt + log explícito si OpenAI vuelve a rechazar la config.

    Nuevo

    • 💾 Composer del canal: tu draft sobrevive a refrescos.
    • 💾 El draft es por canal — cada canal tiene el suyo en localStorage (`channel-draft:{channel.id}`).
    • 💾 Al enviar, el draft se borra del localStorage automáticamente.
    • 💾 Al cambiar de canal, se rehidrata el draft del nuevo.
    • 🎙️ Voz: transcripción del usuario vuelve a aparecer en el chat.
    • 🎙️ Modelo de transcripción revertido de `gpt-4o-mini-transcribe` → `whisper-1` (estable, universal).
    • 🎙️ Eliminado el prompt experimental que reforzaba vocabulario — algunos session.update eran rechazados con esa combinación.

    Mejorado

    • 🪵 Log explícito en consola si OpenAI vuelve a rechazar el `input_audio_transcription` para diagnóstico rápido.

    ChannelView — draft persistido

    • useState con lazy init lee `localStorage.getItem(`channel-draft:${channel.id}`)`.
    • useEffect sync escribe en localStorage cada vez que `input` cambia (vacío → remove, no-vacío → set).
    • useEffect on `channel.id` change rehidrata el draft del nuevo canal.
    • handleSend limpia la entrada del localStorage al enviar.
    • Sin coste perceptible (localStorage es síncrono y barato; sin debounce).

    useRealtimeVoice — fix transcripción usuario

    • input_audio_transcription.model: `gpt-4o-mini-transcribe` → `whisper-1`.
    • Eliminado el campo `prompt` (vocabulario custom). Era experimental y a veces hacía que OpenAI rechazara todo el session.update.
    • Síntoma de la regresión: el usuario hablaba, GIA respondía con voz, pero la burbuja del usuario se quedaba con `…` (placeholder) porque `conversation.item.input_audio_transcription.completed` nunca llegaba → handleTranscript nunca se ejecutaba → la burbuja nunca se actualizaba ni se persistía.
    • Log nuevo en case "error": si el mensaje contiene "transcription" o "input_audio_transcription", warning explícito en consola.
  5. v7.34.0

    Canales: el chat dentro del canal también con estilo nivel dios

    Continuación de v7.33: ahora los mensajes, el composer y los empty states dentro de un canal abierto también tienen el rediseño completo. Avatares de mensaje 10×10 con presence ring animado (verde online, azul typing) + glow detrás, badges "Tú", "IA", "Admin" con gradients, indicador "escribiendo" con dots animados, reactions con gradient violet→fuchsia cuando reaccionaste tú, contador de hilo como pill blue→cyan, toolbar hover con backdrop-blur + iconos coloreados al hover, composer con focus ring violeta y botón send gradient violet→fuchsia→pink con glow, empty state del canal con shimmer + sparkle.

    Nuevo

    • 🦸 Avatar del mensaje: 10×10 con ring de presencia animado (online verde, typing azul) + glow blur detrás.
    • ⌨️ Badge "Tú" en los mensajes propios (indigo pill).
    • 🤖 Badge "IA" gradient violet→fuchsia con logo.
    • 👑 Badge "Admin" gradient amber→orange con corona.
    • ✍️ Badge "escribiendo" con 3 dots animados cuando un usuario está typing.
    • 😀 Reacciones: gradient violet→fuchsia cuando es tuya, pill blanca con backdrop-blur cuando no.
    • 🧵 Contador de hilo: pill blue→cyan con flecha al hover.
    • 🪟 Toolbar hover: rounded-xl + ring + shadow elevada + iconos que cambian a color al hover (smile→amber, reply→blue, pin→amber, link→violet, trash→rose).
    • 😀 Quick emoji picker: rounded-2xl + emojis 18px + scale 1.25 al hover.
    • Empty state del canal: hero icon 80×80 con halo violet→fuchsia→pink, título con shimmer animado, "Sé el primero en escribir 👋".
    • 📂 Botón "Cargar mensajes anteriores": glassy con backdrop-blur + spinner violeta + chevron.
    • 🔢 Contador de chars en el composer pasa a rosa cuando >1800.

    Mejorado

    • Composer: bg blanco/85 con backdrop-blur, focus ring violeta + shadow violeta, botones que escalan, send con gradient violet→fuchsia→pink + glow potente.
    • ⌨️ Hint de @ con kbd visual.

    ChannelView — mensajes

    • Animation entrada: cubic-bezier(0.34, 1.56, 0.64, 1) bounce.
    • Hover: gradient sutil indigo (mensaje propio) o zinc (otros).
    • Avatar 10×10 con ring 2px presence-aware + glow blur detrás.
    • Toolbar hover: rounded-xl, backdrop-blur-xl, shadow elevada, divider antes de Edit/Delete.
    • Quick emoji picker: rounded-2xl con channel-chip-in animation.

    ChannelView — empty state

    • Hero icon 80×80 con halo gradient blur
    • Título #channel.name con shimmer animado
    • Mensaje secundario "Sé el primero en escribir 👋"
    • channel-chip-in animation al entrar

    ChannelView — composer

    • Container glassy: bg-white/85 + backdrop-blur-xl, shadow soft.
    • Focus state: ring violet 2px + shadow violet.
    • Attachment button: rounded-xl, hover scale 1.10, color violet.
    • Send button: gradient violet→fuchsia→pink cuando activo, shadow violet, scale al hover, scale-95 active.
    • Spinner cuando uploading/sending.
    • Hint de @ con kbd visual.
    • Counter rosa cuando >1800 chars.

    ChannelView — reactions

    • Pills con channel-chip-in animation entrada.
    • Reacción propia: gradient violet→fuchsia + ring violeta + shadow.
    • Otras: bg-white/80 backdrop-blur + ring zinc.
    • Emoji 14px + count tabular-nums.
    • Hover: scale 1.10.

    ChannelView — thread badge

    • Pill gradient blue→cyan + ring + flecha →
    • hover scale 1.02
  6. v7.33.0

    Canales: rediseño visual nivel dios

    Repaso a fondo de la sección /channels: aside con orbes flotantes y blur liquid-glass, header con gradient violet→fuchsia→pink, búsqueda más refinada con shortcut visible (⌘K), tabs en píldoras con conteos, fila de canal con glow lateral pulsante cuando está activa, halo en el icono al hover, badges animados (mention shake + pulse), avatares de personas con presence ring que pulsa en verde cuando online o azul cuando typing (con dots animados), header del canal abierto con icono más grande y halo, empty state hero con shimmer en el título y orbes de fondo gigantes.

    Nuevo

    • 🪐 Aside con 3 orbes flotantes detrás (violet, blue, pink) con animación independiente.
    • Botón "+" con gradient violet→fuchsia→pink, escala y glow al hover.
    • ⌘K visible en el placeholder de búsqueda + ring focus violeta.
    • 🎯 Tabs como píldoras con contador (canales: total, personas: count online con dot pulsante).
    • 🌟 Fila de canal activa: glow lateral 3px pulsante con gradient + ring violeta.
    • 🌀 Icono del canal con halo difuso al hover (escala 1.06 + rotate -3°).
    • 🚨 Badge de menciones: gradient rose→pink, pulse + shake.
    • 🔔 Badge de no leídos: gradient violet→indigo, pulse.
    • 🟢 Avatares de personas: ring que pulsa verde cuando online, azul cuando typing.
    • ⌨️ "Typing" con 3 dots animados al lado del estado.
    • 👑 Tag "Admin" amarillo + corona dorada.
    • 🏆 Empty state: orb gigante 600px detrás, hero icon con halo violet→pink, title con shimmer animado, 3 chips Canales/Personas/DMs, hint "⌘K para buscar".

    Mejorado

    • 🎨 Header del canal abierto: icono más grande (40×40) con halo, gradient en el badge "Privado", chip de miembros emerald con dot.

    CSS — globals.css

    • 13 nuevos @keyframes: channel-orb-float-1/2/3, channel-active-pulse, channel-badge-pulse, channel-mention-shake, channel-presence-ring-online, channel-presence-ring-typing, channel-empty-shimmer, channel-chip-in, channel-skeleton-sweep, channel-header-glow.
    • Clases utility nuevas: channels-aside-orb-1/2/3, channel-row-active-bar, channel-badge-mention/unread, channel-presence-online/typing, channel-icon-halo, channel-empty-shimmer-title, channel-chip-in, channel-skeleton, channel-header-glow.
    • prefers-reduced-motion respeta todas las animaciones nuevas.

    Página /channels (page.tsx)

    • Aside ahora bg-white/85 + backdrop-blur-xl con 3 orbes detrás.
    • Header del aside con icono "MessageSquarePlus" en gradient violet→fuchsia→pink y título con gradient text.
    • Empty state nuevo: orbes detrás (600px y 280px y 320px), hero icon Hash 80×80 con halo, sparkle flotante decorativo, título con shimmer, 3 FeatureChip (Canales / Personas / DMs), hint con kbd ⌘K.

    ChannelList (sidebar)

    • Header con padding mayor, separator más sutil, gradient suave de fondo.
    • Search input con ring focus violeta, kbd ⌘K visible cuando vacío, X cuando hay query.
    • Tabs reescritas como píldoras dentro de un container ring + contadores.
    • Filtro de menciones reescrito como pill con gradient rose→pink al activar.
    • Empty states con icon hero + halo + texto secundario.
    • Filas de canal: padding mayor, rounded-xl, transición de translate-x al hover, halo en el icono, descripción inline.
    • Filas de personas: avatar 9×9, presence ring animado verde/azul, dots de typing, tag "Admin" amarillo, → arrow al hover.

    ChannelView (header del canal abierto)

    • Glow detrás del header (gradient ellipse violet + blue) animado.
    • Icono del canal: 40×40 con halo blur, ring violeta, gradient bg.
    • Hash icon en violeta brillante.
    • Tag "Privado" como pill amarilla con shield icon.
    • Chip de miembros: gradient emerald→teal con icono de Users colored.
  7. v7.32.0

    Fix: la card de imagen se duplicaba al terminar la generación

    En v7.25 hicimos persistir la card efímera (`activeImageGenerationRun`) hasta que terminara la imagen. En v7.30 hicimos que el bubble del mensaje persistido también renderizara la card. Resultado: cuando la imagen se attachaba al mensaje, durante unos instantes había DOS cards visibles — la efímera y la del mensaje persistido. Fix: ChatArea oculta la efímera si detecta que la URL ya está en algún mensaje persistido (en meta_json.generated_image_url, meta_json.image_url o attachments_json.kind=generated_image). Además, ChatInput recarga los mensajes inmediatamente tras éxito para que el persistido aparezca rápido.

    Nuevo

    • 🚫 Sin más duplicados: la card efímera desaparece cuando el mensaje persistido la incluye.
    • 🔍 Match por URL exacta entre `imageGenerationRun.imageUrl` y los 3 sitios donde la imagen puede estar persistida.
    • 🛡️ Si la imagen aún no está persistida (race), la efímera sigue mostrándose — sin parpadeo.

    Mejorado

    • ChatInput.tsx hace `loadMessages(convId, { silent: true })` justo después de éxito para acelerar la transición efímero → persistido.

    Cambios en ChatArea.tsx

    • `activeImageGenerationRun` useMemo ahora también depende de `messages`.
    • Cuando run.status === "ready" y run.imageUrl, busca la URL en cada mensaje del conversationId.
    • Match en 3 sitios: meta_json.image_url, meta_json.generated_image_url, attachments_json[kind=generated_image].url.
    • Si encuentra match → devuelve null (oculta la card efímera). Si no → sigue mostrando la efímera.

    Cambios en ChatInput.tsx

    • Tras `setImageGenerationRun({ status: "ready", ... })`, llama a `loadMessages(convId, { silent: true })`.
    • Esto trae el assistant message con la imagen attachada (meta_json + attachments_json) al store.
    • Una vez en el store, ChatArea detecta el match por URL y oculta la efímera.
  8. v7.31.0

    Las frases del centro del preview ahora rotan también

    En v7.29 hicimos rotar el subtítulo de la cabecera de la card, pero las dos líneas grandes del centro del preview ("Renderizando preview" + "La imagen aparecerá aquí al completarse") seguían fijas. Ahora rotan cada 2.2s entre 10 pares (Renderizando preview, Mezclando colores, Esculpiendo formas, Encendiendo luces, Tejiendo detalles, Afinando contraste, Capturando matices, Pintando capas, Casi lista, Empaquetando imagen). Cada cambio entra con un fade-in con blur.

    Nuevo

    • 🔄 10 pares de frases rotativas en el centro del preview (cada 2.2s).
    • Animación fade-in con blur al cambiar de frase (0.7s cubic-bezier).
    • prefers-reduced-motion respetado.

    Arreglado

    • ⏸️ Sólo rotan en estado `running`. En error muestra "Generación detenida" + el error real.
  9. v7.30.0

    Fix: la imagen al refrescar salía como "archivo 0 B" + ahora aparece en /imagenes

    Tras refrescar el chat la imagen generada salía como un attachment genérico (clip de papel "archivo 0 B") en vez de la card de imagen. Causa: desde v7.22 el endpoint /api/image/generate guarda la imagen en `meta_json.generated_image_url`, pero MessageBubble (`getImageUrl`) y la página /imagenes seguían leyendo el campo legacy `meta_json.image_url`. Resultado: el url no se encontraba, no se renderizaba la card, y el attachment con `kind: "generated_image"` caía al renderizador genérico. Fix: leer ambos campos (legacy + nuevo + attachments_json) y filtrar el attachment generated_image para que no se muestre como archivo genérico.

    Nuevo

    • 🖼️ La imagen ahora reaparece como CARD al refrescar (antes salía como attachment "archivo 0 B").
    • 🗂️ Las imágenes nuevas YA aparecen en la página /imagenes (galería).
    • 🔍 `getImageUrl(message)` busca en 3 sitios: meta_json.image_url (legacy), meta_json.generated_image_url (v7.22+), y attachments_json[] con kind="generated_image".
    • 🚫 Filtrado: el attachment con `kind: "generated_image"` se excluye del listado de attachments del bubble — ya se renderiza arriba en la card, no debe duplicarse como archivo.
    • 🏷️ El badge del modelo en la card persistida ahora viene del registro real (antes estaba hardcoded a "gpt-image-1").
    • 📝 Nueva función `getImageModel(message)` simétrica a getImageUrl/getImagePrompt.

    Causa raíz

    • Pre-v7.22 (cuando la imagen se generaba inline en /api/chat) los campos eran `image_url`/`image_prompt` en meta_json.
    • En v7.22 movimos la generación a /api/image/generate, que escribe en `generated_image_url`/`generated_image_prompt` y añade un `attachments_json[].kind: "generated_image"`.
    • Pero MessageBubble.tsx:getImageUrl y ImagesGalleryPage.tsx seguían leyendo SÓLO los campos legacy. Resultado: las imágenes generadas desde v7.22 nunca persistían visualmente.
    • Adicional: el attachment generated_image llegaba al renderizador genérico de archivos sin file_id ni size → mostraba "archivo / 0 B" con icono de clip.

    Cambios en MessageBubble.tsx

    • getImageUrl: lee meta_json.image_url || meta_json.generated_image_url || attachments_json[kind=generated_image].url.
    • getImagePrompt: misma lógica con prompt.
    • getImageModel: nueva función para el badge del modelo.
    • rawAttachments: filtra los `kind: "generated_image"` para que no se rendericen como attachment genérico.
    • ImageGenerationCard recibe modelLabel del registro real (no hardcoded).

    Cambios en ImagesGalleryPage.tsx (/imagenes)

    • Query incluye attachments_json además de meta_json.
    • Lookup en 3 sitios: meta_json.image_url legacy, meta_json.generated_image_url, attachments_json[kind=generated_image].url.
    • Las imágenes generadas desde v7.22 ahora aparecen en la galería del usuario.
    • Las pre-v7.22 (legacy) siguen apareciendo (compat hacia atrás).
  10. v7.29.0

    Card de generación de imagen con más fases y muchas más partículas

    Durante la generación, la card ahora rota por 13 fases (antes 5) cambiando de mensaje cada 1.5s, así el admin no ve la misma frase 25s seguidos. Triplicado el campo de partículas (10 → 30) con más variación. Añadidas tres capas visuales nuevas: una segunda órbita interior rotando al contrario, un glow central pulsante, y 10 sparkles que parpadean en sitios fijos. El timer interno actualiza ahora cada 250ms (era 420ms) para animación más fluida.

    Nuevo

    • 🌀 Doble órbita: una exterior rotando 360° y otra interior al revés.
    • 10 sparkles que parpadean (twinkle) en sitios fijos del card, además de las partículas que flotan.
    • 💫 30 partículas (era 10) con sizes 3-7px y delays/durations variados — más densidad de magia.
    • 🌟 Glow central pulsante rosa/violeta detrás de todo, escala de 0.9 a 1.15.
    • ⏱️ Las fases rotan por TIEMPO (cada 1.5s) además de por progreso. Si el modelo tarda 25s ya no se queda atascado en la misma frase.
    • 🎯 prefers-reduced-motion respetado en todos los nuevos efectos.

    Mejorado

    • 🔄 13 fases en la subtítulo (Preparando prompt, Interpretando idea, Eligiendo paleta, Componiendo escena, Trazando volúmenes, Generando composición, Iluminando, Refinando detalles, Suavizando bordes, Ajustando contraste, Optimizando, Guardando imagen, Preparando resultado).
    • Timer interno: 420ms → 250ms para animaciones más suaves.

    Cambios en ImageGenerationCard.tsx

    • PHASES: 5 → 13 elementos.
    • PARTICLES: 10 → 30 con más variación de tamaños (3-7px) y delays (0-2.5s).
    • Nuevo array SPARKLES con 10 sparkles fijos que parpadean.
    • phaseIndex ahora combina progress (mínimo) + tick (Math.floor(elapsedMs/1500) % PHASES.length) para animar continuamente.
    • Tick del timer: 420ms → 250ms.

    Cambios en globals.css

    • Nuevo @keyframes image-generation-orbit-pulse-reverse: rotación 360° en sentido contrario.
    • Nuevo @keyframes image-generation-glow-pulse: opacidad 0.35→0.7 + scale 0.9→1.15.
    • Nuevo @keyframes image-generation-sparkle-twinkle: aparición/rotación con escala variable.
    • Clases CSS .image-generation-particle-orbit-inner, .image-generation-glow, .image-generation-sparkle.
    • prefers-reduced-motion ampliado para incluir todos los nuevos elementos.
  11. v7.28.1

    Hotfix: build fallaba por maxDuration=800 en Hobby

    En v7.28.0 puse `maxDuration = 800` (máximo de Pro), pero Vercel Hobby sólo admite valores entre 1 y 300, así que el deploy fallaba con "Builder returned invalid maxDuration value for Serverless Function api/image/generate. Serverless Functions must have a maxDuration between 1 and 300 for plan hobby". Bajado a 300 (máximo de Hobby) — sigue siendo más que suficiente para cualquier modelo de imagen (gpt-image-1 ~25s, dall-e-3 ~12s).

    Nuevo

    • 🔧 maxDuration: 800 → 300 (máximo permitido en Vercel Hobby).
    • Build vuelve a pasar.
    • 💪 300s = 5min sigue siendo holgadísimo para cualquier modelo de imagen.
  12. v7.28.0

    Quitadas TODAS las limitaciones de tiempo en generación de imagen

    Eliminados los AbortSignal internos que cortaban la generación antes de tiempo. Ahora el único límite es el del runtime de Vercel (800s declarados, máximo del plan Pro). En Hobby la plataforma sigue capando a 10s a nivel de infraestructura — eso no se puede tocar desde el código. En Pro y dev local, el modelo puede tardar lo que necesite (gpt-image-1 25s, dall-e-3 12s, etc.) y se entrega sin cortar.

    Nuevo

    • ⏱️ `maxDuration = 60` → `800` (máximo de Vercel Pro). En Hobby sigue siendo 10s a nivel de plataforma.
    • ✂️ Eliminado `AbortSignal.timeout(55_000)` de la llamada a OpenAI Images. Antes cortaba a los 55s aunque tuviéramos 800s disponibles.
    • ✂️ Eliminado `AbortSignal.timeout(2500)` del prompt assist. Si gpt-4o-mini tarda más, esperamos.
    • 🧹 `callImage(model)` simplificado — ya no recibe `timeoutMs`.
    • ⚠️ Hobby (10s hardcap): gpt-image-1 (10-30s) seguirá fallando. Si estás en Hobby, usa dall-e-2 o sube a Pro.

    Cambios en /api/image/generate

    • maxDuration: 60 → 800 (Pro max). En Hobby la plataforma sigue capando a 10s independientemente de lo declarado.
    • Quitado el AbortSignal del fetch principal a OpenAI Images. Sin recorte interno.
    • Quitado el AbortSignal del prompt assist (gpt-4o-mini). Si tarda, esperamos.
    • callImage() ya no recibe timeoutMs como argumento.
    • El catch global sigue manejando errores de red por si Vercel mata la función — pero ya no hay timeout *nuestro* que la corte.

    Lo que sigue limitando

    • **Vercel Pro**: 800s máximo declarados. La función puede correr hasta 13min antes de que Vercel la mate.
    • **Vercel Hobby**: 10s máximo a nivel de plataforma, no se puede subir desde el código. gpt-image-1 (10-30s) no cabrá. Solución: subir a Pro o usar dall-e-2 (2-5s).
    • **Dev local**: sin límites de plataforma. La función corre hasta que el modelo termine.
    • **OpenAI**: el SDK/API de OpenAI tiene sus propios timeouts internos (~10min). Si el servidor de OpenAI tarda más que eso, esa llamada falla del lado de ellos.
  13. v7.27.0

    El modelo del panel admin se RESPETA en el chat (sin fallback silencioso ni placeholder engañoso)

    Tres bugs en cascada hacían pensar al admin que su selección no se aplicaba: (1) El bubble del chat enseñaba `gpt-image-1` por defecto durante la generación — un placeholder visual que no reflejaba la config real. (2) El v7.26 hacía fallback silencioso a dall-e-2 si el modelo principal fallaba — el admin selecciona gpt-image-2, ve dall-e-2 (o gpt-image-1) y cree que su config no se aplica. (3) El dropdown del panel mostraba el primer modelo detectado en vez del realmente guardado cuando el guardado no estaba en la lista de OpenAI. Fix: el bubble lee `/api/image/config` y muestra el modelo configurado desde el primer momento; el fallback silencioso eliminado (si el modelo del admin falla, se devuelve el error real); el dropdown muestra siempre el valor guardado.

    Nuevo

    • 🎯 El bubble ahora muestra el modelo CONFIGURADO durante la generación (no el placeholder `gpt-image-1`).
    • 📦 Nuevo endpoint público `GET /api/image/config` (cualquier usuario logueado): devuelve `{ model, size, enabled }`. SIN api_key.
    • 🔄 ChatInput hace `fetch /api/image/config` justo antes de generar, actualiza el bubble con el modelo correcto.
    • 🪟 Dropdown del panel admin: el valor mostrado es siempre el guardado (antes caía a `models[0]` si no estaba detectado).

    Mejorado

    • ⚠️ Si el modelo configurado falla por timeout, ahora el error dice claramente: "La generación con [modelo] tardó demasiado. Cambia el modelo en /admin/image-generation a uno más rápido".

    Arreglado

    • 🚫 Eliminado el fallback silencioso a dall-e-2. Si el admin selecciona gpt-image-2, se usa gpt-image-2 (o se devuelve el error real).

    Por qué confundía

    • Admin selecciona gpt-image-2 en /admin/image-generation y guarda → config.generation.model = "gpt-image-2".
    • Admin manda mensaje "hazme una imagen de Barcelona" → el bubble aparece con badge "GPT-IMAGE-1" porque era el placeholder default en `ChatArea.tsx` y `ImageGenerationCard.tsx`.
    • En v7.26 además, si gpt-image-2 fallaba (timeout, model not found), se llamaba a dall-e-2 silenciosamente. La respuesta llevaba `model: "dall-e-2"` aunque el admin pidió otra cosa.
    • Conclusión del admin: "mi selección no se aplica, está cogiendo gpt-image-1" — confusión total.

    Endpoint /api/image/config

    • GET, runtime nodejs, requiere auth de usuario (no admin) — porque cualquier usuario que pueda generar imagen tiene derecho a saber qué modelo se usa.
    • Lee `app_settings.image_generation` igual que `/api/image/generate`, pero devuelve sólo `{ model, size, enabled }`.
    • NO devuelve api_key, api_url, ni extra_params — campos sensibles permanecen sólo en el endpoint admin.

    ChatInput.tsx — modelo en el bubble desde el inicio

    • Antes: setImageGenerationRun({ status: "running", ... }) sin model → bubble usaba el default "gpt-image-1".
    • Ahora: la IIFE async hace primero un `fetch /api/image/config`. Si responde con un modelo, se actualiza el bubble con `model: cfgModel`.
    • El bubble pasa de "GPT-IMAGE-1" placeholder a mostrar el modelo real configurado en menos de 1s.
    • Si /api/image/config falla (raro), el bubble muestra el badge null = sin badge. Mejor que mentir.

    Eliminado fallback silencioso

    • En v7.26 añadimos fallback automático a dall-e-2 cuando el modelo principal fallaba. Era para arreglar el "tardó demasiado" que apareció en v7.25.
    • Pero el admin lo vio como traición a su config: "selecciono gpt-image-2 y me pone dall-e-2".
    • En v7.27 eliminado. El modelo configurado se llama una sola vez con timeout 55s. Si falla, error real al admin.
    • Si quiere un modelo más rápido, puede cambiarlo en /admin/image-generation. El nuevo error message lo sugiere explícitamente.

    Panel admin — dropdown muestra el valor guardado

    • Antes: si el modelo guardado (gpt-image-2) no estaba en la lista detectada por OpenAI, el `<select>` mostraba `models[0]` (gpt-image-1) → confusión total.
    • Ahora: el valor mostrado es siempre el guardado. Si no está en la lista detectada, aparece como primera opción con label "X (personalizado)".
    • El admin puede seguir cambiando a otro modelo de la lista o mantener su valor custom.
  14. v7.26.0

    Fallback automático a dall-e-2 si gpt-image-1 falla / tarda demasiado

    En v7.25.0 dejamos visible el error real cuando la generación de imagen fallaba — el usuario empezó a ver "La generación tardó demasiado" porque `gpt-image-1` (default desde v7.23.0) tarda 10-30s y muchas veces excede el budget de la función serverless, además de requerir Verified Organization en OpenAI. Solución: fallback automático a `dall-e-2` (2-5s, no requiere verificación) cuando el modelo principal falla por timeout o error. También bajado el timeout interno del modelo principal a 28s para dejar margen al fallback dentro de los 60s declarados, y el prompt assist a 2.5s para no quemar budget si OpenAI va lento.

    Nuevo

    • ⏱️ Timeout principal bajado de 55s a 28s. dall-e-2 con 22s adicionales = 50s totales, dentro del budget de 60s en Pro.
    • 🧰 `buildPayload(model, size)` adaptativo no sólo en quality, ahora también en size: si el size guardado no es válido para el modelo (ej. dall-e-3 con 256x256) lo normaliza a 1024x1024.
    • 🏷️ La respuesta ahora incluye `model` (el realmente usado), `requested_model` (el pedido) y `fallback_used: true` cuando hubo fallback. El bubble muestra el modelo correcto.

    Mejorado

    • Timeout del prompt assist bajado de 4s a 2.5s. Si gpt-4o-mini va lento, se salta y se usa el prompt original.

    Arreglado

    • 🔁 Fallback automático: si `gpt-image-1` (o cualquier modelo configurado) falla por timeout o error 4xx/5xx → se reintenta con `dall-e-2` automáticamente.
    • 📝 Mensaje de error más informativo: "La generación tardó demasiado (probado fallback dall-e-2 también)" si ambos fallan, antes era genérico.

    Por qué fallaba antes

    • gpt-image-1 (default desde v7.23) requiere Verified Organization en OpenAI. Cuentas sin verificar reciben 403 inmediato.
    • gpt-image-1 además tarda 10-30s. En Vercel Hobby (10s cap) no cabe. En Pro cabe pero a veces excede el AbortSignal de 55s.
    • En v7.25 hicimos persistir la burbuja, así que el usuario por fin vio el error real que antes desaparecía con la burbuja.

    Estrategia del fallback

    • Helper `callImage(model, timeoutMs)` extrae la lógica de llamar a OpenAI. Devuelve `{ ok, data, modelUsed }` o `{ ok: false, status, detail, isTimeout }`.
    • Primer intento: el modelo configurado por el admin con timeout 28s.
    • Si falla y el modelo NO era ya `dall-e-2` → segundo intento con `dall-e-2` (timeout 22s). dall-e-2 es el más rápido y disponible para todas las cuentas.
    • Si ambos fallan → devuelve el error del primario. Si el primero es timeout, el mensaje incluye "(probado fallback dall-e-2 también)".

    Cambios en el endpoint

    • src/app/api/image/generate/route.ts: refactor a helper + fallback automático.
    • Prompt assist: 4s → 2.5s timeout.
    • Tamaños inválidos para el modelo seleccionado se normalizan automáticamente a 1024x1024.
    • Respuesta JSON ampliada con `requested_model` y `fallback_used` para el cliente.
  15. v7.25.0

    Fix DEFINITIVO: la burbuja de imagen ya no desaparece al acabar el chat

    En v7.23.0 quitamos el `setTimeout(clearImageGenerationRun, 1400)` del finally del chat creyendo que era el bug que mataba la burbuja, pero la verdad estaba en `ChatArea.tsx` línea 1138: el render era `{isStreamingHere && activeImageGenerationRun && (...)}`. Cuando el chat termina (3-5s), `isStreamingHere` pasa a false y la burbuja se desmonta — incluso aunque la imagen siga generándose en /api/image/generate (10-30s). Resultado: el usuario veía "Creando imagen / 24%" y a los segundos desaparecía sin imagen. Fix: quitada la condición `isStreamingHere &&`. La burbuja ahora vive mientras haya un `activeImageGenerationRun`, independiente del estado de streaming del chat.

    Nuevo

    • 🎯 Causa raíz REAL (que se nos escapó en v7.23.0): el render de la burbuja en ChatArea.tsx estaba gateado por `isStreamingHere &&`.
    • ⏱️ Timing: chat 3-5s, imagen 10-30s. Cuando el chat acaba la burbuja se desmonta sin esperar a la imagen.

    Arreglado

    • ✂️ Fix de 1 línea: `{isStreamingHere && activeImageGenerationRun && (...)}` → `{activeImageGenerationRun && (...)}`.
    • 🔄 La IIFE async de /api/image/generate sigue actualizando el run a `ready`/`error` y la burbuja ahora SÍ recibe ese update porque ya está montada.
    • 🧠 Lección: en v7.23.0 buscamos el bug en ChatInput (el clear) en vez de en el render condicional. Ambas cosas mataban la burbuja, pero quitar el clear no es suficiente si el render se desmonta solo.

    ChatArea.tsx — render condicional

    • Antes: `{isStreamingHere && activeImageGenerationRun && (...)}` — la burbuja vivía sólo mientras el chat streameaba.
    • Ahora: `{activeImageGenerationRun && (...)}` — la burbuja vive mientras haya un run activo.
    • El run se sobrescribe automáticamente cuando el usuario manda otra solicitud de imagen, así que no hay leak.
    • Cuando el run pasa a `ready` con `imageUrl`, el ImageGenerationCard ya muestra la imagen final con badge "Completado".

    Por qué el fix de v7.23.0 no fue suficiente

    • En v7.23.0 quitamos `setTimeout(clearImageGenerationRun, 1400)` del finally de ChatInput.
    • Pero el render condicional en ChatArea seguía haciendo `isStreamingHere &&`. Resultado: aunque el run en el store seguía vivo (status=running), la UI lo escondía al acabar el chat.
    • Pensábamos que el clear era el único responsable porque coincidía visualmente: chat acaba ≈ 3-5s, clear era a +1.4s del finally → unmount visual a 4-6s. Pero quitando el clear, el unmount seguía ocurriendo a chat-end por el render condicional.
    • Ahora ambos fixes en sintonía: no hay clear en el finally + el render no depende de `isStreamingHere`.
  16. v7.24.0

    Modelos de imagen detectados desde la API de OpenAI (no más hardcoded)

    El selector de modelo del panel `/admin/image-generation` ya no muestra una lista hardcoded de 3 modelos: ahora hace una llamada a `/v1/models` con la api_key del admin y filtra a los que generan imagen (`gpt-image-*`, `dall-e-*`). Si la cuenta tiene acceso a modelos nuevos aparecen automáticamente. Si la API falla o no hay key, usa los 3 conocidos como fallback. Botón refrescar al lado del select para volver a pedir la lista cuando se cambia la api_key.

    Nuevo

    • 🛰️ Nuevo endpoint GET `/api/admin/image-generation/models?target=generation|edit` (admin-only).
    • 🔑 Lee `api_url` + `api_key` del bloque correcto en `app_settings.image_generation`. Fallback a `OPENAI_API_KEY` env si no hay key configurada.
    • 🧪 Filtra los modelos a los que generan imágenes: id que empieza por `gpt-image` o `dall-e`. Resto descartado.
    • 🪂 Si la API falla (401, timeout, network) → devuelve los 3 modelos conocidos con `fallback: true` y el panel muestra un hint explicando que está en modo fallback.
    • 🔄 Botón refrescar (icono ↻) al lado del select. Útil tras guardar una nueva api_key sin recargar la página.
    • Loading state mientras se piden los modelos (spinner en el botón refrescar, select disabled).
    • 🛟 Si el modelo guardado no aparece en la lista detectada, se muestra como `xxx (no detectado)` para que el admin lo vea pero pueda cambiarlo.
    • 🏷️ Orden estable: `gpt-image-1` primero, luego otros gpt-image, luego dall-e-3, dall-e-2 y el resto alfabéticamente.

    Endpoint /api/admin/image-generation/models

    • GET con verifyAdmin pattern (auth user + check `profile.role === "admin"`).
    • Lee la config existente en `app_settings.image_generation` para extraer api_url y api_key del target solicitado (`generation` o `edit`). Si el target no tiene key, prueba el otro bloque. Si tampoco, fallback a `process.env.OPENAI_API_KEY`.
    • Llama a `${api_url}/models` con `AbortSignal.timeout(10_000)` y `cache: "no-store"`.
    • Devuelve `{ models: [{ id, label }], fallback: boolean }`. Si fallback=true incluye `reason` (no_api_key, fetch_error, openai_error, no_image_models_in_account).
    • Filtro `isImageModel(id)`: matchea `gpt-image*` o `dall-e*`. Si en el futuro OpenAI lanza otra familia (ej. `imagen-x`) se puede ampliar fácilmente.

    Panel admin (ImageGenerationPanel)

    • Reemplazado `OPENAI_IMAGE_MODELS` constante por `genModels` y `editModels` state — uno por cada select (Crear / Editar imagen).
    • Dos `useEffect` que llaman a `fetchModels(target)` cuando la tab está activa, ya cargó la config, y el engine es `openai`.
    • Nuevo helper `<ModelSelectWithRefresh>` que combina select + botón refrescar. Disabled mientras carga, spinner en el botón.
    • Si el modelo guardado no está en la lista detectada, se añade al final del select con label `xxx (no detectado)` — así no se pierde la selección y el admin sabe que no está validado.
    • Hint del row "Modelo" cambia según `fallback`: si fallback=true dice "No se pudo consultar la API — mostrando modelos por defecto", si false dice "Modelos detectados desde tu cuenta de OpenAI".

    Por qué este cambio

    • En v7.23.0 hicimos el select con 3 modelos hardcoded para evitar typos como `gpt-image-2` (que no existe).
    • Pero hardcodear significa que si OpenAI lanza un modelo nuevo, hay que recompilar. Tampoco respeta cuentas con modelos custom o de partners.
    • Solución pedida por el usuario: "vamos hacerlo diferente que haga llamada a la API de OpenIA para ver los modelos de imagenes que tenemos disponibles de el usuario por su API y salgan en el selector de modelo".
    • Ahora la lista refleja exactamente lo que la api_key tiene acceso. Si OpenAI lanza `gpt-image-2` mañana, aparece sin redeploy.
  17. v7.23.0

    Fix CRÍTICO: la imagen aparecía y luego desaparecía sin generarse

    La versión 7.23.0 arregla 3 bugs en cadena que impedían generar imágenes desde v7.22.0: (1) ChatInput tenía un setTimeout(clearImageGenerationRun, 1400) en el finally del chat — antes de v7.22.0 era inocuo porque la imagen se generaba inline y ya estaba lista, ahora corre en paralelo (10-30s) y la limpieza llegaba a 1.4s tras el chat (3-5s) → la burbuja desaparecía mientras la imagen seguía generándose en background. (2) El modelo default era `gpt-image-1.5` que NO existe en OpenAI — los modelos reales son `gpt-image-1`, `dall-e-3`, `dall-e-2`. Cualquier llamada con el default fallaba con "model not found". (3) El payload era estático con `quality: "auto"` — `dall-e-2` no acepta ese param. Ahora payload adaptativo por modelo. Bonus: el input del modelo en el admin pasa a dropdown con sólo los 3 modelos reales para evitar typos.

    Nuevo

    • ✂️ Causa raíz #1: `clearImageGenerationRun(id, 1400ms)` en el finally del chat. Pre-v7.22 la imagen ya estaba lista al acabar el chat. Post-v7.22 está corriendo en paralelo y la limpieza la mataba.
    • 🏷️ Badge correcto: el bubble ahora muestra el modelo REAL devuelto por la API (no el hardcoded `gpt-image-1.5`).

    Mejorado

    • 👻 Causa raíz #2: defaults `gpt-image-1.5` (NO existe) en `chat/route.ts`, `/api/image/generate`, y 4 sitios de UI. OpenAI Images sólo acepta `gpt-image-1`, `dall-e-3`, `dall-e-2`.

    Arreglado

    • 🚨 Bug visible (foto del usuario): "Creando imagen / 24%" aparecía → tras unos segundos la burbuja desaparecía sin imagen.
    • 🎯 Fix #1: eliminado el `setTimeout(clearImageGenerationRun)` del finally del chat. La burbuja ya se gestiona desde la IIFE async de la generación (status running → ready/error).
    • 🎯 Fix #2: defaults cambiados a `gpt-image-1` en backend Y UI. Si el admin nunca ha guardado config, ahora funciona.
    • 🎛️ Fix #3: input del modelo en `/admin/image-generation` pasa a dropdown con los 3 modelos OpenAI reales (cuando engine=openai). Para engines custom (A1111, ComfyUI, etc.) sigue siendo input libre.
    • 🧬 Fix #4: payload adaptativo por modelo — `dall-e-2` no recibe `quality`, `dall-e-3` recibe `quality: standard`, `gpt-image-1` recibe `quality: auto`. Antes era `quality: auto` para todos → dall-e-2 fallaba.

    Bug #1 — clear prematuro

    • Antes: `setTimeout(() => clearImageGenerationRun(runId), 1400)` en el finally del POST a /api/chat.
    • En v7.21.0 y anteriores el chat hacía la imagen inline → cuando el chat terminaba, la imagen YA estaba lista. El clear de 1.4s sólo daba tiempo a ver el "done" antes de quitar la burbuja.
    • En v7.22.0 movimos la imagen a `/api/image/generate` paralelo. El chat tarda 3-5s, la imagen 10-30s. El clear se disparaba a `chat_end + 1.4s ≈ 5-7s` → mataba la burbuja mientras la imagen seguía generándose.
    • Fix: eliminado el clear. La IIFE async que llama a /api/image/generate ya pone `setImageGenerationRun({ status: "ready" / "error" })` cuando termina. La burbuja persiste hasta que el usuario navega o se descarga la imagen.

    Bug #2 — modelo default inexistente

    • El default era `gpt-image-1.5` en 6 sitios (chat/route.ts, image/generate/route.ts, ChatArea.tsx ×2, ImageGenerationCard.tsx, MessageBubble.tsx). Modelo INVENTADO — no existe en la API de OpenAI.
    • Modelos reales OpenAI Images (mayo 2025): `gpt-image-1` (recomendado, lanzado abril 2025), `dall-e-3`, `dall-e-2`.
    • Cuando el admin no había guardado config Y se intentaba generar → 400 "model not found" → la IIFE async marca status: error → bubble desaparece tras 1.4s por el bug #1.
    • Fix: todos los defaults `gpt-image-1.5` → `gpt-image-1`.

    Bug #3 — payload no adaptativo

    • Antes: payload estático `{ model, prompt, n, size, quality: "auto" }`.
    • `dall-e-2` no soporta `quality` → 400 "Unrecognized parameter: quality".
    • `dall-e-3` soporta `quality: standard | hd` (no acepta "auto").
    • `gpt-image-1` soporta `quality: low | medium | high | auto`.
    • Fix: payload construido adaptativo:
    • - dall-e-2: sin quality
    • - dall-e-3: quality: standard
    • - gpt-image-1 (y otros): quality: auto
    • - extra_params del admin pueden sobrescribir cualquier campo (incluido quality)

    Bug #4 — typos en el admin

    • El admin en /admin/image-generation tenía un input de texto libre para el modelo. Usuario podía escribir cualquier nombre (ej: gpt-image-2) y la API rechazaba sin que se viera el error claramente.
    • Fix: cuando engine=openai, el campo "Modelo" pasa a Select con los 3 únicos modelos válidos: gpt-image-1, dall-e-3, dall-e-2.
    • Si el admin elige otro engine (A1111, ComfyUI, Replicate, custom), el campo sigue siendo input libre porque cada engine tiene sus propios modelos.
    • Si la config guardada tiene un modelo desconocido (legacy), el dropdown cae a `gpt-image-1` automáticamente al renderizar.
    • Hint en el campo: "Sólo modelos OpenAI Images reales. Otros nombres dan error 400."

    Mejora — modelo real en el badge del bubble

    • El endpoint /api/image/generate devuelve `{ success, image_url, model, prompt }`.
    • Antes el bubble ignoraba `data.model` y usaba `modelLabel="gpt-image-1.5"` hardcoded.
    • Ahora la IIFE async guarda `data.model` en `imageGenerationRun.model` (campo nuevo añadido al type).
    • El componente `<ImageGenerationCard modelLabel={run.model || "gpt-image-1"}>` muestra el modelo correcto.
    • Si el admin tiene configurado dall-e-3, el badge dice "DALL-E-3"; si gpt-image-1, dice "GPT-IMAGE-1"; etc.

    Calidad técnica

    • Typecheck verde tras los 4 cambios.
    • Sin breaking changes — los toggles del composer y la UI funcionan igual.
    • Comentarios "7.23.0" en cada punto de cambio.
    • Si la config guardada tiene un modelo legacy invalido (gpt-image-1.5, gpt-image-2), el dropdown lo trata como inexistente y muestra gpt-image-1 (sin perder la config — el admin sólo tiene que pulsar Guardar para fijar el válido).
  18. v7.22.0

    Fix CRÍTICO: FUNCTION_INVOCATION_TIMEOUT en generación de imagen

    La versión 7.22.0 arregla el FUNCTION_INVOCATION_TIMEOUT que aparecía al pedir imágenes en Vercel. La causa: el endpoint /api/chat hacía la llamada a OpenAI Images API inline (15-30s para generar), excediendo el límite de 10s de Vercel Hobby. La función se cortaba a la mitad y la respuesta del chat también se perdía. Solución: extraída la generación a un endpoint dedicado /api/image/generate que corre en su propia función serverless con timeout independiente. El chat responde inmediato con texto ("estoy generando la imagen…") y el cliente lanza /api/image/generate en paralelo, mostrando el progreso en el bubble cuando llega el resultado.

    Nuevo

    • ✂️ Generación de imagen REMOVIDA del endpoint /api/chat. El chat ya no se bloquea esperando la imagen — devuelve respuesta inmediata.
    • 🆕 Nuevo endpoint `/api/image/generate` con su propia función serverless de 60s declarados (10s reales en Hobby) — independiente del chat. Si timeout, sólo falla la imagen, NO el chat.
    • 🔁 El cliente lanza `/api/image/generate` en paralelo cuando el toggle imagen está activo. Antes ambos eran secuenciales en la misma función → bloqueo total.
    • 📎 El nuevo endpoint sigue leyendo `app_settings.image_generation` (modelo, tamaño, URL, key, params) y hace el "asistente de prompt" si está activado.
    • 🔗 Tras generar, attachea la imagen al último mensaje assistant del chat actualizando `attachments_json` y `meta_json` — así sobrevive a refreshes y se ve en historial.

    Arreglado

    • 🚨 Bug: pedir una imagen mostraba "An error occurred with your deployment FUNCTION_INVOCATION_TIMEOUT" — Vercel Hobby cortaba el chat a 10s mientras la API de imagen aún tardaba 15-30s.
    • ⏱️ AbortSignal.timeout(55000) en la llamada a OpenAI — si la API tarda más de 55s, abortamos para devolver error legible antes del kill duro de Vercel.

    Causa raíz del FUNCTION_INVOCATION_TIMEOUT

    • Vercel Hobby tiene límite de 10s en funciones serverless. Pro tiene 60s.
    • /api/chat declaraba `maxDuration = 60` (irrelevante en Hobby).
    • Dentro del chat, si `image_generation: true`, hacía `await fetch("https://api.openai.com/v1/images/generations")` que tarda 15-30s con `gpt-image-1`/`dall-e-3`.
    • A los 10s exactos, Vercel mata la función → cliente recibe FUNCTION_INVOCATION_TIMEOUT → ni respuesta de chat ni imagen.
    • Esto era SISTEMÁTICO: cualquier petición con imagen activa fallaba en Hobby.

    Solución: endpoint separado

    • Nuevo `src/app/api/image/generate/route.ts` (POST) — su propia función serverless.
    • Recibe `{ prompt, conversation_id }` y devuelve `{ success, image_url, model, prompt }`.
    • Lee la config del admin desde `app_settings.image_generation` (igual que antes el chat — código duplicado intencional para que el endpoint sea autónomo).
    • Hace el "asistente de prompt" si `generate_prompt_assist: true` (rewrite con gpt-4o-mini, timeout 4s).
    • Llama a `${api_url}/images/generations` con AbortSignal.timeout(55000).
    • Sube el resultado a Supabase Storage `generated-images/{user_id}/{ts}_generated.png` y devuelve la public URL.
    • Si hay `conversation_id`, busca el último mensaje `assistant` de la conversación y lo actualiza con el attachment de la imagen — así la imagen queda persistida en el historial.
    • Errores devueltos con status apropiados (403 si admin desactivó, 502 si API falla, 504 si timeout).

    Cambios en /api/chat/route.ts

    • Eliminado el bloque inline de generación de imagen (~140 líneas que llamaban a /v1/images/generations + uploadToStorage).
    • Cuando `image_generation` está activo, sólo añade un hint al system prompt: "El usuario ha activado generación de imagen. La imagen se está generando en paralelo en /api/image/generate y se mostrará automáticamente en el chat cuando esté lista. Tu respuesta debe ser breve (1-2 frases) confirmando que estás generando la imagen."
    • El chat responde inmediato → no hay timeout porque la fetch a OpenAI Images ya no se hace aquí.
    • El SSE `image_generation_event` que antes mandaba `status: ready/error` ahora manda sólo `status: running` para compatibilidad — el state real lo gestiona el cliente desde la respuesta del nuevo endpoint.

    Cambios en ChatInput.tsx

    • Justo después de `setImageGenerationRun({ status: "running" })`, lanza un `fetch("/api/image/generate")` en una IIFE async (sin await — corre en paralelo).
    • Cuando llega la respuesta:
    • - Éxito: `setImageGenerationRun({ status: "ready", imageUrl })`
    • - Error: `setImageGenerationRun({ status: "error", error: msg })`
    • - Excepción de red: `setImageGenerationRun({ status: "error", error: errMsg })`
    • El bubble del chat ya tenía la lógica para mostrar progress + imagen final desde imageGenerationRun → no se tocó nada del UI.
    • El chat principal (texto del modelo) sigue su flujo normal vía /api/chat. Imagen y texto llegan independientemente.

    Comparativa antes/después

    • **Antes (v7.21.0)**: 1 sola petición a /api/chat, debe completar imagen + texto en 10s. Resultado en Hobby: FAIL siempre.
    • **Ahora (v7.22.0)**: 2 peticiones independientes en paralelo. /api/chat termina rápido (sólo texto, ~3-5s). /api/image/generate corre aparte (≤10s en Hobby, ≤60s en Pro). Si la imagen falla, el chat sigue funcionando.
    • Coste: idéntico — la API de imagen se llama una vez, igual que antes.
    • Latencia percibida: MEJOR. El usuario ve la respuesta del chat en 3-5s y la imagen llega cuando esté lista — no espera 30s para ver nada.

    Calidad técnica

    • Typecheck verde tras los cambios.
    • Sin breaking changes — el toggle de imagen del composer funciona igual.
    • El estado `imageGenerationRun` (Zustand store ya existente) se sigue usando como única fuente de verdad para la UI.
    • Si en el futuro se migra a Vercel Pro, el código sigue funcionando — sólo aumenta el timeout efectivo.
    • El endpoint `/api/image/generate` también puede llamarse desde fuera del chat (ej: regenerar una imagen, dar otro estilo, etc.) — útil para futuras features.
  19. v7.21.0

    Fix: la config del panel admin de Imagen ahora SÍ se aplica al chat

    La versión 7.21.0 cierra el loop del panel de Imagen (v7.19.0): el admin podía guardar el modelo (gpt-image-2, etc.), tamaño, motor, URL/key, parámetros — pero el chat seguía usando "gpt-image-1.5" hardcoded. Ahora el endpoint de chat lee `app_settings.image_generation` antes de cada generación y usa los valores del admin. El badge del modelo en el bubble de imagen también refleja el modelo real (no el hardcoded). Bonus: implementado el "Asistente de prompt" (cuando está activo en el panel, reescribe el prompt del usuario con un LLM ligero antes de enviarlo al modelo de imagen para mejor calidad).

    Nuevo

    • 📐 Tamaño de la imagen también lee del admin (`generation.size`) — antes siempre 1024×1024 fijo.
    • 🔌 URL base de la API y clave API se leen del admin: permite usar OpenAI con cuenta personal, AUTOMATIC1111 local, ComfyUI, Replicate, o cualquier endpoint compatible con la API de OpenAI Images.
    • ⚙️ Parámetros JSON adicionales del admin (ej: `{"quality": "hd", "style": "vivid"}`) se mezclan con la petición.
    • 🏷️ El badge del modelo en el bubble del chat ahora muestra el modelo REAL (era "GPT-IMAGE-1.5" hardcoded).
    • 🛑 Si el admin desactiva globalmente la generación de imágenes (toggle "Generación de imágenes"), el chat NO genera aunque el usuario active el toggle del composer.

    Mejorado

    • 🎨 El modelo configurado en `/admin/image-generation` (ej: `gpt-image-2`) ahora se usa de verdad en cada generación. Antes era ignorado y el chat usaba `gpt-image-1.5` hardcoded.
    • Asistente de prompt implementado: si está activado en el panel, reescribe el prompt con `gpt-4o-mini` añadiendo detalles visuales (estilo, composición, iluminación) antes de generar. Timeout 5s — si falla, usa el prompt original.

    Lectura dinámica de la config (chat/route.ts)

    • Antes de generar imagen, el endpoint hace `serviceClient.from("app_settings").select("value").eq("key", "image_generation").maybeSingle()`.
    • Si encuentra config: extrae `enabled`, `generate_prompt_assist`, `generation.{model, size, api_url, api_key, extra_params}`.
    • Si no encuentra config (admin nunca guardó): fallback a defaults `gpt-image-1.5` + `1024x1024` + `https://api.openai.com/v1` + `process.env.OPENAI_API_KEY` + `{}`.
    • `extra_params` se parsea con `JSON.parse` con try/catch — si el admin guardó JSON inválido, se ignora silenciosamente.
    • Ningún campo es required — si falta `model` se usa el default. Cualquier valor "" (string vacío) se ignora.

    URL endpoint configurable

    • La petición ahora va a `${imageGenApiUrl}/images/generations` en lugar de hardcoded `https://api.openai.com/v1/images/generations`.
    • El `api_url` del admin se normaliza con `.replace(/\/$/, "")` para quitar slash final si el admin lo añadió.
    • Permite usar:
    • - `https://api.openai.com/v1` (OpenAI default)
    • - `http://localhost:7860` (AUTOMATIC1111 local)
    • - `https://api.replicate.com/v1` (Replicate)
    • - Cualquier endpoint compatible con la API OpenAI Images.

    Asistente de prompt (rewriter)

    • Si `app_settings.image_generation.generate_prompt_assist === true`, antes de llamar al modelo de imagen se hace una llamada extra a `gpt-4o-mini` para reescribir el prompt.
    • System prompt del rewriter: "Eres un experto en prompts para generación de imagen. Reescribe el prompt del usuario añadiendo detalles visuales (estilo, composición, iluminación, paleta) que mejoren el resultado. Devuelve SÓLO el prompt mejorado en una línea, sin comillas ni explicación. Mantén el idioma original."
    • Temperature 0.5, max_tokens 200, timeout 5s con `AbortSignal.timeout(5000)`.
    • Si el rewrite falla o devuelve string vacío, se usa el prompt original (no-fatal).
    • Coste: ~$0.0001 por imagen — insignificante comparado con el coste de la imagen ($0.04+).
    • Útil para cuando el usuario escribe prompts cortos tipo "perro" → el rewriter lo expande a "Fotografía de un golden retriever en un parque al atardecer, luz dorada, profundidad de campo, 4K".

    Badge del modelo en el chat

    • El stream event `image_generation_event` incluye el campo `model` que se muestra en el bubble como "GPT-IMAGE-2", "DALL-E-3", etc.
    • Antes era hardcoded `model: "gpt-image-1.5"` → siempre se veía ese badge aunque el admin hubiera configurado otro.
    • Ahora `model: imageGenModel` que viene del config del admin.

    Toggle global del admin

    • Si `app_settings.image_generation.enabled === false`, la generación de imagen se SKIPPEA aunque el usuario active el toggle del composer.
    • Caso de uso: el admin quiere desactivar temporalmente la imagen sin tocar permisos por rol.
    • El usuario verá la respuesta del modelo de chat normal sin imagen — sin error explícito (decisión: el admin ya sabe que la apagó).

    Calidad técnica

    • Typecheck verde.
    • Sin breaking changes — si no hay config en `app_settings.image_generation`, todo funciona como antes.
    • Try/catch envolviendo la lectura de config (no fatal si Supabase falla).
    • Try/catch envolviendo el rewriter (no fatal si OpenAI tarda o falla).
    • Logs claros: `[ImageGen] Generating with model=X, size=Y, url=Z` y `[ImageGen] Prompt rewritten via assist: ...`
  20. v7.20.0

    Fix: image_generation visible en menú + long-press en sidebar mobile

    La versión 7.20.0 arregla dos problemas reportados: (1) la herramienta de generación de imágenes desapareció del menú de herramientas porque se gateaba por `selectedModelCapabilities.image_generation`, lo cual era incorrecto — la generación de imagen llama a un endpoint aparte (gpt-image / DALL-E) y no depende del modelo de chat. Lo mismo aplica al code_interpreter (Pyodide en el navegador). Ambos toggles vuelven a aparecer con cualquier modelo. (2) En la sidebar de mobile no había forma de abrir el menú de opciones (3 puntos) de cada conversación porque el botón sólo aparecía con hover (que no existe en táctil). Ahora long-press de 500ms sobre la fila abre el menú en la posición del dedo, con vibración háptica suave; el botón de 3 puntos también es visible siempre en mobile como alternativa.

    Nuevo

    • 📱 Long-press en sidebar mobile: 500ms de pulsación sostenida sobre una conversación abre el menú de opciones (renombrar, fijar, exportar, mover de carpeta, archivar, eliminar) en la posición del dedo.
    • 👆 El botón de 3 puntos ahora es visible siempre en mobile (no requiere hover); en desktop se mantiene el comportamiento de aparecer al hover sobre la fila.
    • 📳 Vibración háptica de 15ms cuando se dispara el long-press (en dispositivos que soportan navigator.vibrate).
    • 🛡️ Long-press cancelable: si mueves el dedo más de 12px o levantas antes de los 500ms, no se dispara — así no interfiere con el scroll de la lista.

    Mejorado

    • 🎨 image_generation y code_interpreter ya no se gatean por capabilities del modelo de chat. Estos tools llaman a APIs propias (gpt-image / Pyodide) — la herramienta debe poder activarse con cualquier modelo si la cuenta tiene credenciales y rol adecuado.

    Fix #1 — image_generation y code_interpreter visibles

    • Antes: `effectiveToolPermissions.imageGeneration = toolPermissions.imageGeneration && collaborationAllowedTools.imageGeneration && (selectedModelCapabilities?.image_generation ?? true)`. La última condición desactivaba el tool si el modelo de chat tenía `capabilities.image_generation: false` (que es el default de la mayoría de modelos GIA-5.x, gpt-4o, etc.).
    • Razón del bug: la generación de imagen NO la hace el modelo de chat — el endpoint /api/image llama a gpt-image-1 o DALL-E 3 directamente. El modelo de chat sólo recibe la URL resultante.
    • Lo mismo con code_interpreter: corre en Pyodide en el navegador desde v7.10.0, no depende del modelo de chat.
    • Ahora: `imageGeneration: toolPermissions.imageGeneration && collaborationAllowedTools.imageGeneration` (sin gate de modelo).
    • Igual para `codeInterpreter`. El resto de herramientas (web_search, deep_research, network_drive_rag, document_generation, spreadsheet_analysis) SÍ siguen gateando por capabilities porque sí dependen del modelo de chat (tools API, vision para attachments, etc.).

    Fix #2 — Long-press en sidebar mobile

    • Nuevo `onTouchStart` en cada `<li>` de ChatRow: arranca un timer de 500ms y guarda la posición inicial del dedo.
    • `onTouchMove`: si el dedo se desplaza más de 12px (scroll detectado), cancela el timer.
    • `onTouchEnd` / `onTouchCancel`: cancela el timer si el usuario suelta antes de los 500ms.
    • Cuando el timer dispara: vibración 15ms (`navigator.vibrate(15)`), abre el menú con `setMenuOpen(true)` y posiciona en `(touchX, touchY + 12)` con clamp para que no se salga de pantalla.
    • Flag `longPressFiredRef.current = true` evita que el siguiente click (synthetic del touchend) abra la conversación tras el long-press.
    • Cleanup useEffect: cancela timers al desmontar para evitar fugas si el usuario navega rápido.
    • CSS: `WebkitUserSelect: none`, `userSelect: none`, `WebkitTouchCallout: none` en el `<li>` para evitar el menú de iOS de selección de texto durante el long-press.
    • Botón MoreHorizontal: `opacity-100 md:opacity-0 md:group-hover/row:opacity-100` — siempre visible en mobile, sólo en hover en desktop. Así el usuario también puede tocar el botón directamente sin esperar al long-press.

    Calidad técnica

    • Typecheck verde.
    • Sin breaking changes.
    • Comentarios "7.20.0" en cada punto de cambio.
    • El long-press es modular: usa refs y useCallback para no recrear handlers en cada render.
    • LONG_PRESS_MS = 500ms (estándar Material) y LONG_PRESS_MOVE_TOLERANCE = 12px.
  21. v7.19.0

    Cierre del docx Errores GIA — los 6 fixes pendientes resueltos

    La versión 7.19.0 cierra el ciclo del documento Errores GIA.docx aplicando los 6 fixes que quedaron pendientes en 7.18.0: (E2+E3) Whisper actualizado a gpt-4o-mini-transcribe con prompt de vocabulario para no "limpiar" dominios raros como efigarola.es; (E4) modelo virtual GIA-Voz añadido a model_configs vía migración 076 y voice modal lee system_prompt configurable desde admin; (E6) dedup defensivo añadido por (role + content + ventana de 3s) para casos edge entre Realtime broadcast e INSERT del cliente; (E10) nueva tab "Imagen" en panel admin estilo OpenWebUI con modelo, tamaño, motor, URL/key, parámetros JSON tanto para crear como para editar imágenes; (E11) cada tab del admin tiene su propia URL ahora (/admin/users, /admin/models, /admin/image-generation, etc.) con back/forward del navegador funcionando.

    Nuevo

    • 🎤 E2+E3 — Whisper: cambio de `whisper-1` a `gpt-4o-mini-transcribe` (mejor precisión). Prompt de vocabulario "GIA, Ferran, Gesem, fgarola, efigarola.es..." para que NO transforme dominios raros en typos comunes. Fallback automático a whisper-1 si el nuevo no está disponible.
    • 🐰 E4 — Modelo virtual "GIA-Voz" creado en `model_configs` (migración 076). Aparece en el panel admin Modelos y se puede editar (system_prompt, max_tokens, etc.). El voice modal lee `system_prompt` del admin si existe; si está vacío usa el default.
    • 🛡️ E6 — Nuevo dedup `dedupeMessagesByContentWindow`: detecta y elimina mensajes con (role + content idéntico + created_at en ventana de 3s) aunque tengan ids distintos. Cubre el caso edge de Realtime broadcast llegando con id distinto al INSERT del cliente.
    • 🖼️ E10 — Nueva tab "Imagen" en panel admin estilo OpenWebUI: configuración de motor, modelo, tamaño, URL base, clave API, versión API y parámetros JSON para "Crear imagen" y "Editar imagen" por separado. Persiste en `app_settings` bajo key `image_generation`.
    • 🔗 E11 — Cada tab del admin ahora tiene su propia URL: `/admin/dashboard`, `/admin/users`, `/admin/models`, `/admin/image-generation`, etc. (21 tabs con sus rutas). Back/forward del navegador funciona. `/admin` redirige a `/admin/dashboard`.
    • 📋 Logging de duplicados: `dedupeMessagesById` y `dedupeMessagesByContentWindow` ahora loguean en consola cuando detectan y eliminan duplicados — visibilidad para diagnosticar casos edge futuros.

    E2+E3 — Whisper más preciso

    • Modelo: `whisper-1` → `gpt-4o-mini-transcribe` (preview-stable, mejor precisión).
    • Prompt de vocabulario: "Transcribe literalmente, respeta dominios y nombres propios. Vocabulario: GIA, GIA-Voz, GEIA, Ferran, Gesem, fgarola, efigarola, Supabase, Vercel, OpenAI, Pyodide, Whisper, Realtime, Tavily, efigarola.es, fgarola.es, gesem.es, gia.gesem.es."
    • `temperature: 0` para más determinismo (cada audio se transcribe igual cada vez).
    • Fallback automático: si `gpt-4o-mini-transcribe` falla con 4xx, retry con `whisper-1` + el mismo prompt.
    • Aplicado en `/api/voice/transcribe` (botón mic del composer) Y en `useRealtimeVoice.ts` para el `input_audio_transcription` del Realtime API.
    • Resultado: "efigarola.es" se transcribe literal — antes Whisper lo convertía en "fgarola.es" (más común) y la IA buscaba en internet el dominio incorrecto.

    E4 — Modelo GIA-Voz en admin

    • Migración 076 (`gia_voz_model_entry.sql`): inserta `display_name="GIA-Voz"` en `model_configs` apuntando a `gpt-4o-mini-realtime-preview`.
    • Idempotente: si el modelo ya existe (por slug o display_name), no hace nada.
    • Auto-detección del provider OpenAI activo (`is_active=true`, ordenado por `priority`).
    • Capabilities: voice, realtime, audio_input, audio_output, tools (sin vision).
    • Sin `system_prompt` por defecto — la idea del usuario era "elimina las instrucciones que tiene ahora mismo, déjalo solamente que se llama GIA-Voz". El admin puede añadir prompt personalizado desde el panel si quiere.
    • VoiceConversationModal ahora lee `system_prompt` del modelo "GIA-Voz" en `model_configs`. Si existe, lo usa como base; si no, fallback al default hardcoded.
    • Resultado: el admin puede entrar a /admin/models, ver "GIA-Voz", y editar el system_prompt (o cualquier otra opción) sin tocar el código.

    E6 — Dedup adicional por content-window

    • Nueva función `dedupeMessagesByContentWindow(messages: Message[]): Message[]` en `chat-store.ts`.
    • Para cada mensaje, comprueba si ya hay otro con: mismo role + content idéntico (trim) + `created_at` en ventana de ±3s.
    • Si encuentra duplicado, conserva el primero y elimina el segundo. Loguea en consola cuántos quitó.
    • Aplicado en `mergeServerMessagesWithOptimistic` (la función que combina mensajes del server con los optimistic locales en cada `loadMessages`).
    • Cubre el caso edge: Realtime broadcast trae un mensaje con id distinto al INSERT que hizo el cliente — antes se mostraban dos bubbles iguales.
    • No reemplaza al `dedupeMessagesById` — se aplica DESPUÉS, como segunda red.

    E10 — Tab Imagen en admin (estilo OpenWebUI)

    • Nuevo componente `src/components/admin/panels/ImageGenerationPanel.tsx`.
    • Sección "General": toggle global para activar/desactivar generación de imágenes.
    • Sección "Crear imagen": motor (OpenAI / AUTOMATIC1111 / ComfyUI / Replicate / Custom), modelo, tamaño (256-2048), toggle de asistente de prompt, URL base API, clave API, versión API, parámetros JSON.
    • Sección "Editar imagen": mismos campos para configurar el motor de edición de imágenes por separado (puedes generar con OpenAI y editar con AUTOMATIC1111).
    • Persiste en `app_settings` con key `image_generation` (añadida a la whitelist en `/api/admin/app-settings`).
    • UI con subcomponentes Toggle, Input, Select, SecretInput (con eye/eye-off), Textarea — patron consistente con el resto del admin.
    • Validación: el panel sólo monta los datos cuando `active=true` (igual que otros paneles del admin).

    E11 — URLs por tab del admin

    • Nuevo route dynamic `src/app/admin/[tab]/page.tsx` que valida el tab y pasa `initialTab` a `AdminPageClient`.
    • Whitelist de 21 tabs válidos (dashboard, users, roles, tools, models, knowledge, providers, connections, network-drives, files, banners, document-analysis, agents, themes, notification-sound, fine-tuning, invitations, feedback, calendar-occasions, welcome-messages, image-generation).
    • Tab desconocido → redirige a `/admin/dashboard`.
    • `/admin` (página antigua) ahora redirige a `/admin/dashboard`.
    • AdminPageClient añade `initialTab` prop que se usa para el state inicial del tab.
    • `setTab(next)` ahora hace `setTabState(next)` + `window.history.pushState("/admin/" + next)`. Cambio sin re-render del SSR pero con URL sincronizada.
    • Listener `popstate` para que back/forward del navegador actualice el tab state.
    • Resultado: `/admin/users` abre directo en la tab de usuarios. Compartir URL del tab funciona. Back/forward navega entre tabs como cualquier página.

    Calidad técnica

    • Typecheck verde tras los 6 fixes.
    • Sin breaking changes — APIs estables.
    • Comentarios "7.19.0" en cada punto de cambio.
    • Migración 076 idempotente: se puede aplicar varias veces sin duplicar.
    • AdminPageClient añade un `useCallback` para `setTab` y un `useEffect` para `popstate`.
  22. v7.18.0

    Lote de fixes del Errores GIA.docx — voz, Pyodide, imágenes, Welcome

    La versión 7.18.0 ataca 7 de los 13 errores reportados en el documento "Errores GIA.docx" del usuario. Cubre las áreas de voz (sonidos premium con sonido READY al estar listo, volumen 3× más alto, fix del primer mensaje que no aparecía en chat, fix del preview de voces que no reproducía), Pyodide (input() interactivo ahora funciona con window.prompt como fallback en lugar de lanzar RuntimeError), imágenes (preview con spinner sobre el thumbnail estilo ChatGPT desde el inicio del upload, sin toast separado) y Welcome (hora se actualiza cada segundo con segundos visibles). Los 6 errores restantes (admin tab Imagen, separar admin en archivos, modelo Gia-Voz en panel modelos, Whisper transcripción, duplicado de mensajes, casos edge) requieren refactor más amplio y se atacarán en próximas releases.

    Nuevo

    • 🕐 E12 — Welcome: hora se actualiza cada segundo (antes sólo cambiaba al navegar) + ahora muestra segundos también.
    • 🔔 E5 — Voz: nuevo sonido `ready` cuando GIA-Voz pasa a estado LISTO (campanita Do-Mi-Sol con sub-octava + glissando final brillante). NO suena al abrir ni al conectar — sólo al estar listo para escuchar.
    • 🎤 E1 — Primer mensaje de voz: si el evento `audio_transcript.delta/done` no llega antes de `response.done` (común en el primer turno), se usa el fallback "[Respuesta de voz sin transcripción]" para que el bubble se cree igualmente. Antes la respuesta se perdía silenciosamente.

    Mejorado

    • 🔊 E5 — Voz: volumen subido 3× (master 0.18 → 0.55), envelope ADSR con attack suave y release exponencial. Sonidos `mute`/`unmute`/`hangup` mejorados con capas armónicas.
    • 🖼️ E9 — Imagen pegada: el toast "Imagen pegada, subiendo…" sustituido por spinner SOBRE el thumbnail (estilo ChatGPT). Previsualización aparece inmediatamente, con loader encima mientras sube.
    • 🖼️ E8 — Preview imagen durante upload: ahora se muestra al instante (placeholder optimista con `temp_id` + `uploading: true`). Antes sólo aparecía tras finalizar la subida.

    Arreglado

    • 🐍 E13 — Pyodide: `input()` ya NO lanza RuntimeError. Usa `window.prompt()` del navegador como fallback. Si el usuario cancela, devuelve string vacío en lugar de romper el código.
    • 🎚️ E7 — Preview de voz: `gpt-4o-mini-tts` no soporta voces `marin`/`cedar` (Realtime-only) — ahora hay fallback automático a tts-1-hd con voz mapeada (marin→verse, cedar→onyx). Toast con error claro si falla.

    E12 — Welcome: hora en vivo con segundos

    • Antes: `getTimeLabel()` se calculaba SOLO al montar `WelcomeScreen`. Si abrías la pantalla a las 11:59 y mirabas a las 12:30, seguía mostrando 11:59 hasta que cambiabas de página y volvías.
    • Ahora: el `time` (y el `weekday`, por consistencia) se calcula con `countdownNow` que ya tenía un `setInterval(setCountdownNow(new Date()), 1000)` activo. Cada segundo se refresca.
    • `getTimeLabel()` ahora acepta una `Date` opcional y formatea con `hour: 2-digit`, `minute: 2-digit`, `second: 2-digit` — antes sólo hora:minuto.

    E13 — Pyodide: input() interactivo con prompt() del navegador

    • Antes (v7.10.0): cualquier código con `input()` daba `RuntimeError: input() interactivo no está disponible en el navegador`. Bloqueado a propósito.
    • Ahora: definimos `window.geiaPrompt(msg)` que llama a `window.prompt()` y devuelve la respuesta del usuario. Si cancela, devuelve string vacío.
    • En el sandbox prelude de Python redefinimos `builtins.input = _interactive_input`, que llama a `js.geiaPrompt()` desde Python.
    • Resultado: cualquier ejemplo que use `input()` (ej: `nombre = input("Tu nombre")` que generan los modelos) funciona igual que en una terminal — abre un prompt nativo del navegador, captura la respuesta y la pasa al código.

    E5 — Sonidos premium del asistente de voz

    • Nuevo sonido `ready`: cuando el data-channel pasa a `connected`, suena una campanita ascendente Do5 → Mi5 → Sol5 (acorde de Do mayor) con sub-octava Do4 para calidez + glissando final La5 → Mi6 brillante. Inspirado en Apple Intelligence / Pixel.
    • Suena UNA SOLA VEZ por sesión, en el primer paso a `connected`. Si la sesión termina (idle/error) y vuelve a empezar, vuelve a sonar.
    • NO suena al abrir el modal ni mientras está conectando — sólo cuando ya está LISTO para escuchar.
    • Volumen del master: 0.18 → 0.55 (≈3× más alto). Más audible en entornos con ruido.
    • Envelope mejorado en todos los sonidos: attack 12ms, sustain plano, release exponencial a 0.0001 (silenciamiento orgánico, sin clicks).
    • `mute` / `unmute` / `hangup`: tonos con 2-3 capas armónicas (fundamental + sub-octava + glissando), no sonido digital plano.
    • `hangup`: añadido tono de "click mecánico" (220 Hz triangle 100ms) al final para sensación táctil de cerrar.

    E7 — Preview de voz: fallback robusto

    • Síntoma: pulsar play en cualquier voz se quedaba colgado en "Cargando…" sin emitir audio. Especialmente con Marin/Cedar (no soportadas en TTS) pero también ocurría intermitentemente con Alloy.
    • Causa raíz 1: `gpt-4o-mini-tts` no acepta `marin`/`cedar` (esas voces son EXCLUSIVAS de la Realtime API). El endpoint /v1/audio/speech devolvía 400 y el cliente no mostraba error.
    • Fix #1 (mapeo de voces): en `/api/voice/tts`, si la voz solicitada no está en la whitelist TTS, fallback automático: `marin → verse`, `cedar → onyx`. El resto de voces no soportadas → `alloy`.
    • Fix #2 (fallback de modelo): si la primera petición a `gpt-4o-mini-tts` falla con 4xx (cuenta sin acceso, etc.), reintentamos automáticamente con `tts-1-hd` (más viejo, pero con compatibilidad universal).
    • Fix #3 (visibilidad): el endpoint ahora devuelve el `error.message` del backend en el JSON de respuesta. El frontend muestra un toast con ese mensaje si el preview falla.

    E9 + E8 — Imagen pegada con spinner sobre thumbnail

    • Antes (E9): pegar imagen mostraba un toast "Imagen pegada, subiendo…" en bottom-right de la pantalla, separado del input. UX confusa.
    • Antes (E8): la previsualización de la imagen sólo aparecía tras terminar la subida — durante el upload, no había feedback visual encima del input.
    • Ahora: añadimos el attachment OPTIMÍSTICAMENTE al state al inicio del upload con `temp_id` y `uploading: true`. El thumbnail aparece al instante con la imagen del `localPreview` (blob URL).
    • Encima del thumbnail, mientras `uploading: true`, se renderiza un overlay con `bg-black/45 backdrop-blur-[2px]` y un `Loader2` blanco animado en el centro — exactamente como ChatGPT.
    • Cuando termina el upload con éxito, swap del temp_id por el file_id real y `uploading: false` → el spinner desaparece y queda el thumbnail limpio.
    • Si falla, se quita el placeholder optimista y se muestra el toast de error.
    • Eliminado el `addToast` para paste — el feedback ahora es 100% visual sobre la imagen.

    E1 — Primer mensaje de voz no aparecía en el chat

    • Síntoma: después del primer turno tras abrir GIA-Voz, el mensaje del usuario aparecía en el chat pero la respuesta de la IA NO. El audio de la respuesta sí sonaba, pero no había bubble en el chat. Sólo se arreglaba en turnos posteriores.
    • Causa raíz: en `useRealtimeVoice.ts` el handler de `response.done` sólo llama a `onAIResponseDone(finalText)` si `finalText` es truthy. En el primer turno, los eventos `audio_transcript.delta` y `audio_transcript.done` a veces no llegan antes que `response.done` (race), y `aiTranscriptBufferRef.current` queda vacío. `finalText = ""` → no se llama a la callback → no se persiste el mensaje.
    • Fix: ahora SIEMPRE se llama a `onAIResponseDone`, incluso si `finalText` está vacío. En ese caso se pasa el fallback `"[Respuesta de voz sin transcripción]"` para que el bubble se cree y el usuario sepa que hubo una respuesta de audio.
    • En la mayoría de los casos `finalText` SÍ llega y este fallback no se usa. Sólo cubre el caso edge del primer turno con timing raro.

    Errores PENDIENTES (próximas sesiones)

    • E2 — Whisper transcribe mal dominios raros (`efigarola.es` → `fgarola.es`). Causa: Whisper "limpia" lo que parecen typos. Solución potencial: pasar `prompt` con vocabulario común a Whisper, o usar `whisper-large-v3` cuando esté en el endpoint de OpenAI.
    • E3 — A veces no transcribe bien. Mejorable subiendo el `silence_duration_ms` o usando un modelo de transcripción más reciente.
    • E4 — Modelo "Gia-Voz" en el panel admin tab Modelos. Requiere añadir entrada virtual al sistema de modelos.
    • E6 — Caso edge nuevo de duplicación de mensajes (sin foto). Pendiente de reproducción.
    • E10 — Tab "Imagen" en el panel admin (estilo OpenWebUI). Requiere construir una nueva tab completa con campos para modelo, tamaño, API URL/key.
    • E11 — Separar el panel admin en archivos por tab/URL. Refactor masivo del componente AdminPageClient.

    Calidad técnica

    • Typecheck verde tras los 7 fixes.
    • Sin breaking changes — APIs públicas estables.
    • Comentarios "7.18.0" en cada punto de cambio para visibilidad histórica.
    • `PendingAttachment` interface ampliada con `temp_id` y `uploading` para soportar el placeholder optimista.
    • TTS endpoint: error message del backend ahora viaja al cliente (antes se perdía).
  23. v7.17.0

    Voz: vuelta a la burbuja compacta no intrusiva (anclada al composer)

    La versión 7.17.0 vuelve al diseño de burbuja compacta del asistente de voz, anclada encima del composer del chat. Las versiones 7.15.0 (fullscreen) y 7.16.0 (modal centrado) eran demasiado intrusivas y bloqueaban la lectura del chat. Ahora la burbuja flota arriba del composer (~max-w-md, ~70px alto), no oscurece el resto de la página, y el usuario puede seguir leyendo mensajes mientras habla con la IA. Mantiene el look premium con orbe pequeño + halo glow + status pill + waveform inline + 3 botones a la derecha.

    Nuevo

    • 🫧 Burbuja compacta anclada al composer (vuelve al estilo pre-7.15). NO ocupa toda la pantalla, NO tiene backdrop oscuro, NO bloquea el chat.
    • 📐 Tamaño: max-w-md (~448px), altura ~70px. Sólo ocupa lo necesario.
    • 👈 Click fuera no cierra (estaba en 7.16, era confuso). El Hangup explícito es el único cierre.
    • 🌑 Sin backdrop ni overlay oscurecido — el usuario sigue viendo el chat completo detrás.
    • 🪐 Avatar pequeño (44px) con halo glow del estado + 2px ring del color primario.
    • 🏷️ Status pill compacta (uppercase tracking-[0.14em]) con dot animado + label corto.
    • 🕒 Timer + cost en línea con la status pill, no en líneas separadas.
    • 📊 Waveform inline al final de la línea de status (12px alto, 2px gap).
    • 🔘 3 botones compactos (36×36 cada uno) a la derecha: Interrupt (condicional, ámbar) · Mute (zinc/glass) · Hangup (rojo, primary).
    • Mantiene: animaciones cubic-bezier, status-dot ping, text-fade entre turnos, gradient radial sutil del estado, glow exterior.

    Arreglado

    • 🪄 Wrapper en ChatShell: `fixed left-0 right-0 bottom-X` con offset = composer height + 12px. Posición exacta sobre el input.

    Por qué volver a la burbuja

    • v7.15.0 (fullscreen): la experiencia ocupaba toda la pantalla con aurora gradient mesh + 10 partículas + orbe gigante. Era inmersiva pero bloqueaba completamente la lectura del chat. Demasiado intrusiva para un toggle de "escuchando con la IA".
    • v7.16.0 (modal centrado): cubría ~50% de la pantalla con un card de 600px. Aún demasiado dominante. El backdrop oscurecía el chat detrás → usuario no podía seguir el contexto.
    • v7.17.0 (vuelta a la burbuja): asistente de voz como herramienta NO INVASIVA. El chat sigue 100% visible y legible. El usuario pulsa el botón de voz, aparece la burbuja flotante, habla, y puede seguir viendo los mensajes que llegan.
    • Diseño "tipo Spotify Now Playing": pill flotante anclada al composer que NO bloquea nada.

    ChatShell — wrapper restaurado

    • Antes (v7.15-16): el modal se renderizaba directamente sin wrapper, gestionando su propio fixed inset-0.
    • Ahora: vuelta al wrapper `fixed left-0 right-0 z-50 pointer-events-none` con `bottom: var(--mobile-nav-current-height) + 10px` (mobile) o `var(--chat-composer-height) + 12px` (desktop).
    • Wrapper interno `max-w-4xl mx-auto px-3 sm:px-4 pointer-events-auto` para centrar y permitir clicks dentro.
    • Z-50 (no z-200 como en fullscreen) — encima del chat pero debajo de toasts/dialogs.

    Burbuja — diseño compacto premium

    • Container: `max-w-md mx-auto`, padding x3 y2.5, rounded-22px.
    • Background: gradient radial sutil del estado (top-left, aurora1) + linear oscuro `rgba(20,22,30,0.94)` → `rgba(15,18,28,0.92)`.
    • Backdrop-filter: blur(28px) saturate(180%) — el contenido detrás se difumina ligeramente sólo donde está la burbuja.
    • Border: `${stateColor.primary}38` — tintado por el estado.
    • Box-shadow triple: drop 14×38, glow exterior 28px del estado, inset highlight blanco.
    • Animación entrada: `content-up` con `cubic-bezier(0.16, 1, 0.3, 1)` 280ms.

    Layout interno: avatar | center | botones

    • **Avatar (izquierda, 44×44):** circle con halo blur 8px del estado + 2px ring primario + sombra fuerte. Logo con padding 6.
    • **Centro (flex-1):** dos líneas:
    • - Línea 1: status pill (uppercase tracking-[0.14em] con dot animado) + timer (tabular-nums) + cost pill (si > 0) + waveform inline al final.
    • - Línea 2: transcript truncado a 1 línea con animación text-fade entre turnos.
    • **Botones (derecha, gap-1.5):** Interrupt (condicional 36×36 ámbar) · Mute (36×36 zinc/glass) · Hangup (36×36 rojo).

    Eliminado vs versión fullscreen/centered

    • ❌ Backdrop oscuro fullscreen.
    • ❌ Aurora gradient mesh tri-color animado (era para fullscreen).
    • ❌ 10 partículas ambient flotantes.
    • ❌ Orbe central gigante (200-260px). Reemplazado por avatar 44px.
    • ❌ Triple anillo orbital con rotaciones largas. Demasiado para una burbuja.
    • ❌ Transcript en card grande max-w-2xl. Reemplazado por transcript inline truncado.
    • ❌ 3 botones grandes (48-80px) con labels uppercase. Reemplazado por 36×36 sin label.
    • ❌ Click fuera = cerrar (era confuso, fácil de cerrar accidentalmente).

    Mantenido del look premium

    • ✅ Sistema de color por estado (verde/violeta/ámbar/rojo/slate).
    • ✅ Gradient radial sutil en la burbuja con aurora1 del estado.
    • ✅ Halo glow exterior alrededor del avatar (blur 8px del color del estado).
    • ✅ 2px ring del color primario alrededor del avatar.
    • ✅ Status pill con dot animado (animation `status-dot 1.6s` cuando listening/speaking).
    • ✅ Botones con gradients reales y sombras de profundidad.
    • ✅ Animación de entrada `content-up` con cubic-bezier(0.16, 1, 0.3, 1).
    • ✅ Animación `text-fade` cuando cambia el transcript.
    • ✅ Hangup con sombra rojo intenso para jerarquía clara.

    Calidad técnica

    • Typecheck verde.
    • Sin breaking changes en la firma del componente VoiceConversationModal.
    • Refs (mobileBarsRef, desktopBarsRef) siguen funcionando — el waveform analyser sigue actualizándolos.
    • Las animaciones GPU-accelerated (transform, opacity, filter:blur).
  24. v7.16.0

    Voz: modal centrado estilo ChatGPT (no fullscreen)

    La versión 7.16.0 cambia la interfaz del asistente de voz de fullscreen a un modal centrado estilo ChatGPT: backdrop semi-transparente con blur del chat detrás, card compacto en el centro (max-w-md), orbe de 200px más limpio, transcript breve, y 3 botones flotantes abajo. Click fuera del card cierra. Mantiene todas las animaciones y la estética premium pero sin tomar la pantalla entera.

    Nuevo

    • 🎯 Modal centrado (max-w-md / max-w-lg) con `max-h: min(620px, 100vh - 32px)` — NO fullscreen.
    • 🌫️ Backdrop semi-transparente `rgba(8,11,22,0.62)` con `blur(14px)` — el chat se ve atenuado detrás.
    • 🖱️ Click fuera del card cierra el modal (estilo ChatGPT/Claude).
    • 🪐 Orbe central reducido de 260px a **200px** — más coherente con el modal compacto.
    • 🔘 Botones inferiores ajustados: Mute (48×48), Hangup (56×56), Interrupt (48×48). Más compactos sin perder jerarquía.
    • 🌟 Partículas flotantes eliminadas — el efecto era para fullscreen, en modal compacto resulta sobrecargado.
    • Mantenidos: triple anillo orbital, halo glow pulse, status pill animada, transcript card con animación text-fade.

    Mejorado

    • 🌈 Aurora removido del fondo (era para fullscreen). Ahora sólo gradient sutil del estado en la parte superior del card + borde tintado + glow exterior.

    Cambio de fullscreen → modal centrado

    • Antes (v7.15.0): `fixed inset-0 flex flex-col` con backdrop completo de aurora gradient mesh.
    • Ahora (v7.16.0): `fixed inset-0 flex items-center justify-center p-4` — sólo backdrop centrado, el card va dentro.
    • Backdrop: bg `rgba(8,11,22,0.62)` con `blur(14px) saturate(140%)`. El chat se sigue viendo difuminado detrás.
    • Card central: `max-w-md sm:max-w-lg` (~448-512px), `maxHeight: min(620px, 100vh - 32px)`.
    • Click fuera del card cierra el modal (e.target === e.currentTarget). Click dentro del card NO cierra (stopPropagation).

    Card design — compacto premium

    • BorderRadius 28px.
    • Background: gradient radial sutil (top-center) con `aurora1` del estado + linear vertical de #14161f a #0d0f17.
    • Border: `${stateColor.primary}30` — tintado con el estado actual.
    • Box-shadow triple: drop shadow 60px + state glow 50px + inset highlight.
    • Animación entrada: `content-up` con `cubic-bezier(0.16, 1, 0.3, 1)` 340ms.

    Orbe — más compacto

    • 200×200px (antes 260×260) para encajar en el modal.
    • Avatar central 120×120 (antes 130×130).
    • Triple anillo: exterior dashed (24s), medio fino (30s), inset 18px.
    • Halo glow blur 22px (antes 28px).
    • Sin anillo interior #3 (era exclusivo del fullscreen).

    Bottom action bar — botones compactos

    • Mute: 48×48 (antes 64×64) con bg `rgba(255,255,255,0.10)` cuando activo, gradient zinc cuando muted.
    • Hangup: 56×56 (antes 80×80) — sigue siendo el botón más grande para jerarquía.
    • Interrupt: 48×48 (antes 64×64), condicional cuando isSpeaking.
    • Sin labels uppercase debajo (eran para el fullscreen) — los iconos ya son claros.
    • Spacer invisible si no hay Interrupt para mantener centrado el Hangup.

    Cambios eliminados del fullscreen

    • ❌ Aurora gradient mesh tri-color (3 capas radiales animadas con aurora-shift).
    • ❌ 10 partículas ambient flotantes con particle-float.
    • ❌ Anillo orbital interior #3 (orbit-slow 14s).
    • ❌ Labels uppercase bajo cada botón (Silencio/Colgar/Parar).
    • ❌ Botón X cerrar separado del Hangup (ahora hay X compacto en header del card).
    • Estos elementos eran apropiados para una experiencia fullscreen, pero saturaban el modal compacto.

    Calidad técnica

    • Typecheck verde.
    • Sin breaking changes.
    • Mismas animaciones GPU-accelerated (transform/opacity/filter:blur).
    • Click fuera = cerrar (UX estándar de modales).
  25. v7.15.0

    Voz: modelo mini revertido (8× más barato) + interfaz fullscreen pro completamente nueva

    La versión 7.15.0 revierte el modelo Realtime al "mini" (8× más barato que el full) manteniendo todos los fixes de comportamiento de v7.14.0 (VAD ajustado, system prompt reescrito, historial limitado, Whisper auto-detect) — la combinación da respuestas correctas a coste razonable. Y rehace la interfaz del asistente de voz desde cero: pasa de un panel pequeño anclado al composer a una experiencia FULLSCREEN inmersiva tipo "llamada premium" con aurora gradient mesh dinámico, orbe central de 260px con triple anillo orbital, partículas flotantes, transcript card grande con tipografía optical, status pill con icono, y barra de acción inferior con botones premium.

    Nuevo

    • 💰 Coste de voz: modelo Realtime revertido a `gpt-4o-mini-realtime-preview` (8× más barato que el full). Pricing: $0.60/$10/$2.40/$20 per 1M tokens (vs $5/$40/$20/$80 del full).
    • 🪐 Orbe central de 260px con TRIPLE anillo orbital: exterior dashed (22s horario), medio fino (32s contrario), interior fino (14s horario). Halo glow blur 28px que pulsa con `pulse-soft 2.4s` cuando hay actividad.
    • 🌟 10 partículas flotantes ambient en posiciones aleatorias, animadas con `particle-float` 4-6s desfasadas, brillan con el color del estado.
    • 📰 Transcript en tarjeta grande (max-w-2xl) con backdrop-blur-xl, tipografía 18-20px medium, animación `text-fade` cuando cambia de turno.
    • 🏷️ Status pill premium en top-left: dot animado + icono Lucide semántico + label uppercase tracking-[0.18em] con backdrop-blur.
    • 🔘 Bottom action bar: 3 botones flotantes con gradients reales y sombras de profundidad. Hangup centrado y MÁS GRANDE (20×20) en rojo intenso para jerarquía. Mute (16×16) y Interrupt (16×16, condicional) a los lados.
    • ⌨️ Animaciones de entrada escalonadas (`mount-fade` 400ms del backdrop, `content-up` con delays 0.05/0.12/0.20s para top bar / centro / bottom).
    • ✕ Botón X cerrar en top-right (además del Hangup principal abajo).

    Mejorado

    • 🎬 NUEVA interfaz de voz: pasa de panel pequeño anclado al composer → experiencia FULLSCREEN inmersiva tipo "llamada premium".

    Arreglado

    • Mantenidos los fixes de comportamiento de v7.14.0: VAD ajustado, system prompt reescrito, historial limitado a 8 msgs, Whisper auto-detect. Combinados estos fixes hacen que el mini ya responda bien.
    • 🌈 Aurora gradient mesh dinámico: 3 capas de gradients radiales que se mueven lentamente con `aurora-shift 18s/22s` y se tintan con el color del estado (verde escuchando · violeta hablando · ámbar buscando · rojo error).

    Modelo Realtime — vuelta al mini

    • `gpt-4o-realtime-preview` → `gpt-4o-mini-realtime-preview` revertido en 4 sitios: useRealtimeVoice.ts (3 referencias) + voice/session/route.ts (default).
    • Coste por 1M tokens vuelve a: text_input $0.60, audio_input $10, text_output $2.40, audio_output $20 (vs $5/$40/$20/$80 del full).
    • Pricing actualizado en `track-usage/route.ts` (backend) y `VoiceConversationModal.tsx` (frontend, para el badge de coste en vivo).
    • CRÍTICO: se mantienen TODOS los fixes de comportamiento de v7.14.0 — VAD ajustado, system prompt reescrito, historial limitado, Whisper auto-detect. Estos fixes son la causa de que el mini ahora responda bien.
    • El cliente puede pasar `model: "gpt-4o-realtime-preview"` explícitamente si quiere el full.

    Interfaz fullscreen pro — concepto general

    • ChatShell.tsx: el modal ya no se renderiza dentro de un panel anclado al composer (`fixed left-0 right-0 bottom-X`). Ahora se monta directamente y el modal gestiona su propio layout fullscreen.
    • Modal: `fixed inset-0 z-[200]` toma toda la pantalla. Backdrop completo en lugar de "pill" pequeño.
    • Mismo flow funcional (start/stop, mute, interrupt, hangup, transcript, cost tracking) — sólo cambia la presentación visual.
    • Animación de entrada: backdrop hace fade-in con `mount-fade 0.4s` que también pasa de `blur(0)` a `blur(28px)`. Los 3 bloques (top bar, centro, bottom bar) entran escalonados con `content-up` y delays 0.05/0.12/0.20s.

    Background — aurora gradient mesh dinámico

    • Base: linear-gradient vertical de #0a0b14 (top) a #0f1424 (bottom). Aspecto "noche premium".
    • Capa 1 — radial-gradient ellipse 80%×60% en (20%, 20%) con color `aurora1` del estado (alpha 28-35%). Animación `aurora-shift 18s` que la mueve lentamente.
    • Capa 2 — radial-gradient ellipse 70%×50% en (80%, 30%) con `aurora2` (alpha 16-18%). Animación `aurora-shift-2 22s` con dirección distinta.
    • Capa 3 — radial-gradient ellipse 90%×70% bottom con `aurora1` otra vez. Crea sensación de espacio.
    • Las dos capas animadas tienen `filter: blur(60-80px)` y `opacity 0.7-0.85` cuando isConnected, 0.3-0.4 cuando no. Transición 0.6s.
    • Resultado: el modal tiene una atmósfera viva que cambia con el estado, sin ser distractor.

    Orbe central — 260×260px con triple anillo + halo

    • Anillo exterior: `border: 1.5px dashed ${stateColor.primary}55`, rota en sentido horario en 22s (`orbit-rotate`).
    • Anillo medio: `inset 22px` (más interno), `border: 1px solid alpha 44`, rota CONTRARIO en 32s (`orbit-counter`).
    • Anillo interior: `inset 44px`, `border: 0.5px solid alpha 66`, rota horario en 14s (`orbit-slow`).
    • Halo glow: `inset -30px` (envuelve el orbe), `radial-gradient` con `glow` color, `filter: blur(28px)`. Animación `pulse-soft 2.4s` cuando isSpeaking || isListening.
    • Avatar central: 130×130 circle con padding 22px (logo más pequeño centrado). Box-shadow triple — anillo blanco + anillo del estado + sombra fuerte + inset highlight.
    • Bajo el orbe: waveform con 12 barras (renderBars con 4px width, max 32px alto) o spinner Search 32px si está buscando.

    Partículas ambient — 10 dots flotantes

    • Sólo se renderizan cuando `isConnected = true` (no malgastan CPU en idle).
    • 10 dots de 2-5px en posiciones específicas (no random — definidas estáticamente para coherencia visual entre renders).
    • Cada dot tiene su propia duración (4.2-5.8s) y delay (0-2.6s) para que floten desfasados.
    • Box-shadow 4×size con el color del estado → glow real, no plano.
    • Animación `particle-float`: translate(8px, -12px) y opacity 0.4↔0.8.
    • Posicionados sobre toda la pantalla pero en zonas que no chocan con el contenido central.

    Top bar — status pill + timer + cost + close

    • Status pill: rounded-full px-3 py-1.5 con backdrop-blur-md. Dot 7×7 animado + icono Lucide (Mic/Volume2/Search/Loader2/Radio) + label uppercase tracking-[0.18em].
    • Color del pill = `${stateColor.primary}1f` (bg) + `${stateColor.primary}55` (border). Texto = primary del estado.
    • Timer y cost: píldoras separadas con bg `rgba(255,255,255,0.06)` y border `rgba(255,255,255,0.12)`. Tabular-nums. En desktop, en línea con el status; en mobile, debajo del orbe.
    • Botón X cerrar: 10×10 rounded-full backdrop-blur, hover:bg-white/10.

    Transcript card — protagonista visual

    • Max-width 2xl (672px). Mucho espacio para que el texto respire en pantallas grandes.
    • Speaker label: "TÚ" / "GIA" en uppercase 11px tracking-[0.22em], color del estado, mb-2.5.
    • Card: rounded-3xl (24px), padding x6 y5, backdrop-blur-xl, bg `rgba(255,255,255,0.04)`, border `rgba(255,255,255,0.10)`, inset highlight.
    • Texto: 18-20px font-medium leading-relaxed, text-shadow sutil, animación `text-fade 0.3s` cuando cambia el contenido (key={`text-${displayText}`}).
    • Empty state: si no hay transcript aún, muestra placeholder contextual ("Empieza a hablar..." / "Estableciendo conexión..." / "No se pudo conectar").
    • min-height: 100px para evitar saltos de layout cuando aparece/desaparece transcript.

    Bottom action bar — 3 botones premium

    • **Mute** (izquierda, 64×64): si activo gradient zinc-700/zinc-600 con shadow zinc, si no transparent glass con border blanco. Label "Activar" / "Silencio" debajo en uppercase tracking-[0.16em].
    • **Hangup** (centro, 80×80, MÁS GRANDE): gradient red-500/red-600 con shadow rojo intenso 20px×44px. Label "Colgar" en rojo claro. Botón principal de la barra.
    • **Interrupt** (derecha, 64×64, CONDICIONAL): sólo aparece cuando isSpeaking && !isSearching. Gradient amber-500/orange-600. Label "Parar" en ámbar.
    • Cuando Interrupt no se muestra, hay un spacer 64×64 invisible para que el layout no salte. Hangup queda perfectamente centrado siempre.
    • Todos los botones: hover:brightness-110, active:scale-90, gap entre icono y label de 8px.
    • Sombras de profundidad reales (12-20px blur) con offset positivo. Inset highlights para sensación de relieve.

    Calidad técnica

    • Typecheck verde tras todos los cambios.
    • Sin breaking changes — la firma del componente VoiceConversationModal sigue igual (props onClose, conversationId, userId, onEnsureConversation, systemInstructions).
    • El audio analyser y los refs de waveform (`mobileBarsRef`, `desktopBarsRef`) siguen funcionando — sólo el JSX que los usa ha cambiado.
    • Animaciones GPU-accelerated (transform, opacity, filter:blur).
    • Las animaciones largas (orbits, aurora-shift) se desactivan/atenúan cuando `!isConnected` para no consumir CPU.
    • Modal renderizado a `z-50` → `z-200` para asegurar que está encima de cualquier toast/dialog.
  26. v7.14.0

    GIA-Voz responde correctamente: modelo full + VAD ajustado + prompt directo

    La versión 7.14.0 ataca el bug crítico reportado: GIA-Voz daba respuestas desconectadas de las preguntas del usuario ("Entiendo, me avisas si necesitas algo" tras "¿Quién eres?"). Tras analizar la causa raíz se aplicaron 5 fixes: (1) modelo Realtime cambiado de "mini" a FULL — el mini era el principal culpable de respuestas pobres; (2) VAD reajustado: silence 600ms→1500ms, threshold 0.5→0.6, padding 300ms→500ms — evita cortar al usuario mid-frase; (3) system prompt reescrito sin la línea "Si no entiendes pide aclaración" que disparaba respuestas tipo "no he entendido bien"; (4) historial inyectado limitado a 8 mensajes (no 20) con etiqueta explícita "histórico, NO respondas a esto"; (5) Whisper sin language fijo, auto-detecta es/ca/en. Pricing del cost indicator actualizado al modelo full.

    Nuevo

    • 🚀 Modelo Realtime cambiado de `gpt-4o-mini-realtime-preview` a `gpt-4o-realtime-preview` (FULL). Comprensión y coherencia mucho superiores. Coste ~8× pero la calidad lo justifica.
    • ⏱️ VAD ajustado: `silence_duration_ms` 600→1500ms (1.5s para pensar mid-frase), `threshold` 0.5→0.6 (menos falsos positivos), `prefix_padding_ms` 300→500ms (no se pierde la primera sílaba).
    • 📚 Historial inyectado reducido de 20 → 8 mensajes (4 turnos), truncado de 400 → 240 chars. Con etiqueta explícita "=== Histórico — NO respondas a esto ===" para que el modelo distinga contexto de fondo vs turno actual.
    • 🌍 Whisper sin `language` fijo: auto-detecta español, catalán, inglés... Si alternas idiomas mid-conversación, transcribe cada turno en su idioma real.
    • 🎙️ Voz por defecto del Realtime API también es ahora "marin" (premium, casi humana) en lugar de "alloy".
    • 💰 Cost indicator actualizado al pricing del modelo full ($5/M text in, $40/M audio in, $20/M text out, $80/M audio out).

    Mejorado

    • 📝 System prompt reescrito: fuera la línea "Si no entiendes pide aclaración" que disparaba "no he entendido bien" en bucle. Ahora: "Responde SIEMPRE al último mensaje del usuario, nunca cambies de tema". Si no se oye → "No te he escuchado bien, ¿puedes repetir?" (literal, no improvise relleno).

    Arreglado

    • 🎯 BUG resuelto: respuestas desconectadas. La IA ya no responde "Entiendo, me avisas si necesitas algo" tras "¿Quién eres?".

    Causa raíz del bug "respuestas desconectadas"

    • Síntoma: el usuario dice "¿Quién eres?" y GIA-Voz responde "Entiendo, me avisas si necesitas algo. ¡Estoy aquí para ayudar!". Tras "¿Quién eres? ¿Cómo te llamas?" responde "Claro, cuando esté listo para hablar, estaré aquí para ayudarte. ¡Que tengas un buen día!".
    • Análisis del flow: cuando el VAD corta el audio mid-frase (silence 600ms es muy corto), el modelo recibe input incompleto + ve la línea "Si no entiendes pide aclaración" en el system prompt → genera respuesta defensiva genérica.
    • Análisis adicional: el modelo "mini" tiene comprensión limitada. Cuando se combina con history dump grande (20 mensajes) + input parcial → respuesta off-topic.
    • Tres causas combinadas → 5 fixes coordinados.

    Fix #1 — Modelo Realtime FULL en lugar de mini

    • Cambiado en 4 sitios: `useRealtimeVoice.ts` (3 referencias) + `/api/voice/session/route.ts` (default).
    • `gpt-4o-mini-realtime-preview` → `gpt-4o-realtime-preview` (default).
    • Coste ~8× mayor pero respuestas mucho más coherentes y contextualmente acertadas.
    • El cliente puede sobrescribir al mini pasando explícitamente `model: "gpt-4o-mini-realtime-preview"` si el coste es prioritario sobre calidad.
    • Voz por defecto del session también cambiada de "alloy" a "marin" (premium).

    Fix #2 — VAD reajustado

    • `silence_duration_ms`: 600 → **1500** ms. Antes una pausa natural (",", "...") cortaba el turno. Ahora 1.5s de silencio antes de cerrar — permite pensar mid-frase.
    • `threshold`: 0.5 → **0.6**. Menos falsos positivos con ruido ambiental, perros, teclado. Requiere voz más clara para considerar "speech started".
    • `prefix_padding_ms`: 300 → **500** ms. Captura medio segundo PREVIO al inicio del habla detectada. Si empezabas rápido y se perdía la primera sílaba ("Quién" salía como "uién"), ya no.

    Fix #3 — System instructions reescritas

    • Antes: "Si no entiendes la petición, pide aclaración corta en lugar de cambiar de tema." → este texto disparaba el bucle "Parece que no he entendido bien" porque el modelo lo interpretaba como invitación a usarlo siempre.
    • Ahora reglas explícitas:
    • - "Responde SIEMPRE al último mensaje del usuario, nunca cambies de tema ni respondas a turnos anteriores."
    • - "Si te preguntan quién eres, di: \"Soy GIA, tu asistente de voz.\""
    • - "Si no escuchas bien o el audio llega cortado, di literalmente \"No te he escuchado bien, ¿puedes repetir?\" — NO inventes una respuesta."
    • - "No digas frases tipo \"estoy aquí para ayudarte\" si el usuario te ha hecho una pregunta concreta — responde la pregunta."
    • Persona base reescrita: "Eres GIA, un asistente de voz inteligente. Responde de forma natural, conversacional y al punto. Voz cálida pero eficiente."

    Fix #4 — Historial reducido y mejor etiquetado

    • Antes: 20 mensajes históricos × hasta 400 chars cada uno = ~8000 chars en el system prompt. Modelo perdía foco entre contexto histórico y turno actual.
    • Ahora: 8 mensajes × hasta 240 chars = ~1920 chars máximo. Suficiente para "recordar el hilo" sin saturar.
    • Etiqueta cambiada de "Contexto del chat (mensajes anteriores...)" a "=== Histórico del chat (sólo contexto de fondo, NO respondas a esto) === ... === Fin histórico — espera el siguiente turno del usuario ===".
    • Las marcas `===` y la instrucción explícita "NO respondas a esto" ayudan al modelo a tratar el histórico como contexto de referencia, no como turnos a los que debe responder.

    Fix #5 — Whisper auto-detect language

    • Antes: `input_audio_transcription: { model: "whisper-1", language: "es" | "ca" }` — forzaba el idioma del UI de la app.
    • Ahora: `input_audio_transcription: { model: "whisper-1" }` — Whisper detecta el idioma de cada turno automáticamente.
    • Beneficio: si dices una palabra técnica en inglés ("framework", "API") en medio de una frase en español, ya no se transcribe raro. Si cambias de idioma mid-conversación (turno en es, turno en ca), cada uno transcribe correcto.
    • Whisper-1 detecta idioma con altísima precisión, no hay desventaja real al no forzarlo.

    Pricing del cost indicator

    • Actualizado en frontend (`VoiceConversationModal.tsx`) y backend (`/api/voice/track-usage/route.ts`) para reflejar el modelo full.
    • Pricing oficial OpenAI: text_input $5/1M, audio_input $40/1M, text_output $20/1M, audio_output $80/1M.
    • El badge `$X.XXX` en el modal seguirá actualizándose en vivo, ahora con el coste real (más alto que con el mini, pero proporcional a la calidad).

    Calidad técnica

    • Typecheck verde tras todos los cambios.
    • Sin breaking changes en la API — la firma de useRealtimeVoice y los endpoints mantienen el mismo contrato.
    • Comentarios "7.14.0" en cada punto de cambio para visibilidad histórica.
    • Los pricings van como constantes en código, NO como env var — coherentes entre cliente y servidor.
  27. v7.13.0

    Voces premium (gpt-4o-mini-tts + marin/cedar) · rediseño aurora-glass del asistente

    La versión 7.13.0 cambia el motor TTS por defecto a `gpt-4o-mini-tts` (calidad notablemente superior, soporta instrucciones de estilo y suena mucho más natural que tts-1) y añade las dos voces premium nuevas de OpenAI: Marin y Cedar — voces casi humanas, expresivas, con prosodia muy natural. La voz por defecto pasa de Alloy (neutra/robótica) a Marin. El modal del asistente de voz se ha rediseñado por completo con un look "aurora-glass" premium: sistema de color por estado, anillos orbitales animados sobre el avatar, halo de glow que pulsa con el audio, transcript en tarjeta vidriada, píldora de estado con icono semántico, botones con gradients y sombras de profundidad real.

    Nuevo

    • 🎙️ Motor TTS actualizado a `gpt-4o-mini-tts` por defecto (antes tts-1) — calidad mucho superior, soporta `instructions` para guiar tono/cadencia.
    • Dos voces nuevas premium: **Marin** (expresiva, casi humana) y **Cedar** (profunda y natural) — destacadas con anillo dorado en SettingsModal.
    • 📋 Lista de voces ampliada de 8 a 12 (añade Marin, Cedar, Nova, Fable) y reordenada por calidad: premium → top → resto.
    • 🌀 Avatar con doble anillo orbital animado (uno en sentido horario, otro contrario) + halo glow que pulsa con `pulse-soft 2.6s`.
    • 🏷️ Píldora de estado con dot animado + icono semántico (Mic / Volume2 / Search / Loader2 / Radio) y color de marca por estado.
    • 💬 Transcript ahora en tarjeta vidriada con `backdrop-blur 20px` y borde sutil — mucho más legible que el texto suelto.
    • 🔘 Botones con gradients reales (no solo `bg-color`), sombras de profundidad e inset highlights — sensación de relieve premium. Botón Colgar más grande (16×16) y rojo intenso para jerarquía clara.
    • ⌨️ Animación de entrada `mount-up` con cubic-bezier(0.16, 1, 0.3, 1) — el modal aparece con respiración natural.
    • 🖥️ Desktop pill: borde fino con tinte de estado, glow sutil alrededor del avatar, píldora de estado compacta integrada con el timer.

    Mejorado

    • 🎨 Voz por defecto cambiada de "alloy" a "marin" — primera impresión mucho más natural.
    • 📐 Layout móvil: 348px de altura mínima, padding refinado, jerarquía visual clara (status → avatar → transcript → controles).

    Arreglado

    • Modal de voz: rediseño completo "aurora-glass" — fondo gradient mesh dinámico que cambia con el estado (verde escuchando · violeta hablando · ámbar buscando · rojo error).

    Motor TTS — gpt-4o-mini-tts

    • `/api/voice/tts` ahora acepta el campo `model` (opcional). Default = `gpt-4o-mini-tts` (lanzado por OpenAI en 2025, calidad mucho superior).
    • gpt-4o-mini-tts soporta `instructions` (style instructions) que guían tono, cadencia y emoción. Default: "Voz natural y conversacional en español neutro. Tono cálido pero profesional. Pronunciación clara, sin prisa."
    • Si el caller pasa `model: "tts-1"` o `tts-1-hd` se usa el campo `speed` legacy para retro-compatibilidad.
    • El cliente puede sobrescribir `instructions` para personalizar el estilo (ej: "habla como narrador de audiolibro").

    Voces nuevas — Marin y Cedar

    • OpenAI lanzó dos voces premium en 2025 con prosodia mucho más natural que las anteriores. Ahora disponibles en GIA.
    • **Marin** (femenina-andrógina, expresiva): la voz por defecto recomendada. Casi indistinguible de una persona real en frases largas.
    • **Cedar** (masculina, profunda): tono cálido y autoritativo. Excelente para narración o explicaciones técnicas.
    • Ambas se compatibles con el Realtime API y con `gpt-4o-mini-tts`.
    • En SettingsModal estas voces se destacan con anillo dorado y badge "⭐ Premium" en la descripción.
    • Tier "top" (Verse, Ballad, Coral) también marcado en la lista — son las mejores tras las premium.

    Voz por defecto cambiada a Marin

    • Antes: nuevos usuarios empezaban con "alloy" (neutra, algo robótica).
    • Ahora: default = "marin". Si el usuario tiene una preferencia guardada en `profiles.settings_json.voice_voice`, se respeta.
    • Sin cambios para usuarios existentes que ya hayan configurado otra voz.

    Modal de voz — rediseño aurora-glass

    • Sistema de color por estado: cada estado (idle/connecting/listening/speaking/searching/error) tiene una paleta coherente con primary (bordes/texto), soft (fondos), glow (sombras).
    • Fondo del modal con gradient mesh tri-color: top-left = soft del estado, top-right = white wash sutil, bottom = soft del estado de nuevo, base = oscuro premium (rgba(28,29,38,0.92) → rgba(15,23,42,0.95)).
    • Border-color del panel: `${stateColor.primary}30` (alpha 30) — sutil pero perceptible.
    • Box-shadow del panel: doble sombra (drop shadow + inset highlight) + glow exterior `0 0 80px ${stateColor.glow}` que cambia con el estado.
    • Avatar: anillo exterior dashed que rota lentamente (18s en sentido horario), anillo interior fino que rota al contrario (28s), halo radial blur 8px con animación pulse-soft cuando hay actividad.
    • Logo dentro de círculo blanco con anillo doble: ring blanco semitransparente + ring del color de estado (primary 80) + sombra fuerte.
    • Píldora de estado: rounded-full con dot animado (animation `status-dot 1.6s` cuando listening/speaking) + icono semántico Lucide (Mic/Volume2/Search/Loader2/Radio) + label uppercase tracking-wide.
    • Timer + cost: ahora píldoras compactas con border `rgba(255,255,255,0.10)` y bg `rgba(255,255,255,0.06)` — coherente con el resto del panel.
    • Transcript: tarjeta vidriada propia con `backdrop-blur 20px`, border sutil `rgba(255,255,255,0.08)`, padding generoso. Min-height 64px para no saltar de tamaño.
    • Botones inferiores: gradients reales (no flat), inset highlights `inset 0 1px 0 rgba(255,255,255,0.25)`, sombras de profundidad con offset positivo. Botón Colgar 16×16px (más prominente que los otros 14×14px) en rojo intenso para jerarquía clara.
    • Labels bajo cada botón: uppercase tracking-wider, color tinted con el estado del botón (rojo Colgar, slate Silenciar, ámbar Parar).
    • Animación de entrada `mount-up`: opacity 0→1, translateY 8→0, scale 0.985→1, cubic-bezier(0.16,1,0.3,1) en 320ms. Sensación de respiración natural.

    Modal de voz — desktop pill mejorado

    • Borde con tinte del estado: `border: 1px solid ${stateColor.primary}30` — sutil pero comunica el estado activo.
    • Glow sutil alrededor del panel: `box-shadow: 0 0 32px ${stateColor.glow}` — el modal "vibra" con el estado.
    • Fondo: gradient diagonal oscuro + gradient radial del color de estado en el lado izquierdo (donde está el avatar).
    • Avatar con halo glow de fondo (radial gradient blur 6px) que se ilumina cuando hay actividad.
    • Píldora de estado compacta integrada en la línea de header (label + timer + cost) en lugar de ocupar línea propia.
    • Botones más pulidos: gradients lineares en lugar de bg-color flat, hover:brightness-110 en lugar de hover:bg-color, active:scale-90 más natural que active:scale-95.
    • Botón Colgar destacado con sombra rojo intenso `0 4px 14px rgba(239,68,68,0.45)`.

    Calidad técnica

    • Typecheck verde tras todos los cambios.
    • Sin breaking changes — la firma de useRealtimeVoice y el endpoint /api/voice/tts mantienen compatibilidad.
    • Performance: las animaciones CSS (orbit-rotate, pulse-soft, status-dot) usan transform/opacity (GPU-accelerated). El waveform sigue siendo 60fps via requestAnimationFrame con ref directo al DOM.
    • Prefiere reduced motion: las animaciones largas (orbits 18-28s) se desactivan en `isConnected=false` para no consumir CPU sin razón.
    • Lucide icons añadidos: Search, Loader2, Volume2, Radio (todos ya en bundle por otros componentes — sin coste extra).
  28. v7.12.0

    Sistema de voz: 5 fixes críticos + memoria del chat + preview de voz

    La versión 7.12.0 ataca los 5 problemas más críticos del sistema de voz identificados en la auditoría: (B1) leak de seguridad en network-search, (B2) idioma fijo a "es" ignorando catalán, (B3) created_at hardcoded +2000ms causaba orden inconsistente, (U1) STT sin VAD ni timeout permitía graburar indefinidamente, (C3) sin indicador de coste en vivo. Además: GIA-Voz ahora tiene memoria de los últimos 20 mensajes del chat (sean texto o voz) inyectados en systemInstructions; nueva preview de voz en SettingsModal con botón play/stop por cada voz disponible.

    Nuevo

    • B1 (security) — /api/voice/network-search ahora filtra por `drive_id IN (drives activos)`. Antes leía cualquier chunk de network_file_chunks sin filtrar drives, exponiendo info de drives desactivados.
    • B2 — idioma del Realtime API y Whisper ahora viene de useTranslation() (es / ca). Antes hardcoded a "es" — usuarios en catalán recibían respuestas en castellano.
    • U1 — STT con VAD basado en AnalyserNode: detecta silencio sostenido >2s tras hablar y auto-para la grabación. Si no detecta voz en 8s, cancela. Hard cap absoluto de 60s. Evita graburar indefinidamente y costes accidentales de Whisper.
    • U1 — /api/voice/transcribe ahora valida tamaño: rechaza files >25 MB con 413 y files <256 bytes con 400.
    • C3 — coste estimado en vivo en el modal de voz (USD): píldora con $X.XXX que se actualiza tras cada response.done usando los pricing constants del backend. Tooltip muestra tokens consumidos.
    • Memoria del chat: GIA-Voz ahora "recuerda" los últimos 20 mensajes de la conversación (sean texto o voz) y los inyecta en systemInstructions del session.update inicial. Permite continuar una conversación de texto por voz sin perder contexto.
    • Preview de voz en SettingsModal pestaña Voz: cada una de las 8 voces (Alloy, Ash, Ballad, Coral, Echo, Sage, Shimmer, Verse) tiene un botón play que reproduce un sample. Click otra vez para parar. Cleanup automático al cerrar settings.

    Mejorado

    • B1 — matching mejorado: AND-style sobre TODAS las keywords (no sólo la primera) + stopwords (de/la/el/que/etc.) descartadas + descenso de acentos antes de comparar.
    • B3 — created_at del AI message ahora se calcula consultando el created_at REAL del último user message persistido + 1ms (no +2000ms hardcoded). Resuelve casos donde turnos rápidos quedaban en orden incorrecto al recargar.

    Arreglado

    • Toasts mejorados en STT: errores de red, errores de transcripción, "no detecté tu voz" — feedback claro en lugar de fallo silencioso.

    Fix B1 — Security en network-search por voz

    • Antes: `/api/voice/network-search` hacía `ilike "%kw%"` sobre `network_file_chunks` sin filtrar `drive_id`. Cualquier usuario autenticado podía leer chunks de drives desactivados o de drives que se quitaron del control.
    • Ahora: primero carga drives activos (`is_active=true`) y restringe la búsqueda a `drive_id IN (...)`. Mismo modelo de permisos que el chat normal.
    • Si no hay drives activos: devuelve mensaje claro en vez de buscar en vacío.
    • Tokenización mejorada: split + filtro stopwords (de, la, el, los, las, que, cómo, dónde, para, por, etc.) + descartar palabras <3 chars + strip de acentos antes de match.
    • AND-style matching: el primer keyword usa el índice (`ilike`), los demás filtran client-side. Antes una búsqueda "michael jackson" buscaba sólo "michael" y traía cualquier chunk con esa palabra. Ahora exige que el chunk contenga AMBOS términos.
    • Si AND devuelve 0 resultados, fallback a OR (al menos coincide la primera keyword) para no quedar sin nada.

    Fix B2 — Idioma de voz

    • Antes: `language: "es"` hardcoded en VoiceConversationModal. El useRealtimeVoice pasaba `whisper-1` con `language: "es"` aunque la app estuviera en catalán.
    • Ahora: `language: appLanguage === "ca" ? "ca" : "es"` leído de `useTranslation()`. Whisper transcribe en el idioma correcto y la voz responde en consecuencia.
    • Compatible con todos los modos: STT del composer (ya leía `language` del context), Realtime (ahora también).

    Fix B3 — Orden de mensajes en voz

    • Antes: el AI message se insertaba con `created_at = userTs + 2000ms` hardcoded. En turnos cortos (<2s entre user→AI→user), el SEGUNDO user message quedaba ANTES del AI anterior por created_at, causando orden raro al recargar.
    • Ahora: query a Postgres por el último user message del conversation_id y `created_at = max(lastUser.created_at + 1ms, Date.now())`. Garantiza orden estricto sin depender de hardcoded offsets.
    • Fallback al patrón antiguo (+1ms en lugar de +2000ms) si la query falla.
    • Resolución microsegundo de Postgres timestamps hace que +1ms sea suficiente para garantizar orden estricto.

    Fix U1 — STT con VAD + timeouts

    • Nuevo: Voice Activity Detection (VAD) usando AnalyserNode + RMS de la onda en tiempo real (60fps via requestAnimationFrame).
    • Umbral RMS configurado a 0.012 (calibrado para voz humana en mics típicos).
    • Auto-stop tras 2s de silencio sostenido tras haber detectado habla.
    • Cancelación si pasan 8s sin que se detecte habla en absoluto (probablemente click accidental).
    • Hard cap absoluto: 60s — si el usuario olvida apagar el mic, no se queda grabando para siempre.
    • Toast informativo si no se detectó voz: "No detecté tu voz. Inténtalo de nuevo."
    • Toasts de error específicos para fallo de red, fallo de transcripción Whisper, etc. Antes era silencioso.
    • Server: `/api/voice/transcribe` ahora rechaza files >25 MB (413) y files <256 bytes (400). Protección contra files manipulados que inflaran coste de Whisper.
    • Cleanup robusto: AnalyserNode, MediaStreamSource y AudioContext se cierran tanto en stop manual como en stop por VAD.

    C3 — Coste estimado en vivo

    • Pricing constants del backend (`REALTIME_PRICING`) duplicados en el cliente para estimación local sin viaje extra al servidor.
    • Tras cada `response.done`, `useRealtimeVoice` llama a `onUsageReady` con `{ input_tokens, output_tokens, input_token_details, output_token_details }`. El modal calcula el coste de ese turno y lo acumula.
    • Píldora visible con `$X.XXX` (4 decimales si <$0.01, 3 decimales si más) en la cabecera del modal.
    • Tooltip muestra `${total_tokens} tokens · estimación local`.
    • Implementación tanto en el layout móvil como en el desktop.
    • No reemplaza al `/api/voice/track-usage` (que sigue persistiendo en `token_usage` para histórico). Es sólo display.

    Memoria del chat — GIA-Voz recuerda mensajes anteriores

    • Cuando el modal se abre con un `conversation_id`, carga los últimos 20 mensajes (`role IN (user, assistant)`) ordenados por `created_at DESC` y los invierte para orden cronológico.
    • Cada mensaje se trunca a 400 chars para no inflar tokens en exceso (un sistema con 60 memorias + 20 turnos cabe holgado en el contexto del Realtime).
    • Formato: `Usuario: <texto>\nGIA: <texto>\n...` añadido a `effectiveInstructions` como bloque "Contexto del chat (mensajes anteriores, sean de texto o de voz)".
    • Carga en paralelo con memorias del usuario; ambos se necesitan ANTES de iniciar la sesión Realtime.
    • Sin distinción texto/voz: cualquier mensaje del chat actual entra. Si la conversación empezó por texto y el usuario abre voz para continuar, GIA-Voz tiene contexto completo.
    • Si la conversación es nueva (sin mensajes previos), no se inyecta nada y la session.update va con instructions limpias.

    Preview de voz en SettingsModal

    • Cada una de las 8 voces tiene ahora un botón circular de play (lucide Play icon) a la derecha del card.
    • Click → llama a `/api/voice/tts` con un sample fijo: "Hola, soy GIA, tu asistente de voz. Estoy aquí para ayudarte con cualquier cosa que necesites."
    • Mientras carga: spinner Loader2 dentro del botón.
    • Mientras suena: botón cambia a stop (lucide Square) en color rose-500.
    • Click en stop → para inmediatamente y libera el blob URL.
    • Click en otro play mientras suena uno → para el actual y arranca el nuevo.
    • Cleanup al desmontar el modal o cambiar de tab → para audio y libera blob.
    • No se hace cache de los blobs entre clicks (cada preview pide fresh al backend) — es intencional, el usuario raramente hace preview repetido.

    Calidad técnica

    • Typecheck verde tras todos los cambios.
    • Sin breaking changes — el contrato de useRealtimeVoice mantiene la firma; sólo añade comportamiento.
    • VoiceConversationModal pasa a 800+ LOC pero las nuevas responsabilidades están bien aisladas (estado de coste/memoria son self-contained).
    • Logs nuevos: `[VoiceModal] failed to load chat history`, `[VoicePreview] failed`, `[STT] VAD setup failed` — todos con fallback elegante.
    • Comentarios `7.12.0 — Fix BX` en cada punto de cambio para visibilidad histórica.
  29. v7.11.0

    Fix CRÍTICO duplicado de respuesta al editar mensaje · auditoría completa del sistema de edición

    La versión 7.11.0 corrige un bug crítico donde tras editar un mensaje aparecían DOS respuestas del asistente (la antigua + la nueva regenerada) al acabar el streaming. Tras una auditoría completa del sistema de edición (cliente, store, Realtime, /api/messages/edit y /api/chat regen), se identificaron 4 superficies de bug y se aplicaron fixes en CADA UNA: (1) el handler Realtime UPDATE de mensajes "ghost-añadía" filas eliminadas localmente, (2) el fallback de updateMessage en el store APPENDeaba un nuevo registro cuando el id no existía, (3) el endpoint /api/messages/edit no protegía contra races (un INSERT por Realtime entre lectura y delete sobrevivía), (4) /api/chat regen branch no garantizaba ausencia de duplicados al final. Nueva capa de dedup defensivo a nivel de store.

    Nuevo

    • Capa de dedup defensivo a nivel de store: nueva función `dedupeMessagesById()` aplicada en `addMessage`, `updateMessage` y `mergeServerMessagesWithOptimistic`. Garantiza que el render NUNCA vea dos filas con el mismo id, incluso si por alguna razón el array las tuviera.
    • updateMessage ya no APPENDea filas con id desconocido. Si el mensaje no está en local, el update se ignora (caso típico tras removeMessagesAfter durante regen).
    • messages/edit returns deleted_count incrementado con los excedentes del safeguard cleanup. Logs detallados para debugging futuro.
    • Logs de chat/route.ts cuando detecta huérfanos: incluye regen_target, conversation_id, orphan_ids para debugging.

    Arreglado

    • BUG CRÍTICO — Respuesta duplicada al editar mensaje: tras editar y regenerar aparecían 2 respuestas del asistente (la antigua y la nueva, ambas con header GIA-5.5 y contenido casi idéntico). Identificado y corregido en 4 superficies independientes.
    • CAUSA 1 — Realtime UPDATE handler: tras `removeMessagesAfter()` el assistant local se eliminaba; cuando llegaba el UPDATE de Realtime con el contenido regenerado, `updateMessage()` lo añadía de nuevo via fallback APPEND. Después `loadMessages()` lo añadía otra vez. Resultado: 2 filas. Fix: el handler Realtime UPDATE ignora el evento si el mensaje no está en local (loadMessages posterior reflejará el estado correcto).
    • CAUSA 2 — Fallback de updateMessage en chat-store: si el id no existía, hacía `[...messages, candidate]` (APPEND) en lugar de ignorar. Antes esto era inocuo; con el flow de edit causaba duplicado. Fix: si el id no existe, simplemente devolvemos el state sin cambios (el siguiente loadMessages traerá el estado correcto desde DB).
    • CAUSA 3 — Race en /api/messages/edit: la lista de afterEdited se capturaba al inicio, pero entre la lectura y el `delete` podía entrar un INSERT por Realtime/otra request. Fix: doble verificación tras el primer delete — re-leer la conversación, identificar excedentes (otros assistants posteriores al regenTarget, otros mensajes user posteriores) y borrarlos.
    • CAUSA 4 — /api/chat regen branch sin garantía: tras actualizar el regen target, no verificaba si quedaban OTROS assistants en la conversación más allá. Fix: tras la update, query defensivo por `conversation_id` + `role=assistant` + `created_at >= regen_target.created_at` + `id != regen_target` → borra cualquier huérfano.

    Auditoría del flujo de edición — superficies analizadas

    • Cliente — `MessageBubble.tsx#handleEditSave` (líneas ~2150-2247): valida targetMessageId, llama a /api/messages/edit con fallback de 404, parsea respuesta, llama a `updateMessage` + `removeMessagesAfter` + `setAssistantVersionOverride` + `regenerateFromHere(skipUserSave=true)`, finalmente `loadMessages(silent)`.
    • Cliente — `MessageBubble.tsx#regenerateFromHere` (líneas ~2288-2495): setea streaming state, fetch /api/chat con `regenerate_message_id`, captura streaming chunks, en finally llama a `loadMessages(silent)` y limpia streaming state.
    • Cliente — `chat-store.ts#updateMessage` (líneas ~600-625): el fallback APPEND era el origen del ghost-add tras Realtime UPDATE.
    • Cliente — `ChatArea.tsx` Realtime listener (líneas ~288-360): los handlers INSERT/UPDATE/DELETE de Supabase Realtime pueden disparar mientras el cliente tiene el assistant temporalmente eliminado de local.
    • Servidor — `/api/messages/edit/route.ts`: valida ownership, actualiza el mensaje, inserta en `message_edits`, identifica `regenTarget` (primer assistant tras el editado), elimina los demás afterEdited.
    • Servidor — `/api/chat/route.ts` regen branch (líneas ~12025-12095): valida `regenerate_message_id`, inserta nueva versión en `message_versions`, actualiza `messages.content`. Si update falla → fallback con payload reducido.

    Fix #1 — Realtime UPDATE handler ignora ghosts

    • Antes: el listener `postgres_changes UPDATE` llamaba a `updateMessage(id, incoming)` sin comprobar si el mensaje estaba en local. Si el assistant había sido eliminado por `removeMessagesAfter` y luego Realtime mandaba el contenido regenerado, el fallback APPEND del store creaba una nueva fila.
    • Después: el listener verifica `useChatStore.getState().messages.some((m) => m.id === incoming.id)`. Si el mensaje no está en local, el evento se ignora — `loadMessages` posterior traerá el estado correcto desde DB.
    • Sin pérdida de funcionalidad: cuando el mensaje sí está en local, el update en sitio sigue funcionando.

    Fix #2 — chat-store#updateMessage sin APPEND

    • Antes: si el id no existía, `updateMessage` hacía `[...messages, candidate]` para "no perder" updates. Esto era una mala defensa: el caller debería usar `addMessage` para crear, no `updateMessage`.
    • Después: si el id no existe, devolvemos el state sin cambios. Cualquier creación legítima debe pasar por `addMessage` (Realtime INSERT handler sí lo usa).
    • Comentario añadido en código explicando el bug histórico para evitar regresiones.

    Fix #3 — Safeguard cleanup en /api/messages/edit

    • Antes: `idsToDelete` se calculaba sobre `afterEdited` capturado al inicio. Race entre la lectura y el `.delete()` permitía que mensajes insertados in-flight (por otro tab, por Realtime, por otra request concurrente) sobrevivieran.
    • Después: tras el primer delete, se re-lee la conversación. Si hay extras: assistants posteriores al regenTarget, o messages no-assistant posteriores al editado, se borran en una segunda pasada (`safeguardIdsToDelete`).
    • Logs en consola cuando el safeguard tiene que limpiar (visibilidad para debugging futuro).
    • deleted_count en la respuesta JSON ahora incluye también los del safeguard.

    Fix #4 — Cleanup de huérfanos en /api/chat regen branch

    • Antes: tras actualizar el regen target con el nuevo contenido, no se verificaba que no hubiera otros assistants posteriores en la conversación. Si por algún bug previo o race quedaban, se mostraban como duplicados.
    • Después: tras la update, query defensivo por `conversation_id` + `role=assistant` + `created_at >= regen_target.created_at` + `id != regen_target_id` → si hay resultados son huérfanos → se borran.
    • Logs `[Chat] Removing orphan assistant duplicates after regen:` con regen_target, conversation_id, orphan_ids para visibilidad.
    • try/catch envuelve el cleanup de huérfanos: si falla, se loguea pero no se rompe el flujo principal.

    Capa de dedup defensivo en el store

    • Nueva función `dedupeMessagesById(messages: Message[]): Message[]` que conserva la primera ocurrencia de cada id en el array.
    • Aplicada en `addMessage` tras sortMessages.
    • Aplicada en `updateMessage` tras sortMessages (caso de update en sitio).
    • Aplicada en `mergeServerMessagesWithOptimistic` (loadMessages flow) tras combinar server + retainedOptimistic.
    • Es una capa "belt-and-suspenders" — los fixes anteriores deberían ser suficientes, pero el dedup garantiza que aunque alguno fallase en el futuro, el render nunca verá filas duplicadas.

    Calidad técnica

    • Typecheck verde tras todos los cambios.
    • Sin breaking changes — la API pública de `/api/messages/edit` sigue devolviendo los mismos campos (sólo añade `deleted_count` aumentado).
    • Sin pérdida de funcionalidad — la cadena de versiones del assistant sigue intacta (regen target keep + version_index incrementado).
    • Logs de cleanup defensivo cuando se ejecuta — visibilidad sin ruido (sólo loguea cuando realmente había que limpiar).
    • Comentarios "FIX 7.11.0" en todos los puntos de cambio para visibilidad histórica.
  30. v7.10.0

    Code interpreter funcional en Vercel · Python en el navegador con Pyodide

    La versión 7.10.0 migra el code interpreter de Python a Pyodide (WebAssembly), ejecutándose 100% en el navegador del usuario. Antes, en Vercel Hobby, el endpoint /api/code-interpreter siempre fallaba con "No hay runtime de Python disponible" porque el runtime serverless de Vercel no incluye Python. Ahora cualquier bloque de código Python puede ejecutarse desde gia.gesem.es / gia.fgarola.es sin más infraestructura, gratis, con numpy/pandas/matplotlib/scipy/sympy/scikit-learn disponibles vía micropip. La primera ejecución descarga ~6MB cacheados por el navegador; las siguientes son instantáneas.

    Nuevo

    • Code interpreter ejecutándose en producción (Vercel) sin necesidad de servicio externo de Python.
    • CodeExecutionBlock.tsx ya no llama a /api/code-interpreter — usa `executePython()` cliente con onProgress callback que muestra "Cargando Pyodide… → Inicializando Python… → Ejecutando código…".
    • MessageBubble.tsx (bloque de código en respuesta del asistente) también migrado a Pyodide con dynamic import (lazy-loaded sólo si el usuario pulsa Ejecutar).
    • Detección automática de imports: si tu código tiene `import numpy as np` o `from pandas import …`, los paquetes se cargan vía `loadPackagesFromImports` antes de ejecutar.
    • Sandbox: `input()` se bloquea con un mensaje claro porque no hay stdin interactivo en navegador. El resto de Python estándar funciona normal.
    • Timeout configurable (30s por defecto) protege contra bucles infinitos.
    • Endpoint /api/code-interpreter sigue existiendo como fallback para self-hosted con Docker (el Dockerfile incluye python3) — ahora detecta `process.env.VERCEL` y devuelve un mensaje claro recomendando recargar la app para usar Pyodide.
    • Bibliotecas soportadas de fábrica: numpy, pandas, matplotlib, scipy, scikit-learn, sympy, statsmodels, networkx, pillow, regex, lxml, beautifulsoup4, pyyaml, jinja2.
    • Bibliotecas adicionales vía `micropip.install(...)` desde el propio código (ej. requests, plotly).
    • Coste para ti: 0€. Sin servicio externo, sin Pro plan, sin Modal/E2B.

    Mejorado

    • Nuevo módulo `src/lib/pyodide-client.ts`: singleton loader con caché, lazy-load del CDN jsdelivr, captura de stdout/stderr, detección automática de paquetes desde imports.
    • Coste para el usuario: ~6MB descargados la primera vez, cacheados por el navegador para siempre. Siguientes cargas son instantáneas.

    Por qué Pyodide y no otra solución

    • Vercel Hobby: el runtime serverless no incluye Python. `child_process.spawn("python")` siempre falla con ENOENT. Imposible añadir Python al runtime de Vercel.
    • Alternativa A — Vercel Pro: function timeout de 60s (vs 10s en Hobby) pero SIGUE sin tener Python. No resuelve el problema.
    • Alternativa B — servicio externo (E2B, Modal, Replicate): coste mensual + complejidad de auth/proxying. Para un proyecto Hobby es overkill.
    • Alternativa C — VPS aparte con Docker: $5-10/mes, requiere mantener un microservicio.
    • Alternativa D — Pyodide: 0€, 0 servicios nuevos, ejecuta en el cliente. Funciona en Vercel, Netlify, Cloudflare Pages, GitHub Pages, donde sea. Bibliotecas científicas pre-compiladas. Ganador.

    Nuevo módulo `src/lib/pyodide-client.ts`

    • Singleton: una sola instancia de Pyodide por tab. La primera ejecución carga ~6MB; las siguientes son instantáneas.
    • Lazy-load del script: añade `<script src="https://cdn.jsdelivr.net/pyodide/v0.27.5/full/pyodide.js">` al document.head sólo cuando se llama a `getPyodide()` por primera vez. Sin coste de bundle inicial.
    • API simple: `executePython(code, { onProgress, timeoutMs })` devuelve `{ success, output, error, execution_time_ms, runtime, pyodide_version }`.
    • Captura de stdout/stderr vía `py.setStdout({ batched })` y `py.setStderr({ batched })` — la salida se acumula y se devuelve concatenada.
    • Detección automática de paquetes: regex sobre `import X` y `from X import …`. Si X está en la whitelist (numpy, pandas, matplotlib, scipy, sklearn, sympy, statsmodels, networkx, PIL, regex, lxml, bs4, yaml, jinja2, dateutil, pytz, micropip), se carga antes de ejecutar.
    • Sandbox prelude: bloquea `input()` con `RuntimeError("input() interactivo no está disponible en el navegador. Usa variables fijas en el código.")`.
    • Helpers exportados: `isPyodideReady()`, `isPyodideLoading()`, `prewarmPyodide()` (precarga en idle para tener Pyodide listo cuando el usuario pulse Ejecutar por primera vez).

    Componentes UI actualizados

    • CodeExecutionBlock.tsx: `handleExecute` ahora usa `executePython()` directo. Eliminado el check de `conversationId` (no es necesario en cliente).
    • Estado `pyodideStatus` para mostrar la fase de carga ("Cargando Pyodide…", "Cargando paquetes: numpy, pandas…", "Ejecutando código…").
    • El botón "Ejecutar" muestra el status actual durante la ejecución en lugar de un genérico "Ejecutando…".
    • MessageBubble.tsx (CodeBlock interno): `import("@/lib/pyodide-client")` dinámico — el chunk de Pyodide sólo se descarga si el usuario pulsa Ejecutar (no impacta first paint del chat).
    • Mensajes de error claros: si el CDN no carga, muestra "No se pudo cargar Pyodide desde el CDN. Comprueba tu conexión a internet." (caso usuario sin conexión).

    Endpoint /api/code-interpreter (fallback)

    • No se elimina — sigue siendo útil para self-hosted con Docker (el Dockerfile del proyecto YA incluye `apk add python3`).
    • Detecta `process.env.VERCEL` o `process.env.VERCEL_ENV` y devuelve un mensaje específico: "El runtime serverless de Vercel no incluye Python. En el cliente, GIA ejecuta Python en el navegador vía Pyodide — actualiza la app (Ctrl+R) y reintenta."
    • En self-hosted local sin Vercel y sin Python en PATH, el mensaje recuerda al usuario que el cliente puede ejecutar Pyodide igualmente — recarga la app.

    Compatibilidad y limitaciones

    • Soportado en Chrome, Firefox, Safari, Edge modernos (todos con WebAssembly + dynamic import).
    • NO funciona en navegadores muy antiguos sin BigInt o WebAssembly. En la práctica, irrelevante en 2026.
    • NO hay acceso al filesystem del usuario (correcto: es un sandbox).
    • NO hay subprocess, sockets reales ni threading (limitaciones del entorno de navegador).
    • Las requests HTTP funcionan via `pyodide-http` (que stubbea `requests` y `urllib`) pero CORS aplica como en cualquier fetch del navegador.
    • matplotlib renderiza imágenes que se pueden mostrar como base64 en el output. Para gráficos interactivos plotly funciona pero requiere `micropip.install("plotly")`.
    • numpy + pandas + matplotlib son los paquetes científicos clave y vienen pre-compilados oficialmente — instantáneos tras el primer load.

    Rendimiento

    • First load (cold): ~5-15s descargando 6MB de WASM + bibliotecas core.
    • Subsequent loads (warm): instantáneo (módulo cacheado en memoria).
    • Browser cache: el archivo `.wasm` y los `.whl` quedan en HTTP cache, así que entre sesiones también es rápido.
    • Ejecuciones consecutivas: instantáneas (mismo intérprete, sólo se ejecuta el código nuevo).
    • Bundle inicial de Next.js sin afectar: el `import("@/lib/pyodide-client")` es dinámico → chunk separado que sólo se descarga al pulsar Ejecutar.

    Calidad técnica

    • Typecheck verde tras la migración.
    • Sin nuevas dependencias en `package.json` — Pyodide se carga vía CDN, no vía npm. Mantiene el bundle ligero.
    • Sin breaking changes en la API: el endpoint /api/code-interpreter sigue existiendo y respondiendo, sólo que con un mensaje claro cuando no hay runtime.
    • Cero coste para producción Vercel; cero coste para self-hosted; cero coste para dev local.

1036 versiones publicadas · página 33 de 35. Míralo dentro de la app.