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.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).
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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.
  7. 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`.
  8. 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.
  9. 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).
  10. 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.
  11. 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: ...`
  12. 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.
  13. 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`.
  14. 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).
  15. 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).
  16. 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).
  17. 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.
  18. 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.
  19. 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).
  20. 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.
  21. 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.
  22. 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.
  23. v7.9.0

    Fix edit revert + leak de unidad de red · tablas markdown editoriales · runtime Python robusto

    La versión 7.9.0 corrige tres bugs críticos: (1) al editar un mensaje y regenerar la respuesta, el cliente "volvía" a mostrar la respuesta antigua al acabar streaming hasta cambiar de chat — fix en el effect de message_versions; (2) la herramienta de unidad de red seguía activándose en regen aunque el cliente la enviara como false — fix en chat/route.ts para no inferir intent ni hacer fallback a attachments del histórico cuando hay regenerate_message_id; (3) el code interpreter fallaba con "No hay runtime de Python" cuando Python sí estaba instalado pero no en PATH del proceso Node — detección robusta con sweep de rutas, where/which y caché. También rediseña las tablas markdown con estética editorial profesional (zebra, hover ámbar, tipografía optical, dark mode).

    Nuevo

    • Fallback a `where python`/`where py` en Windows y `which python3`/`which python` en Unix antes de rendirse.
    • attachmentFileIdsForRag = [] en regen sin attachments explícitos: ni siquiera se consulta /files de la conversación. Antes se llevaba todos los attachments_json del histórico al RAG aunque el usuario no los hubiera mencionado.
    • effectiveDbQuery, effectiveGmailRead, effectiveGmailSend también respetan flags explícitas en regen (no se infieren del input editado).

    Mejorado

    • BUG CRÍTICO 1 — Edit message revert: tras editar un mensaje y recibir nueva respuesta, al acabar streaming la UI volvía a la respuesta anterior. Sólo cambiar de chat y volver mostraba la nueva. Fix en MessageBubble.tsx: el useEffect que cachea message_versions sólo se disparaba al montar; ahora también refetchea cuando cambia message.content / updated_at / edit_version / forcedAssistantVersion.
    • Tablas markdown rediseñadas con estética editorial: borde sutil, header zinc-50 con border-bottom 2px, zebra striping suave, hover ámbar, tipografía optical (uppercase 10.5px en headers con tracking 0.08em, 13.5px tabular-nums en cuerpo), primera columna en negrita, dark mode nativo, scroll horizontal limpio.
    • Caché del binario Python resuelto durante 5 minutos: la primera ejecución prueba candidatos, las siguientes van directas al binario que funcionó. Auto-renovación tras instalar.

    Arreglado

    • BUG CRÍTICO 2 — Network drive en edit: aunque la v7.7.0 ya enviaba network_drive_rag: false desde el cliente, el backend SEGUÍA activándolo por dos rutas: (a) inferredNetworkDriveIntent del texto del input editado, (b) fallback a recentConversationAttachmentIds del histórico de la conversación. Fix en chat/route.ts: cuando regenerate_message_id está presente, NO se infieren intents ni se hace fallback al histórico — sólo se respetan las flags y attachments explícitos del cliente.
    • BUG 3 — Runtime de Python: el code interpreter fallaba con "No hay runtime de Python disponible" en Windows incluso con Python instalado, porque el spawn no encontraba `python`/`py` en el PATH del proceso Node (típico tras instalar sin reiniciar el shell del dev server).
    • Detección de Python robusta: sweep de rutas habituales en Windows (LOCALAPPDATA\Programs\Python\Python313/312/.../38, Program Files\Python*) y macOS/Linux (/usr/local/bin, /opt/homebrew/bin, framework de macOS, pyenv).
    • Mensaje de error mejorado: lista los candidatos probados, incluye el path típico para CODE_INTERPRETER_PYTHON_BIN según SO, y recuerda que tras instalar Python hay que reiniciar el dev server.

    Fix CRÍTICO — Edit message revert

    • Síntoma: usuario envía mensaje → recibe respuesta A → edita el mensaje → streaming muestra respuesta B correctamente → al acabar streaming la UI vuelve a A → cambiar de chat y volver muestra B (la real, ya persistida en DB).
    • Causa raíz: el useEffect que carga `message_versions` desde Supabase sólo se ejecutaba al montar el componente (deps `[isAssistant, message.id]`). Tras editar, message.id no cambia (se reusa el regen_target_message_id) — el effect no refetchea. assistantVersions queda con `[{version_index: 1, content: respuestaA}]`.
    • displayContent (línea 2535) prioriza `assistantVersions[idx].content` sobre `message.content`. Como assistantVersions está cacheada con respuestaA, ése es el que se renderiza, aunque message.content de loadMessages ya sea respuestaB.
    • Fix: añadir `message.content`, `message.updated_at`, `message.edit_version` y `forcedAssistantVersion` a las dependencias del effect. Cualquier señal de regeneración refresca el cache local.
    • Fix bonus: el effect ahora usa flag de cancelación (cancelled=true en cleanup) para evitar setState tras unmount durante navegación entre chats.
    • Fix bonus: si ya hay un override `forcedAssistantVersion`, lo respeta al reposicionar el activeAssistantVersionIdx (antes saltaba siempre al último, lo que rompía la navegación entre versiones del usuario).
    • Fix bonus: comparación previMax vs nextMax para evitar reemplazar el array si la query devuelve menos versiones (race con loadMessages).

    Fix CRÍTICO — Network drive leak en regen

    • Síntoma: aunque la v7.7.0 corrigió el cliente para mandar `network_drive_rag: false` y `attachments: []` cuando ninguna herramienta los necesita, el backend seguía adjuntando archivos del contexto a la respuesta editada (chips "generame-un-docx-sobre…" bajo el mensaje).
    • Causa raíz 1 — inferencia textual: chat/route.ts línea 5429 calculaba `effectiveNetworkDriveRag = network_drive_rag || inferredNetworkDriveIntent || explicitFileReferences.length > 0`. Si el contenido editado contenía palabras como "documento", "lista", "manual", "explica" + scope, se reactivaba la herramienta a espaldas del cliente.
    • Causa raíz 2 — fallback a attachments del histórico: chat/route.ts línea 6872 calculaba `attachmentFileIdsForRag = explicitAttachmentFileIds.length > 0 ? explicitAttachmentFileIds : recentConversationAttachmentIds`. En regen el cliente manda explicitAttachmentFileIds=[] → caía al histórico → traía PDFs antiguos al contexto RAG.
    • Fix 1: cuando `regenerate_message_id` está presente, las inferencias `inferredNetworkDriveIntent`, `inferredDbQueryIntent`, `inferredGmailReadIntent`, `inferredGmailSendIntent` se ignoran. Sólo cuentan las flags explícitas del cliente.
    • Fix 2: cuando `regenerate_message_id` está presente Y el cliente NO mandó attachments explícitos, attachmentFileIdsForRag = []. No se hace fallback al histórico.
    • Resultado: editar "¿Qué puedes hacer?" devuelve respuesta limpia sin chips de PDFs viejos, sin reactivar herramientas que el usuario no había marcado.

    Fix — Code interpreter Python runtime

    • Síntoma: en Windows, pulsar "Ejecutar" en un bloque de código Python devuelve "No hay runtime de Python disponible en el servidor (python3/python/py)" aunque Python esté instalado en C:\Python313\python.exe o en LOCALAPPDATA.
    • Causa raíz: el proceso Node del dev server se inició antes de añadir Python al PATH (o tras instalar Python sin reiniciar el shell), por lo que `spawn("python")` falla con ENOENT en los 3 candidatos típicos.
    • Fix 1 — sweep de rutas habituales: nueva función `discoverExtraPythonPaths()` que busca en LOCALAPPDATA\Programs\Python\Python{313,312,311,310,39,38}\python.exe y Program Files\Python*\python.exe en Windows, y /usr/local/bin/python3, /opt/homebrew/bin/python3, /Library/Frameworks/Python.framework/... en macOS/Linux.
    • Fix 2 — fallback con `where`/`which`: si los candidatos comunes fallan, hace `where python`, `where py` (Windows) o `which python3`, `which python` (Unix) y añade los resultados a la lista de candidatos antes de rendirse.
    • Fix 3 — caché del binario resuelto: una vez encontrado un Python que funciona, se cachea durante 5 minutos. Las siguientes ejecuciones van directas al binario sin probar candidatos. Auto-renovación tras expirar.
    • Fix 4 — mensaje de error mejorado: lista los candidatos probados, incluye el path típico para CODE_INTERPRETER_PYTHON_BIN por SO (C:\Python312\python.exe / /usr/local/bin/python3) y recuerda reiniciar `npm run dev` tras instalar Python.
    • Sin breaking changes: si CODE_INTERPRETER_PYTHON_BIN está configurado, sigue siendo prioridad absoluta.

    Tablas markdown — rediseño editorial

    • Antes: tabla con borde rounded-2xl, header bg-zinc-100/90, sin zebra, sin hover, sin dark mode, fuente sm uniforme, primera columna sin distinción.
    • Ahora: borde rounded-lg con border zinc-200/80 dark:zinc-700/60 + shadow-sm.
    • Header: bg-zinc-50 dark:bg-zinc-800/60 con border-bottom 2px gruesa, tipografía uppercase 10.5px tracking 0.08em color zinc-500.
    • Body: zebra striping sutil con `[&>tr:nth-child(even)]:bg-zinc-50/60` (light) y `dark:[&>tr:nth-child(even)]:bg-zinc-800/30`.
    • Hover de fila: bg-amber-50/40 (light) / bg-amber-500/5 (dark) — destaca la fila apuntada sin saturar.
    • Bordes internos finos zinc-100 dark:zinc-800/60, last:border-b-0 (no double border al final).
    • Padding cómodo px-3.5 py-2.5, primer y último td/th con first:pl-4 / last:pr-4 para respiración.
    • Tipografía del cuerpo: 13.5px tabular-nums, `<strong>` se renderiza zinc-900 dark:zinc-50 font-semibold (datos resaltados), primera columna automáticamente font-medium text-zinc-900 dark:text-zinc-50 (parece label).
    • Soporte dark mode nativo en todos los rincones de la tabla.
    • Scroll horizontal cuando la tabla excede el contenedor (overflow-x-auto + min-width 360px sm 480px).

    Calidad técnica

    • Typecheck verde tras todas las modificaciones.
    • Sin breaking changes: el contrato de la API de chat sigue igual, sólo cambia la priorización interna de flags en regen.
    • Cache de python bin con expiración: evita filtraciones de rutas obsoletas tras desinstalar/mover Python.
    • Effect de message_versions con cancellation flag: previene setState on unmounted component al cambiar rápido de chat.
  24. v7.8.0

    Mobile model menu pro · fix subir archivos en canales · nuevas funciones de canal

    La versión 7.8.0 actualiza el menú de modelos mobile (modo automático + separación por plataforma como en PC), arregla un bug por el que subir archivos en canales fallaba siempre con error 500 (bucket inexistente), corrige el badge de corona en avatares cuadrados, y añade un set de nuevas funciones al módulo de canales: buscador interno, scroll-to-bottom con contador de mensajes nuevos, drag&drop de archivos sobre el chat, copiar texto y enlace directo a un mensaje, deep links #m-<id> con highlight, atajos de teclado (Ctrl+K, Ctrl+F, Esc) y picker de emojis y toolbar de acciones rediseñados.

    Nuevo

    • Mobile: el menú de modelos ahora muestra el toggle "Modo automático" con icono Sparkles ámbar (igual que en PC).
    • Mobile: modelos agrupados por proveedor (OpenAI, Google, Anthropic, Meta, Mistral, Ollama, OpenRouter, Groq, Otros) con header de plataforma en color distintivo y separadores.
    • Mobile: cuando "Auto" está activo el botón del header muestra "Auto · <modelo base>" con icono Sparkles, igual que en PC.
    • Whitelist de mimes en canales ampliada: PDF, Word, Excel, PowerPoint (legacy + nuevos), OpenDocument, RTF, CSV, JSON, XML, YAML, MD, HTML, ZIP/7Z/RAR/TAR/GZ, imágenes (HEIC/AVIF incluidos), audio y vídeo. Tamaño máximo subido a 25 MB (antes 10 MB).
    • Logo de admin (corona): el badge se posicionaba mal sobre avatares cuadrados (rounded-md). Ahora `admin-crown-ring` no fuerza border-radius — el avatar decide. Se añade variante `admin-crown-ring--circle` para avatares redondos del header.
    • Buscador interno del canal: nuevo botón Search en el header del canal abre una barra que filtra mensajes por contenido en tiempo real. Muestra contador de resultados.
    • Atajo Ctrl/Cmd + K abre el buscador del canal desde cualquier sitio del módulo. Ctrl/Cmd + F lo abre cuando el foco ya está dentro del canal.
    • Scroll-to-bottom button: aparece flotando arriba del input cuando el usuario está scrolleado y desaparece al llegar abajo.
    • Contador de mensajes nuevos: si llegan mensajes mientras estás scrolleado arriba, el botón de scroll muestra "X nuevo(s)" en píldora rosa.
    • Drag & drop de archivos: arrastra cualquier archivo sobre el área del canal y se sube automáticamente con preview overlay.
    • Copy message: nueva acción "Copiar texto" en la toolbar de hover del mensaje (icono Copy de lucide). Toast de confirmación.
    • Copy message link: la acción de copiar enlace ahora genera deep links `#m-<message_id>` correctamente. Pegar el link en otra pestaña hace scroll-into-view + highlight ámbar 1.4s.
    • Deep links: al abrir el canal con un hash `#m-<id>` el cliente desplaza la lista hasta ese mensaje y lo resalta brevemente.
    • Whitelist del input file = whitelist del backend (incluye .doc, .ppt, .pptx, .odt, .ods, .odp, .rtf, .md, .json, .xml, .yaml, .yml, .log, .html, .htm, .7z, .rar, .tar, .gz).

    Mejorado

    • Esc cierra el buscador o, si está cerrado, el modal de perfil y el popup de menciones.
    • Toolbar de acciones del mensaje rediseñada: rounded-md, bordes neutros zinc-200, hover bg-zinc-100, sin shadows pesadas. Soporte dark mode nativo.
    • Picker de emojis quick-react rediseñado: rounded-md, bordes neutros, hover bg suave en cada emoji.
    • Botón de respuesta a hilo (thread count) rediseñado: outline neutro en lugar de pill indigo.

    Arreglado

    • Bug crítico CANALES: subir archivos siempre devolvía 500 porque /api/channels/upload intentaba escribir en buckets `channel-files` y `files` que NO existen en el proyecto. Ahora usa el bucket real `user-files` con prefijo `channel-attachments/`.

    Mobile — Menú de modelos

    • Añadido toggle "Modo automático" con icono Sparkles ámbar al inicio del bottom-sheet de modelos. Texto explicativo "Elige modelo según complejidad" / "Usar siempre el modelo seleccionado".
    • Añadida la lógica de agrupación por proveedor (mismo `getProviderKey` que el header de PC): OpenAI, Google, Anthropic, Meta, Mistral, Ollama, OpenRouter, Groq, Otros.
    • Cada grupo tiene un header con label en color distintivo (emerald para OpenAI, blue para Google, orange para Anthropic, etc.).
    • Separadores `<hr>` entre grupos, padding refinado.
    • Cuando autoModelMode está activo, ningún modelo se marca como seleccionado (la decisión la hace el backend).
    • Botón principal del header muestra "Auto · <modelo base>" en lugar del nombre del modelo cuando autoModelMode está activo.
    • Icono Sparkles con gradient amber→orange dentro de un cuadradito redondeado en lugar del icono del modelo cuando autoModelMode está activo.

    Fix CRÍTICO — subir archivos en canales

    • Síntoma: pulsar el clip en un canal, seleccionar archivo, recibe error "No se pudo subir el archivo" sin más detalle. Logs del servidor: 500 Internal Server Error.
    • Causa raíz: /api/channels/upload intentaba subir al bucket `channel-files` y, si fallaba, al bucket `files`. Ninguno de los dos existe en este proyecto. Los buckets reales son `user-files`, `avatars` y `public-assets`.
    • Fix: ahora usa el bucket `user-files` (el mismo que /api/files/upload), aislando los archivos del módulo de canales bajo el prefijo `channel-attachments/<user_id>/...`. Sin fallback porque ya no es necesario.
    • Mejora: la whitelist de mime types se expandió usando el mismo modelo que /api/files/upload (ALLOWED_MIME_PREFIXES + ALLOWED_MIME_EXACT + ALLOWED_EXTENSIONS) — ya no rechaza .doc/.xls/.ppt legacy, .zip, .7z, .rar, HEIC, AVIF, etc.
    • application/octet-stream se acepta sólo si la extensión está en la whitelist (mismo patrón anti-abuso que en upload).
    • Tamaño máximo subido de 10 MB a 25 MB.
    • Errores devuelven status correctos (413 para tamaño, 415 para tipo, 500 para upload error) con mensajes legibles en español.
    • El campo accept del input file en ChannelView se sincronizó con la whitelist nueva.

    Logo de admin (corona) — fix de posicionamiento

    • Antes: la clase `.admin-crown-ring` forzaba `border-radius: 9999px`. Esto entraba en conflicto con avatares cuadrados nuevos (rounded-md/rounded-lg) en canales y modal de perfil — los volvía circulares aunque el contenedor fuese cuadrado.
    • Antes: el badge `.admin-crown-badge` se posicionaba a `top: -6px; right: -6px`, demasiado lejos del centro para avatares pequeños (28-32px) usados en filas de canal — quedaba sobre el ring.
    • Ahora: `.admin-crown-ring` no fuerza border-radius (deja que el elemento decida con rounded-md/rounded-full).
    • Ahora: badge a `top: -4px; right: -4px`, tamaño 13×13 (antes 14×14), borde 1.5px sólido blanco para contraste, gradient más suave (fde68a → f59e0b).
    • Nuevo: variante `.admin-crown-ring--circle` para casos en los que el avatar SÍ debe ser redondo (header de chat, modal de perfil, panel de admin).
    • Aplicada `admin-crown-ring--circle` en src/components/chat/Header.tsx (avatar redondo) y src/components/admin/panels/UsersAdminPanelContent.tsx (avatares de tabla y modal).
    • En src/components/channels/ChannelList.tsx se quitó el doble ring (`ring-1.5 ring-amber-400 admin-crown-ring`) — el shadow del crown ring ya provee el efecto.

    Canales — Buscador interno

    • Nuevo botón Search en el header del canal: toggle on/off, estilo zinc-900 cuando activo.
    • Barra de búsqueda full-width que aparece bajo el header cuando se activa, con icono Search, input de tipo text, botón X para limpiar y contador de resultados.
    • Búsqueda en tiempo real: filtra los mensajes cargados en memoria por substring case-insensitive en `content`.
    • Empty state cuando no hay resultados con mensaje claro y sugerencia de cargar histórico antiguo.
    • Atajo Ctrl/Cmd + K: abre el buscador desde cualquier punto del módulo de canales.
    • Atajo Ctrl/Cmd + F: abre el buscador cuando el foco ya está dentro de la vista del canal (data-channel-view).
    • Esc: cierra el buscador (limpia query); si ya está cerrado, cierra modal de perfil / mention popup como antes.

    Canales — Scroll & nuevos mensajes

    • Listener de scroll en el contenedor: detecta si el usuario está cerca del fondo (< 80px) o no.
    • Botón flotante "Ir al final" / "X nuevo(s)" arriba del input, con shadow suave y hover state.
    • Cuando llegan mensajes nuevos por Realtime y el usuario está scrolleado arriba (>120px), se incrementa contador `unreadNew` y se muestra el badge rojo con el número.
    • Si el mensaje viene del propio usuario (envió desde otra ventana), no cuenta para unreadNew.
    • Click en el botón hace scroll smooth al final y resetea el contador.
    • Cuando el usuario llega al fondo manualmente, también resetea unreadNew.

    Canales — Drag & drop de archivos

    • Listener `onDragOver`/`onDragLeave`/`onDrop` en el contenedor `data-channel-view`.
    • Detecta `dataTransfer.types.includes("Files")` para distinguir entre arrastrar texto vs arrastrar archivos.
    • Overlay de drop con backdrop blur, borde dashed blanco, icono Paperclip, mensaje "Suelta el archivo para subirlo" + ayuda "Máximo 25 MB · imágenes, vídeo, audio, documentos, ZIP…"
    • Al soltar, llama a `handleFileUpload(file)` con el primer archivo del DataTransfer.
    • Sin cambios en la UX: aparece el preview habitual + se sube + queda como `pendingAttachment` listo para enviar con el siguiente mensaje.

    Canales — Copy & deep links

    • Nueva acción "Copiar texto" en la toolbar de hover del mensaje (icono Copy de lucide). Usa `navigator.clipboard.writeText` con fallback a textarea oculto + execCommand para navegadores antiguos.
    • Toast de confirmación "Mensaje copiado" / "No se pudo copiar el mensaje" según resultado.
    • La acción de copiar enlace ahora genera URLs con hash `#m-<message_id>` (antes usaba query param `&msg=` que el componente no manejaba).
    • El componente añade `id="m-${msg.id}"` a cada mensaje renderizado, lo que permite a los deep links funcionar con scroll-into-view nativo.
    • useEffect específico para deep links: lee `window.location.hash`, busca el elemento por id, hace `scrollIntoView({ behavior: "smooth", block: "center" })` y aplica un highlight ámbar transitorio (1.4s) con transición suave.
    • Listener de hashchange: si el usuario navega a otro #m-<id> desde la misma página, también funciona.

    Canales — UI refinada

    • Toolbar de acciones del mensaje: rounded-md (antes rounded-2xl), border zinc-200, p-0.5 (antes p-1), shadow-md (antes shadow-lg shadow-black/10), backdrop-blur-md.
    • Cada botón de acción: rounded-md, hover bg-zinc-100, color zinc-500 base con hover a zinc-800. Soporte dark mode (hover bg-zinc-700, hover text zinc-100).
    • Botón Eliminar: hover bg-rose-50 dark:bg-rose-500/15 + hover text rose-600 dark:rose-400.
    • Picker de emojis quick-react: rounded-md, p-1, border zinc-200/70, cada emoji con padding 1×0.5 y hover bg-zinc-100 (antes hover scale-125 sin contenedor).
    • Botón "X respuestas en hilo": outline neutro (border zinc-200/70 + bg-white) en lugar de pill indigo. Más coherente con el resto del módulo.

    Calidad técnica

    • Typecheck verde tras todas las modificaciones.
    • Sin breaking changes: la API del backend no cambia (sólo se corrige el bucket usado internamente).
    • Sin pérdida de funcionalidad existente: todas las acciones previas (responder en hilo, fijar, editar, eliminar, reaccionar) siguen funcionando.
    • Lint clean (sin warnings introducidos).
    • Reuso de helpers (getProviderKey idéntico en mobile y desktop, copy clipboard con fallback robusto).
  25. v7.7.0

    Rediseño minimalista del módulo de Canales y fix crítico del editado de mensajes

    La versión 7.7.0 rehace por completo la estética del sistema de Canales (página, lista y vista de canal) para alinearlo con la línea minimalista del resto del producto: fuera gradientes violeta/indigo, blobs animados y liquid-glass; dentro tipografía más limpia, jerarquía consistente, dark mode nativo y micro-interacciones discretas. Además corrige un bug crítico en el editado/regeneración de mensajes que reactivaba la herramienta de unidad de red sin permiso del usuario y filtraba todos los archivos del contexto en la respuesta.

    Nuevo

    • regenerateFromHere ahora reenvía explícitamente todas las flags de herramientas (web_search, db_query, network_drive_rag, deep_research, document_generation, spreadsheet_analysis, code_interpreter, integraciones Google/Spotify/Microsoft, auto_route_model) leyéndolas del store; no se infieren más por defecto.
    • Los attachments del mensaje original sólo se reenvían cuando hay una herramienta que los consume activa (RAG estricto, network_drive_rag, spreadsheet_analysis, document_generation, code_interpreter). Si re-preguntas "¿Qué puedes hacer?", ya no se cuelan PDFs anteriores.
    • channels/page.tsx: fuera blobs animados (welcome-blob-1/2/3), fuera bg gradient slate→blue→indigo. Fondo unificado #f9f9f9 / #171717 (dark).
    • ChannelList: header limpio (icono cuadrado + título tipográfico), botón "+" en zinc-900 sólido (no gradient indigo→violet), search compacto con bg zinc-100/80, tabs como underline (no pill), filas de canal con icono 28px y badges sólidos (rose-500 para menciones, zinc-900 para no leídos).
    • ChannelView: header sticky con backdrop-blur sutil, icono cuadrado neutro, badges minimalistas, mensajes con hover zinc-100/70 sin shadows, avatares 32px con borde redondo discreto.
    • Modales de crear/editar canal: sin gradient violeta→purpura, header limpio con borde inferior, picker de iconos en estilo neutral (ring zinc-900 cuando seleccionado), botones primarios en zinc-900/zinc-100.
    • Input de mensaje en canal: contenedor con borde neutro, botón send sólido zinc-900/zinc-100 (antes gradient indigo→violet→purple con scale animation), attach como icono ghost.
    • Adjuntos preview (durante upload y tras upload): bg zinc-100 neutro en lugar de gradient indigo/violet.
    • Mention popup en input de canales: bordes y superficies neutras, item activo con bg zinc-100, sin colores de marca azul.
    • Empty state de canal sin mensajes: icono cuadrado en zinc-100, sin gradients indigo/violet.
    • Pin bar (mensajes fijados): amber-50 suave en lugar de gradient amber→orange. Cards de pin con bordes neutros.
    • STATUS_DOT simplificado: emerald-500 / blue-500 / purple-500 / amber-500 / zinc-300 sin shadows.
    • Soporte dark mode nativo en todos los rincones del módulo de canales (antes sólo light).
    • Tipografía coherente con el resto del producto (10/11/12.5/13/13.5/15px) y tracking-tight en títulos.

    Mejorado

    • Sistema de Canales completamente rediseñado estéticamente: minimalista, coherente con chat/admin/sidebar/changelog/welcome.
    • ThreadPanel rediseñado: header limpio, replies con avatar cuadrado neutro, botón send minimalista. Soporte completo dark mode.
    • Modal de perfil de usuario: header sin gradient blue/cyan, cards con bordes neutros, status dot simple sin animación ping.
    • Bordes y superficies usando la paleta unificada zinc-200/70 dark:zinc-800/60 en lugar de white/40 + shadows pesadas.

    Seguridad

    • Bug crítico: al editar un mensaje el backend reactivaba la herramienta de unidad de red ("network_drive_rag") sin permiso del usuario, mostrando una lista enorme de archivos del contexto bajo la respuesta. Corregido.

    Fix crítico: editado de mensajes activaba network drive sin permiso

    • Síntoma: al editar "¿Qué puedes hacer?" la respuesta llegaba acompañada de una lista enorme de archivos (Manual_Interno_GESEM.txt, generame-un-docx-sobre..., 12-25-FerranGarola.pdf, etc.) que no debían aparecer.
    • Causa: regenerateFromHere() en MessageBubble.tsx reenviaba TODOS los rawAttachments del mensaje original en cada edición sin filtrar por herramienta activa. El backend (chat/route.ts línea 5429) calcula effectiveNetworkDriveRag = network_drive_rag || inferredNetworkDriveIntent || explicitFileReferences.length > 0; al recibir attachments siempre disparaba el RAG de unidad de red aunque el usuario no lo hubiera activado.
    • Causa secundaria: regenerateFromHere tampoco enviaba network_drive_rag, db_query, deep_research, document_generation, spreadsheet_analysis, integraciones ni auto_route_model. El backend asumía valores por defecto e inferidos del input.
    • Fix: regenerateFromHere ahora lee del store (useChatStore) las flags actuales del usuario (networkDriveRag, dbQuery, deepResearch, documentGeneration, spreadsheetAnalysis, integrationGoogle, integrationSpotify, integrationMicrosoft, autoModelMode) y las envía explícitas en cada POST a /api/chat.
    • Fix de attachments: forwardedAttachments = attachmentsConsumingToolActive ? attachmentIds : []. Se filtran a [] si no hay herramienta activa que los necesite (networkDriveRag || spreadsheetAnalysis || documentGeneration || codeInterpreter || ragMode === "strict").
    • Resultado: al editar un mensaje plano la respuesta vuelve limpia, sin archivos colados, sin reactivar herramientas que el usuario tiene desactivadas.

    Canales — wrapper (channels/page.tsx)

    • Eliminados los 3 blobs animados (welcome-blob-1, welcome-blob-2, welcome-blob-3) que generaban gradientes violeta/cyan/indigo de 750px con blur de 90px.
    • Eliminado bg-gradient-to-br from-slate-50 via-blue-50/50 to-indigo-50/40 del wrapper.
    • Sustituido por fondo plano #f9f9f9 (light) / #171717 (dark) coherente con chat/admin/sidebar.
    • Sidebar de canales con border-r border-zinc-200/70 dark:border-zinc-800/60, sin liquid-glass-sidebar.
    • Header del sidebar: botón back con hover bg-zinc-100, título con font-medium tracking-tight (no más bold con emoji 📡 inline).
    • Estado vacío rediseñado: icono Hash dentro de un cuadrado redondeado neutro w-14 h-14, título con tracking-tight, descripción text-zinc-500 leading-relaxed.

    Canales — lista (ChannelList.tsx)

    • Header reducido a 6×6 icono + título 13px font-semibold. Botón "+" 7×7 en bg-zinc-900 dark:bg-zinc-100 (antes 7×7 gradient indigo→violet con shadow-md).
    • Search input: pl-8 pr-7 py-1.5 con bg-zinc-100/80 dark:bg-zinc-800/60, focus:bg-white. Icono Search 12.5px alineado.
    • Tabs como underline: pestaña activa con span absoluto h-px en zinc-900, pestaña inactiva text-zinc-500 (antes pill bg-white con shadow-indigo-100).
    • Toggle "Solo menciones" como botón con border zinc-200 (antes pill rosa indigo-100).
    • Filas de canal: w-7×7 icono cuadrado bg-zinc-100, nombre 13px font-medium, "X miembros" 10.5px text-zinc-500. Hover bg-zinc-50, activa bg-zinc-100 (antes gradient indigo→violet shadow-lg).
    • Badges: menciones en bg-rose-500 sólido (antes border rose-200 + bg rose-50), no leídos en bg-zinc-900 (antes gradient red→pink).
    • Filas de usuario: avatar 7×7 redondeado md (no rounded-xl), status dot 2.5×2.5 sin animate-ping, info 12.5px font-medium.
    • Modales (crear/editar): max-w-md (antes max-w-sm), header sin gradient violeta/azul, picker de iconos con ring zinc-900 cuando seleccionado (no scale-110+shadow-indigo). Botones cancelar+guardar como toolbar bottom (no grid 2 cols).

    Canales — vista (ChannelView.tsx)

    • Header sticky: backdrop-blur-md sobre bg-white/90 (antes liquid-glass-header con shadow-sm). Padding vertical reducido a 2.5.
    • Channel icon 8×8 rounded-md (antes 9×9 rounded-2xl gradient indigo→violet con shadow).
    • Pin/mute buttons como icon-only 8×8 con hover bg-zinc-100 (antes pill border + scale).
    • Pinned bar: bg-amber-50/50 dark:bg-amber-500/5 (antes gradient amber→orange shadow-sm), cards de pin con border zinc-200 (no amber gradient).
    • Empty state: w-16×16 rounded-2xl bg-zinc-100 (antes w-20×20 rounded-3xl gradient indigo→violet con border indigo-100).
    • Filas de mensaje: hover bg-zinc-100/70 dark:bg-zinc-800/40 (antes hover bg-white/80 con shadow indigo-rgba). Padding vertical reducido a 2.
    • Avatares de mensaje: 8×8 rounded-md (antes 9×9 rounded-2xl con scale-105 hover). Modelo IA con gradient zinc-700→zinc-900 (no más violet→indigo).
    • Mensaje editado inline: textarea con border zinc-300, botones Save (zinc-900) y Cancel (zinc-200) (antes botón save gradient indigo→violet).
    • Mention popup: items con bg-zinc-100 cuando seleccionado (antes bg-blue-50 text-blue-700), iconos de bot en cuadrado neutro (no indigo-100).
    • Input container: rounded-lg con border-zinc-200 dark:border-zinc-700 + focus-within:border-zinc-400 (antes liquid-glass-input shadow-lg ring-indigo-100/50 con focus expansivo).
    • Botón Send: 8×8 rounded-md bg-zinc-900 dark:bg-zinc-100 (antes p-2.5 rounded-full gradient indigo→violet→purple con scale-110 hover y ch-send-pulse animation).
    • Adjuntos preview (uploading + ready): bg-zinc-100 con border-zinc-200 (antes bg-gradient-to-r from-indigo-50 to-violet-50).
    • Mention hint y char counter: text-[10.5px] text-zinc-400/500 (antes text-[10px] text-zinc-400/70).

    Canales — ThreadPanel y modal de perfil

    • ThreadPanel: bg-white dark:bg-[#1d1d1d], border zinc-200/70 (antes bg-white/80 con backdrop-blur).
    • Header del thread: text-13px font-semibold con icono MessageSquare zinc-500 (antes indigo-500).
    • Parent message: bg-zinc-50 dark:bg-zinc-900/40 con label uppercase tracking-wider zinc-500.
    • Avatares en replies: 6×6 rounded-md bg-zinc-200 (antes gradient blue→indigo).
    • Botón send del thread: 8×8 bg-zinc-900 (antes p-2 gradient indigo→violet con scale hover).
    • Botón "Ver respuestas anteriores": border zinc-200 en bg-white (antes pill text-indigo).
    • Modal de perfil: max-w-md rounded-xl con border zinc-200 (antes rounded-3xl con shadow blob 28px-70px).
    • Header del modal: limpio con border-bottom (antes gradient blue→indigo→cyan).
    • Avatar del usuario: 14×14 rounded-lg (antes 16×16 rounded-full con shadow-md).
    • Status dot: 3×3 simple (antes 3.5×3.5 con animate-ping para online).
    • Cards de info (rol app, rol canal, género, cumpleaños, última actividad, miembro desde, cuenta creada): rounded-md border zinc-200 bg-zinc-50 (antes rounded-xl). Soporte dark mode nativo en todas.
    • Bio card: igual a las otras tarjetas, padding y tipografía coherente.

    Calidad técnica

    • Typecheck verde tras todas las modificaciones de canales y del fix de regenerateFromHere.
    • Sin cambios en la API ni en el contrato de tipos: ChannelList y ChannelView mantienen la misma firma de props.
    • Sin pérdida de funcionalidad: gestión de miembros, mensajes, hilos, reacciones, pins, mute, DMs, edición/borrado de mensajes — todo funciona igual.
    • Reducción de ruido visual estimada: ~20 gradientes eliminados, ~15 shadows pesadas removidas, animaciones blob descartadas (mejora rendimiento en GPU baja gama).
    • Coherencia visual con welcome / chat / admin / sidebar / changelog: ahora canales no se siente "una app distinta" dentro del mismo producto.
  26. v7.6.0

    Hardening de seguridad, velocidad post-streaming y rediseño del changelog

    La versión 7.6.0 endurece la seguridad del backend (SSRF blacklist en webhooks, mime whitelist en upload, RLS tightening en token_usage, SIGKILL forzado en code interpreter), recorta 1-3 segundos del cierre de cada respuesta de la IA (webhooks en paralelo con timeout, generación de título con AbortSignal, cache 5 min de provider api_key, throttle de snapshot a 500ms/128 chars), y rediseña la página de changelog con un layout más expresivo y jerárquico. También limpia varios useEffect sin cleanup, refuerza el manejo de errores y soluciona logs que filtraban stack traces.

    Nuevo

    • Mime whitelist en /api/files/upload: lista explícita de tipos permitidos. Octet-stream solo si la extensión es conocida. Devuelve 415 con razón clara.
    • Code interpreter: SIGTERM + SIGKILL forzado a los 2s para eliminar procesos zombi cuando timeout no funciona.
    • Webhooks en paralelo con timeout 4s cada uno (Promise.allSettled): -100ms a -1s al cierre del response cuando hay 2+ webhooks.
    • Title generation con AbortSignal de 3s + fallback: deja de bloquear el cierre 5-10s si OpenAI tarda.
    • Throttle de snapshot durante stream subido a 500ms/128 chars: -50% adicional en writes Supabase durante respuestas largas.
    • Body req.json() con catch: si el JSON es inválido responde 400 inmediato en vez de colgar el endpoint.
    • Queue wait loop con timeout máximo 60s: si la cola queda atascada, el usuario no espera infinitamente.
    • Promise.all → Promise.allSettled en /api/admin/feedback: una query fallida ya no tumba la respuesta entera.
    • JSON.parse del plan de agentes con fallback: si el modelo devuelve JSON inválido, no crashea (steps vacíos).
    • useEffect cleanup en /auth/update-password y ActivityContext: cancelable, sin race tras unmount.
    • Webhooks Discord/Slack: emojis correctos (antes mojibake "ðŸ..." → ahora 💬 👤 📊 🤖).
    • a11y: motion-safe:animate-pulse en ModernDashboard (respeta prefers-reduced-motion), disabled:pointer-events-none en botón de registro.
    • void supabase.removeChannel en 3 cleanups de Realtime (no-floating-promises).

    Mejorado

    • Página de changelog rediseñada con layout tipo timeline agrupado por mes, hero dramático, stats banner y cards por área de feature.

    Arreglado

    • console.error sin filtrar stack/paths: solo .message en code-interpreter, title gen y webhooks.

    Seguridad

    • SSRF en webhooks: validateWebhookUrl bloquea localhost, 127.x, 10.x, 192.168.x, 172.16-31.x, 169.254 (metadata), ::1 y direcciones IPv6 link-local.
    • RLS tightening (migración 075): WITH CHECK (true) en token_usage → solo service_role o auth.uid()=user_id.
    • Cache in-memory de provider api_key descifrado (TTL 5min): -50-100ms por chat request en cuentas con providers custom.

    Seguridad

    • SSRF: nueva función validateWebhookUrl() en /api/webhooks que rechaza https://localhost, 127.0.0.0/8, 10.0.0.0/8, 192.168.0.0/16, 172.16-31.0.0/12, 169.254.0.0/16 (AWS metadata), ::1, fd*, fc* (link-local IPv6).
    • Aplicado en POST y PATCH de webhooks: si el URL no pasa el check, devuelve 400 con razón legible.
    • Mime whitelist en /api/files/upload: PDF, Word/Excel/PowerPoint, OpenDocument, RTF, CSV, JSON, XML, YAML, código, imágenes (PNG/JPG/WebP/GIF/HEIC/SVG), audio (MP3/WAV/M4A/OGG), video (MP4/MOV) y ZIP.
    • Octet-stream solo se acepta si la extensión está en la whitelist (.pdf, .docx, etc.). Si no, 415 con error.
    • RLS tightening en token_usage (migration 075): la policy "Service role can insert" tenía WITH CHECK (true), permitiendo a cualquier service-role insertar con cualquier user_id. Reemplazada por auth.role() = service_role OR auth.uid() = user_id.
    • Code interpreter: tras SIGTERM por timeout, programa SIGKILL forzado a los 2s para no dejar zombies de Python.
    • Logs sanitizados: console.error sin objeto Error completo (no leak de stack/paths/env).

    Velocidad de respuesta de la IA

    • Webhooks ahora en PARALELO con Promise.allSettled y timeout AbortController de 4s cada uno. Antes: secuencial bloqueando 1-3s por cada webhook configurado.
    • Title generation con AbortSignal de 3s: si OpenAI tarda más, fallback a truncar el input. Antes: sin timeout, bloqueaba el cierre 5-10s.
    • Cache in-memory 5min de provider api_key descifrado: decryptAndMigrateSecretField era ~50-100ms por chat request, ahora primer hit y los siguientes son instant.
    • Throttle de persistLiveAssistantSnapshot subido de 250ms/64 chars a 500ms/128 chars: el cliente lee del propio stream, los snapshots solo importan para colaboradores via Realtime; reduce ~50% más las writes a Supabase.
    • Total ahorrado en cierre del response (post-streaming): 1-3 segundos en condiciones típicas con webhooks/multi-tools.

    Robustez

    • body = await req.json() ahora con .catch(): si el JSON es inválido o vacío, responde 400 sin crashear el endpoint.
    • Queue wait loop con timeout máximo de 60 segundos: si conversation_live_runs queda atascada por un bug en otro request, el usuario no espera infinitamente — se fuerza el run con un warn.
    • Promise.all → Promise.allSettled en /api/admin/feedback: profiles + listUsers + conversations son queries independientes; una fallida ya no tumba la respuesta entera. Cada array hace fallback a [].
    • JSON.parse del plan de agentes en /api/agents/execute: try/catch con fallback a { steps: [] } en vez de 500.
    • Type guard de step.query (unknown → string) en agent execution para evitar passing de undefined a searchWeb.

    Lifecycle React

    • useEffect en /auth/update-password ahora cancelable: setea state solo si el componente sigue montado. Antes generaba React warning "setState on unmounted".
    • useEffect en ActivityContext igual: cancela getUser() si el contexto se desmonta antes de resolver.
    • void en cleanup de supabase.removeChannel en AdminPageClient (sync jobs), UserTokenUsage y MessageReactions: explicita que ignoramos la promesa (silencia no-floating-promises).

    UX y a11y

    • Botón "Solicitar acceso" en /auth/register con disabled:pointer-events-none: ya no se puede activar con tab+enter cuando está deshabilitado (evita doble submit).
    • Skeleton del ModernDashboard ahora es motion-safe:animate-pulse (respeta prefers-reduced-motion del sistema).
    • Webhooks Discord/Slack: el cuerpo del mensaje ahora lleva emojis legibles (💬 chat completado · 👤 usuario · 📊 mensajes · 🤖 modelo). Antes salían mojibake "💬..." por encoding doble.

    Página de Changelog (rediseño nivel dios)

    • Hero rediseñado: título dramático "Cada cambio, documentado." con tipografía 64px + gradient text sutil en la palabra "documentado".
    • Aurora gradient mesh suave en el fondo del hero (violeta/cyan/ámbar), reemplazando los orbs animados pesados.
    • Stat ribbon con 3 píldoras: "Última versión" con dot animado verde, total de versiones, total de cambios.
    • Top bar sticky con backdrop-blur y "Volver al chat" + "Releases GitHub" como atajos.
    • Timeline agrupado por MES (sticky header de mes mientras haces scroll) — antes era una lista plana sin jerarquía temporal.
    • Versión renderizada en font-mono 44px (display) con anchor link copiable (#v7.6.0).
    • Highlights en card destacada con icono ✨ "Lo destacado" y bullets con hover state.
    • Cambios detallados ahora en GRID 2 columnas: cada feature group es una card con icono semántico (Zap=perf, Shield=seguridad, Wrench=fix, Palette=ui, TrendingUp=mejora, Sparkles=default).
    • Cada card de área tiene contador en píldora ("8") y bullets compactos.
    • Footer minimalista con count de versiones + cambios totales + links a chat/GitHub/releases.
    • Dark mode nativo en todos los elementos.
    • scroll-mt-24 en cada artículo para que el anchor link no quede oculto bajo el sticky header.
    • Helpers utilitarios: getAreaIcon() detecta el icono por keywords del área, totalChangesAcrossVersions() suma highlights+features para el stat, groupByMonth() crea las secciones temporales.

    Calidad técnica

    • Typecheck verde después de cada commit (4 commits en esta release).
    • Sin rotura: todos los cambios mantienen API existente (auto_route_model sigue siendo opt-in, webhooks siguen aceptando los mismos campos, etc.).
    • Migración 075 aislada (no toca tablas, solo policy de RLS): aplicar es seguro y reversible.
    • Falsos positivos descartados tras análisis (IDOR en calendar import, path traversal en network-drive-write, trigger sin OR REPLACE en migration 009).
  27. v7.5.0

    Modo automático de modelo, paste de imágenes y limpieza de admin

    La versión 7.5.0 introduce el "Modo automático" de selección de modelo: el sistema baja a un modelo más rápido y barato (GIA-5.4-nano / GIA-5.4-mini / GIA-5.2) cuando la pregunta es trivial, y mantiene el modelo capable seleccionado para preguntas complejas. Incluye toggle visible en el menú de modelos del header, indicador "Auto · <modelo base>" en el botón, mejora del paste de imágenes desde el portapapeles (los screenshots ahora se nombran correctamente), y la limpieza de 4521 LOC de un backup huérfano del panel admin.

    Nuevo

    • Nuevo "Modo automático" de modelo: baja a GIA-5.4-nano/-mini cuando la pregunta es trivial, mantiene GIA-5.5 cuando es compleja.
    • Toggle visible en el dropdown de modelos del header con switch verde/gris y descripción contextual.
    • Indicador en el botón del menú: "Auto · GIA-5.5" con icono ✨ Sparkles cuando el modo automático está activo.
    • Heurística conservadora: pregunta trivial (saludos, ≤4 palabras) → downgrade. Adjuntos o complejidad intermedia con tools activas → mantiene capable.
    • Soporte para IDs con guión (gpt-5-5, gpt-5-4) y con punto (gpt-5.5, gpt-5.4) — comparación case-insensitive.
    • Tiempo de respuesta del modelo visible bajo cada mensaje (1.2 s, 850 ms) con dot verde.
    • Pegar imagen desde portapapeles: los screenshots ahora se nombran "pasted-image-YYYYMMDD-HHMMSS.png" con extensión correcta del mime.
    • Toast informativo al pegar imágenes: "Imagen pegada, subiendo..." (o "N imágenes pegadas...").
    • Activación opt-in del auto-routing por defecto en el frontend (ChatInput + WelcomeScreen).
    • Persistencia del toggle en localStorage: la preferencia del usuario sobrevive a refresh.

    Mejorado

    • Cuando "Auto" está activo, el menú no marca ningún modelo como seleccionado (UI más coherente).
    • Borrado AdminPageClient.backup.tsx (4521 LOC huérfano): cold start del dev más rápido.

    Arreglado

    • Cadena de aliases robusta: si el target no existe en model_configs (gpt-5-4-nano), prueba gia-5-4-nano, gpt-5-4-mini, gia-5-4-mini, gpt-5-2, gia-5-2, gpt-5-mini, antes de hacer fallback al original.

    Auto-routing de modelo

    • Nuevo módulo src/lib/server/chat/auto-router.ts con heurísticas puras y testeables.
    • looksTrivial(): detecta saludos, cortesías, preguntas de ≤4 palabras.
    • looksComplex(): detecta código (```), SQL, palabras clave técnicas (analiza, compara, plan, propuesta, etc.) y >80 palabras.
    • looksCapable(): regex laxa que detecta gpt-[4-9], gia-[4-9], claude sonnet/opus, gemini pro, mistral large, llama 70b/405b. Excluye sufijos fast (mini, nano, haiku, flash, small, turbo).
    • inferFastPeer(): mapea cualquier modelo capable a su peer rápido del mismo proveedor.
    • Soporta IDs con guión y punto: gpt-5-5 ≡ gpt-5.5 ≡ GIA-5.5.
    • Defensa: si el id ya contiene mini/nano, devuelve null (no bajamos más).
    • Pregunta trivial bypass de tools: "Hola" con RAG activo SÍ se downgradea (las tools no afectan a un saludo).
    • Complejidad intermedia con tool activa: mantiene modelo capable (la tool sí va a usarse).

    UI del menú de modelos

    • Toggle "Modo automático" arriba del listado: icono Sparkles ámbar/naranja + switch verde/gris + copy explicativo.
    • Botón del header muestra "Auto · GIA-5.5" cuando autoModelMode está ON, con icono ✨ en lugar del icon del modelo.
    • Tooltip al hover: "Modo automático activo (base: GIA-5.5)".
    • Cuando Auto está activo, los modelos del menú no se marcan como seleccionados (sin checkmark ni bg destacado) — la decisión es del backend.
    • ⭐ favorito sigue visible y persistido independientemente del modo Auto.
    • Estado autoModelMode persistido en localStorage bajo "geia:auto-model-mode".
    • Default: ON (todos los usuarios obtienen el ahorro automático sin configurar nada).

    Resiliencia del backend

    • Fallback de aliases en chat/route.ts: si el target del downgrade (p.ej. gpt-5-4-nano) no existe en model_configs, prueba 6+ alternativas antes de rendirse.
    • Si ningún alias resuelve, vuelve al modelo original sin romper la respuesta.
    • Log detallado en cada llamada con auto_route activo: input (60 chars), selected, decision, downgraded, finalModel, anyToolActive, hasAttachments.
    • Logs separados para "skip por X" y "downgrade: A → B (Y palabras)".
    • Cuando el alias se resuelve, log adicional: "[AutoRoute] target alias resolved: gpt-5-4-nano → gia-5-4-nano".

    Paste de imágenes

    • Los screenshots del portapapeles vienen sin name → ahora se generan con timestamp.
    • Formato: "pasted-image-YYYYMMDD-HHMMSS.png" / "pasted-file-...bin".
    • Detecta la extensión correcta a partir del mime type (image/png → png, image/jpeg → jpg, etc.).
    • Toast informativo al pegar: "Imagen pegada, subiendo..." o "N imágenes pegadas, subiendo...".
    • Funciona con múltiples archivos pegados en simultáneo.

    Limpieza

    • Borrado AdminPageClient.backup.tsx (4521 LOC sin uso) — repo más limpio, ESLint ya no lo escanea.
    • Cold start del dev más rápido (un archivo gigante menos que tipar).

    Calidad técnica

    • Typecheck verde tras cada commit.
    • auto-router.ts puro y sin dependencias de Supabase: testeable como función pura.
    • Heurísticas regex con flags case-insensitive y soporte para múltiples separadores (- _ .).
    • API del auto-router consistente: AutoRouteResult con { model, reason, downgraded } siempre poblado.
    • Activación segura: opt-in vía body flag auto_route_model. Por defecto se respeta el modelo seleccionado.
  28. v7.4.0

    Rediseño minimalista, rendimiento del chat y nuevo sidebar Sleek

    La versión 7.4.0 unifica la estética del producto bajo un sistema minimalista estilo ChatGPT (#f9f9f9 / #171717), introduce un sidebar nuevo "Sleek" con dos modos (expandido y colapsado), rediseña por completo la pantalla de bienvenida, mejora la velocidad de respuesta de la IA reduciendo writes durante el streaming y paralelizando queries pre-modelo, y limpia más de 200 textos con encoding roto en el panel de administración.

    Nuevo

    • Nueva pantalla de bienvenida limpia: tipografía sin gradientes, logo elegante, starter prompts y dark mode nativo.
    • Sidebar "Sleek" único para móvil y desktop con modo colapsado tipo ChatGPT (rail de iconos + tooltips).
    • Estilo minimalista uniforme en chat, admin y sidebar: fondo plano, fuentes coherentes, sin glass morphism pesado.
    • Tiempo de respuesta del modelo visible bajo cada mensaje (1.2 s, 850 ms, etc.) con dot verde.
    • Lazy load de modales (settings, voice, file preview, collaborators) y librerías pesadas (tesseract.js, pdfjs-dist, mammoth, xlsx) — bundle inicial 30%+ más pequeño.
    • Logo del sidebar limpia el contexto antes de navegar — ahora siempre lleva al welcome.
    • Realtime reconnect automático: si el canal de Supabase se cae, recarga mensajes sin requerir refresh manual.
    • Watchdog de 90 s en el reader del stream: nunca más spinner colgado para siempre.
    • Toast cuando una respuesta no se pudo guardar — antes desaparecía silenciosamente al recargar.

    Mejorado

    • Respuestas de la IA hasta 600ms más rápidas: paralelización de queries pre-modelo, menos writes durante streaming, cache in-memory de network drives.
    • Cache global con React Query: navegación instantánea entre páginas, menos hits a Supabase.

    Arreglado

    • Fix del modelo preferido (⭐): se restaura siempre al volver a la welcome o crear chat nuevo, no solo al abrir la app.
    • Más de 200 textos con caracteres rotos arreglados en el admin (mojibake UTF-8 ↔ Latin-1).
    • Error boundary específico del chat — un error en MessageBubble ya no rompe toda la app.

    Pantalla de bienvenida

    • Layout rediseñado con jerarquía clara: status pill → logo → saludo → typewriter → composer → starters.
    • Saludo sin gradient text, tipografía limpia con apellido/nombre del usuario en color secundario.
    • 4 starter prompts de un click (analizar documento, investigación profunda, buscar web, archivos de red) que envían el mensaje al instante.
    • Avatar pill compacto con dot de estado (online / typing / reading / offline) sin glass morphism pesado.
    • Animaciones suavizadas con respeto a prefers-reduced-motion (sin bounce-in al cargar).
    • Dark mode nativo: fondo #171717, paneles #0a0a0b, texto y borders adaptados.
    • Quick actions móvil reducidas a píldoras compactas (rounded-14 vs rounded-2xl) sin sombras saturadas.
    • Subtitle del calendario en color ámbar discreto, sin shadow ni backdrop-blur.

    Sidebar Sleek (nuevo)

    • Componente único SidebarSleek que sustituye Sidebar.tsx (1044 LOC) y MobileSidebar.tsx (995 LOC) — total ahorrado: ~1200 LOC.
    • Modo expandido (260px): logo, búsqueda con ⌘K visible, nav items (imágenes, canales, apps, nueva carpeta), grupos por fecha (Hoy, Ayer, Últimos 7/30 días, mes a mes), pinned arriba, footer con user card.
    • Modo colapsado (60px): rail de iconos estilo ChatGPT con tooltips a la derecha, badge de canales sin leer, avatar al pie con menú emergente (Ajustes, Admin, Cerrar sesión).
    • Click en logo del sidebar limpia projectContextId, folderContextId y activeConversationId antes de navegar — siempre lleva al welcome.
    • Modal de 3 puntos rediseñado estilo ChatGPT minimal: sin headers ni icon tiles, solo iconos pelados con texto.
    • Iconos de carpeta correctamente mapeados (FolderOpen, Brain, Briefcase, etc.) — antes mostraba la cadena cruda "folder", "work", "brain".
    • Pin/Star/Share como mini-iconos a la derecha de cada chat (ámbar/sky) en lugar de chips de texto.
    • Resize handle agarrable: hit area de 8px con línea visible de 1px, antes era 1px imposible de pillar.
    • Empty state con CTA: glow pulsante + botón "Crear primer chat".
    • Atajos de teclado: ⌘K enfoca filtro, "/" focus rápido, click en chat con Cmd/Ctrl abre en nueva pestaña.
    • Drag & drop de carpetas y chats con indicadores visuales suaves.

    Velocidad de respuesta de la IA

    • Throttle de snapshot durante streaming subido de 90ms / 16 chars a 250ms / 64 chars — reduce 80% las writes a Supabase durante respuestas largas.
    • syncLiveRun ya no se llama por cada token, solo en cambios de fase reales — reduce a la mitad las writes restantes.
    • Profile select específico (id, name, avatar_url, role) en lugar de select('*') — ahorra 20-50ms por request.
    • Queries pre-modelo paralelizadas con Promise.all (participantes + profile en lugar de serial) — ahorra 100-200ms TTFT.
    • Cache in-memory de 60s para "¿usuario tiene network drives sincronizados?" — evita query Supabase repetida en cada chat.
    • Tiempo total ahorrado al primer token: 300-600ms en condiciones típicas.

    Tiempo de respuesta visible

    • Bajo cada mensaje del asistente aparece el tiempo del modelo: "1.2 s", "850 ms", "1m 5s".
    • Dot verde junto al tiempo para identificarlo visualmente.
    • Tooltip al hacer hover muestra los milisegundos exactos.
    • Se persiste en meta_json.response_time_ms y meta_json.generation con tokens y provider.

    Performance frontend

    • React Query (@tanstack/react-query) instalado y montado como provider raíz para cache global de queries.
    • Hook useUnreadChannels migrado a React Query: cache compartido + Realtime auto-invalidate, varios componentes ya no fetchean por separado.
    • Hook useFolders disponible para futuras migraciones (sidebar, knowledge, admin).
    • 5 orbs animados con blur-90px eliminados del ChatShell — ahorro de 5-10% de CPU constante en desktop.
    • Modales lazy-loaded en SidebarSleek (CreateFolderModal, ConversationCollaboratorsModal) — solo cargan al abrirlos.
    • next/image en avatares de Header con AVIF/WebP automático — 40% menos peso en imágenes externas.
    • Dominios añadidos al config: googleusercontent.com, *.supabase.co, i.scdn.co, graph.microsoft.com.
    • Cache-Control en /api/public/app-settings y /api/public/welcome-messages: stale-while-revalidate de 5-10 min en lugar de no-store.
    • Throttle del reader del stream (90s sin chunks aborta) ya integrado en ChatInput.

    Bugs críticos del chat

    • Realtime de Supabase reconecta automáticamente si CHANNEL_ERROR / TIMED_OUT / CLOSED — antes los mensajes nuevos no llegaban hasta refresco manual.
    • Insert de mensaje del usuario captura el error explícitamente — antes era silencioso y rompía source_message_id.
    • regenerate_message_id se valida antes de continuar — antes podía duplicar tareas si el ID no existía.
    • Persistencia fallida del mensaje del asistente emite marcador __PERSISTENCE_FAILED__ al cliente — antes la respuesta se perdía sin aviso.
    • Frontend muestra toast de 8s cuando la respuesta no se pudo guardar.
    • ChatErrorBoundary envuelve ChatArea — un error en MessageBubble ya no tumba toda la app.

    Modelo preferido

    • El modelo preferido (⭐) ahora se restaura al volver a la welcome o iniciar chat nuevo, no solo al abrir la app por primera vez.
    • Antes la lógica solo aplicaba el preferido cuando selectedModel era el default inicial "gpt-5.2" — al navegar por chats con otros modelos, el preferido se perdía.
    • Ahora dos casos cubiertos: primera carga + activeConversationId === null — sigue respetando el modelo del chat existente cuando se entra en uno.

    Panel de administración

    • Shell minimalista: fondo #f9f9f9 (light) / #171717 (dark) con dark mode nativo en header, sidebar y dashboard.
    • Eliminados los 3 orbs animados de fondo y el "liquid-glass" pesado.
    • Header del admin sin gradient text en "Panel Admin" — tipografía limpia tracking-tight.
    • Sidebar nav reducido a 256px con bg sólido, items con bg-zinc-200 al activar (sin shadow + scale).
    • Cards del dashboard sin liquid-glass: border + bg-white sólido, hover sutil sin scale-105.
    • Storage stats sin gradient text en cifras: tipografía font-semibold tracking-tight.
    • 205 textos con encoding mojibake corregidos: Conexión, automáticamente, análisis, ningún, podrán, extraído, etc.
    • Emojis arreglados en el admin: 📊 📧 📁 👥 💡 🌐 ✅ ❌ ⭐.
    • Símbolos arreglados: ¿ ¡ ° ½.
    • Script reusable scripts/fix-encoding.mjs con mapeo UTF-8 ↔ Latin-1 doble.

    Estilo y consistencia visual

    • Sistema de colores unificado: light #f9f9f9, dark #171717 (mismos que el sidebar y el shell).
    • Radios reducidos a 3 escalas (8/12/16/20px) en lugar de 5 distintos.
    • Tipografía a 3 tamaños principales (10px labels, 12.5-13.5px contenido, 15px brand).
    • Iconos Lucide con strokeWidth 1.75 en secundarios y 2 en primarios para crear jerarquía.
    • Eliminadas sombras saturadas (shadow-xl, shadow-blue-500/30) — ahora shadow-sm o sin sombra.
    • Backdrop-blur eliminado de zonas donde no aporta (footers sin contenido detrás).

    Calidad técnica

    • Typecheck verde después de cada cambio sin excepciones.
    • Persistencia del mensaje del asistente con metadatos de generación (modelo, provider, tokens, latencia).
    • Component memo + comparador custom en ChatRow del sidebar para evitar re-renders.
    • Selectores específicos en queries Supabase en lugar de select('*').
    • Cache-Control con stale-while-revalidate en endpoints públicos.
    • Eliminados los 5 orbs animados que costaban CPU constante en desktop.
  29. v7.3.5

    Persistencia estable de busquedas y composer movil corregido

    La version 7.3.5 estabiliza las respuestas generadas con busqueda web y Deep Research para que no desaparezcan al refrescar o cambiar de chat, y corrige el composer movil para que el icono de enviar quede integrado correctamente dentro del input.

    Nuevo

    • El cliente reintenta cargar mensajes persistidos antes de crear un fallback local, evitando parpadeos y respuestas perdidas.
    • El input movil separa definitivamente las acciones de voz, stop, cola y enviar para evitar iconos fuera de sitio.
    • El boton de enviar movil solo aparece cuando hay texto o adjuntos, y se mantiene dentro de la capsula del composer.
    • Se anaden protecciones CSS para impedir que el boton de enviar se renderice si el input esta vacio.
    • Version visible actualizada a 7.3.5 en header PC y header movil.

    Seguridad

    • Las respuestas finales de busqueda web y Deep Research se persisten con mas seguridad antes de cerrar el streaming.

    Streaming y persistencia

    • El backend refuerza la escritura de respuestas finales cuando una peticion con herramientas termina o se interrumpe despues de haber generado contenido util.
    • Las respuestas de busqueda web y Deep Research dejan de depender solo del estado live temporal para sobrevivir a refresh, cambio de chat o recarga de mensajes.
    • La recarga final del cliente espera varias ventanas cortas de sincronizacion antes de decidir si necesita insertar un fallback optimista.
    • Los placeholders live_streaming no se tratan como respuestas definitivas si no contienen contenido final valido.

    Composer movil

    • Rama movil dedicada para acciones del input: microfono, modo conversacion, enviar, stop y cola ya no comparten el layout desktop.
    • El icono de enviar se oculta cuando no hay contenido y aparece como boton interno cuando el usuario escribe o adjunta archivos.
    • El stop de streaming y el contador de cola mantienen tamano fijo para evitar desplazamientos visuales en la barra inferior.
    • CSS defensivo con data attributes para bloquear estados inconsistentes en el composer movil.

    Calidad tecnica

    • Typecheck completo ejecutado tras los cambios del composer y la persistencia.
    • Package version y lockfile sincronizados con 7.3.5.
    • Changelog local actualizado para documentar las correcciones incluidas en esta entrega.
  30. v7.3.0

    Nombre publico simplificado, streaming web estable y Deep Research recuperado

    La version 7.3.0 simplifica el nombre publico de la pagina inicial para que el navegador muestre solamente GIA, mantiene intactos los titulos dinamicos de conversaciones, corrige carreras de streaming tras busquedas web, recupera el viewport de Deep Research y evita que estados colaborativos de chats compartidos afecten a chats normales.

    Nuevo

    • El titulo base de la web pasa de "GIA - Gestion Empresarial con IA" a "GIA".
    • Los chats conservan sus titulos dinamicos con formato "Nombre del chat - GIA".
    • Version visible actualizada a 7.3.0 en header PC y header movil.
    • Las respuestas generadas con busqueda web dejan de desaparecer al finalizar el streaming.
    • Deep Research vuelve a emitir frames live en la card inline y en la ventana flotante.
    • Los estados de chats compartidos ya no renderizan tarjetas live dentro de chats normales.

    Identidad y metadata

    • Metadata global de Next.js actualizada para que la pagina inicial use el titulo GIA.
    • No se modifica la logica de ChatShell que genera titulos por conversacion, proyecto o carpeta.
    • Package version y lockfile sincronizados con 7.3.0.

    Chat y busqueda web

    • Serializacion de snapshots live para impedir que estados antiguos como "investigando" sobrescriban la respuesta final.
    • El cierre del streaming espera el snapshot final done antes de recargar mensajes.
    • El cliente ya no considera placeholders live_streaming=true como respuestas persistidas validas.
    • Fallback local reforzado: si el placeholder sigue visible tras una busqueda web, se convierte en la respuesta final sin parpadeo.
    • Los placeholders colaborativos vacios se ocultan en chats privados para evitar bloques residuales bajo respuestas normales.

    Deep Research visual

    • El endpoint de eventos live acepta conversaciones con acceso de owner o participante, no solo consultas owner-only.
    • El hook de live viewport rehidrata historial y mantiene polling controlado para recuperar frames aunque el SSE llegue tarde.
    • La card inline y la ventana flotante muestran errores/fallbacks claros si Playwright no puede arrancar en vez de quedarse cargando indefinidamente.
    • ChatInput envia research_mode y deep_research_mode para que el backend respete el modo rapido o exhaustivo seleccionado.
    • Playwright publica eventos de error de navegador cuando falta configuracion o no se puede crear el contexto visual.

    Chats compartidos aislados

    • El backend solo considera colaborativa una conversacion si tiene participantes activos distintos del propietario.
    • MessageBubble solo renderiza tarjetas live de colaboracion cuando la conversacion esta marcada como compartida.
    • ChatArea limpia streams y pendientes compartidos al entrar en chats normales.
    • El refresco rapido de estados compartidos queda desactivado en chats privados para evitar parpadeos y bloqueos visuales.

1118 versiones publicadas · página 36 de 38. Míralo dentro de la app.