Widget de chat OrbitPilot — transmisión, móvil, móvil y voz
Qué es
OrbitPilot es ahora un widget de chat flotante global para usuarios registrados: a un botón de 90 en la esquina se abre un panel que puedes usar desde cualquier página. El panel es no bloqueante (sigue navegando mientras está abierto), arrastrable, colapsable, y recuerda su posición y tu conversación abierta mientras te mueves por el sitio.
Nuevas habilidades
- Respuestas en streaming — la respuesta aparece palabra por palabra en lugar de después de un tiempo de espera.
- Detener — detiene la IA a mitad de respuesta. Detener guarda lo que se ha escrito hasta ahora y no consume una pregunta de tu saldo mensual.
- Copiar / Descargar todo el chat — botones en la parte superior e inferior del panel (la descarga es una transcripción .txt).
- Copiar un solo mensaje — pasa el ratón sobre cualquier burbuja para un ícono de copia.
- Maximizar la ventana, navegar por tu historial de chat, y comenzar una Nueva conversación.
- Entrada de voz — toca el micrófono y habla; tus palabras se escriben por ti.
Los invitados (no registrados) mantienen la ventana emergente de chat de la página de inicio original.
En tu teléfono (móvil)
- El panel se acopla a la parte inferior de la pantalla y nunca se derrama más allá de los bordes, en cualquier orientación — rotar el teléfono lo ajusta automáticamente.
- Respeta la muesca y el indicador de inicio (áreas seguras), y utiliza la altura de la pantalla en vivo para que la barra de direcciones del navegador no pueda recortar el botón de Enviar.
- Después de que hables y envíes, el cuadro se vacía para tu próxima pregunta — toca el micrófono nuevamente para hablar en el siguiente turno.
Para desarrolladores
Streaming requiere una vista asíncrona bajo ASGI (un generador sincrónico almacena en búfer antes del
primer byte). orbitpilot.views.orbitpilot_ask_stream es un async def que regresa
StreamingHttpResponse sobre un generador asíncrono que emite Eventos Enviados por el Servidor
(meta e token e done). Todo ORM/gating corre en dos ayudantes SINCRONIZADOS
(_prepare_ask_stream / _finalize_ask_stream) a través de
sync_to_async(thread_sensitive=True). Detener = el cliente aborta la
búsqueda e el generador asíncrono es cancelado e la respuesta parcial se guarda y la fila de cuota NO
se escribe. El frontend vive en templates/_orbitpilot_widget.html +
static/css/orbitpilot-widget.css + static/js/orbitpilot-widget.js, incluido
en base.html solo para usuarios autenticados.
Responsividad móvil (Workstream E): todo el widget es
box-sizing: border-box así que el borde de 1px cuenta dentro del ancho (el viejo iPhone
overflow era width:100vw + border). La regla @media (max-width:575px) elimina
100vw para left/right: env(safe-area-inset-*) + width:auto, los tamaños
se manejan con dvh (con un vh de respaldo), y se complementa el compositor con
env(safe-area-inset-bottom). En JS, una posición de arrastre guardada de escritorio no se aplica
en una vista móvil, y un oyente resize/orientationchange vuelve a fijar la
posición a través de applyPos(). Limpieza de voz: el módulo de voz mantiene un saved finalizado
separado de la entrada; send() ahora llama a un resetVoice() compartido
(detiene el reconocimiento, limpia el temporizador de silencio, borra saved) justo
en el punto donde se limpia input.value, así que el siguiente turno comienza vacío.
Tamaño de fuente del chat + solución de envío móvil (2026-07-27)
- A1 / A+ en el encabezado del widget reduce/aumenta el texto del chat. Tu elección se recuerda
(guardado en el navegador a través de
localStoragecon la claveop_widget_font) en todas las páginas y sesiones. El tamaño se restringe a 12-22px. - Botón Enviar en teléfonos: la fila del compositor ahora mantiene visibles los botones de Enviar (y micrófono) junto a la caja de texto en pantallas estrechas — anteriormente la caja de texto podría empujar Enviar fuera del borde derecho.
Para desarrolladores: marcado templates/_orbitpilot_widget.html, comportamiento
static/js/orbitpilot-widget.js (applyFont()/bumpFont(), restaurado en la inicialización), estilo
static/css/orbitpilot-widget.css. Los botones aumentan una variable CSS --op-chat-font leída por el
cuerpo del mensaje + entrada; el compositor usa min-width:0 en el área de texto y flex:0 0 auto en
los botones para que Enviar nunca quede fuera de la pantalla.
Menú hamburguesa + modo de pantalla completa (2026-07-27)
El encabezado del widget de chat ahora tiene un menú y un botón de pantalla completa:
- Tus instrucciones — notas permanentes que la IA siempre considera (como instrucciones personalizadas de ChatGPT).
Guardadas en
OrbitPilotSettings.custom_user_promptvíaorbitpilot_custom_instructions; restringido por la funcióncustom_prompt(un mensaje bloqueado + un enlace de mejora aparece si el plan no lo permite; el servidor también lo hace cumplir con un 403). - Descargas — ahora claramente dos opciones: Esta conversación solamente
(
orbitpilot_conversation_download) vs Todo mi historial de chat (nuevaorbitpilot_history_download). El botón de la barra de herramientas se renombra Este chat. - Memoria e Reconstruir mi memoria ahora — una reconstrucción completa
(
orbitpilot_rebuild_memory) detrás de una advertencia de confirmación de costo (vuelve a leer todo y reembeda, lo que utiliza créditos de IA); no se acumulará una reconstrucción mientras una esté genuinamente en curso. (BL-404, 2026-09-07) Si esa construcción de "ya en curso" en realidad falló o fue detenida por un reinicio del servidor — atascada en planeada o ejecutándose mucho más allá de una ventana segura (10 minutos con el trabajo en segundo plano activado, 3 minutos sin él) — presionar el botón ahora lo limpia automáticamente y comienza una nueva reconstrucción, utilizando el mismo cosechador que el Inspector de Booklet ha usado siempre (orbitpilot.services.reap_stuck_builds). La respuesta dice que se limpió una construcción atascada en lugar de pretender que no pasó nada. Antes de esta solución, una construcción muerta en ese estado bloqueaba la actualización para siempre, sin forma de desbloquearla salvo que un administrador abriera el Inspector.
Modo de pantalla completa () abre /orbitpilot/chat/ en una nueva pestaña, un chat de pantalla completa estilo ChatGPT/Gemini
que reutiliza el MISMO widget (a través de la clase del cuerpo op-fullpage) — por lo que cambiar entre el
pequeño widget y la página completa mantiene la misma conversación (almacenamiento compartido op_widget_conv en
localStorage + un oyente de eventos storage). Además: A1 / A+ cambia la fuente del chat (recordada), y en un
teléfono el botón Enviar se mantiene visible junto al micrófono.
Para desarrolladores: marcado templates/_orbitpilot_widget.html, comportamiento
static/js/orbitpilot-widget.js, estilo static/css/orbitpilot-widget.css; vista de pantalla completa
orbitpilot.views.orbitpilot_chat e orbitpilot/templates/orbitpilot/chat_full.html.
Actualización rápida vs Reconstrucción completa (memoria suave/dura, 2026-07-27)
El menú 40 Memoria del widget ahora ofrece dos reconstrucciones:
- 81 Actualización rápida (suave) — vuelve a leer y reembeda solo los elementos que has cambiado desde la última construcción; los elementos no cambiados mantienen sus incrustaciones existentes, los elementos eliminados se eliminan. Mucho más barato, porque el costo de incrustación de OpenAI escala con el delta, no con todo el corpus.
- B0 Reconstrucción completa (dura) — vuelve a leer y reembeda todo desde cero (usa más créditos de IA). Úsalo solo si algo parece incorrecto.
Para desarrolladores: run_memory_build(user, settings, build, incremental=True) diferencia los nuevos
fragmentos en contra de los almacenados por la clave (source_key, source_id, chunk_index, content_hash)
antes de llamar a embed_chunks, por lo que solo los fragmentos nuevos/cambiados se incrustan; las filas obsoletas se
eliminan en el mismo bloque atómico; las filas no cambiadas quedan intactas. El modo se mantiene en
OrbitPilotMemoryBuild.incremental (migración orbitpilot/0025) y se establece por
orbitpilot_rebuild_memory desde mode=soft|hard. Si incremental se omite,
run_memory_build lo lee del registro de construcción. La ruta completa/dura mantiene exactamente el comportamiento atómico previo de eliminar-todo + re-incrustar.
Correcciones impulsadas por el chat (2026-07-28)
Analizando conversaciones reales de OrbitPilot (redactadas de PII) surgieron cuatro correcciones:
- Enlaces de IA rotos (
your-app-link): el modelo inventó un dominio cuando se le entregaron rutas relativas de un folleto obsoleto. Ahora (a) un chequeo de Salud del Sistema advierte siSITE_URLes localhost en producción, y (b)orbitpilot.services.heal_links()reescribe cualquier enlace de marcador de posición/localhost hacia el host del sitio real en cada respuesta (stream/blocking/invitado) — así que los enlaces funcionan incluso antes de una reconstrucción. La solución permanente (establecerSITE_URL+ reconstruir) está en la Lista de Verificación de Configuración de Admin. - Listar mis Blinks ahora funciona:
pipeline/retrieve.py add_list_intent_sources()inyecta TODOS los fragmentos de una fuente cuando la consulta es una solicitud de enumeración ("listar/mostrar/todos mis blinks/tareas/eventos"), y la instantánea del espacio de trabajo muestra títulos de Blink — así que la IA lista elementos reales, no solo el conteo. - Acciones siguientes clicables: las acciones son ahora
{label, url}. Un enlace de navegación real se renderiza como un botón clicable (se abre en una nueva pestaña); de lo contrario es un chip de pre-llenado. Arreglado en todo el widget, el modal de inicio de invitado (que anteriormente eliminaba acciones), y el resultado de pantalla completa. Los enlaces de respuesta en línea también se abren en una nueva pestaña ahora. - Pulido del prompt: el prompt de respuesta central deja de repetir en exceso la meta principal del usuario y maneja con gracia "¿qué cambió desde ayer?" (funciona desde la instantánea actual, no desde el historial de día a día).
Para desarrolladores: orbitpilot/services.py (heal_links, request_base,
split_answer_actions ahora que devuelve diccionarios, capa de acciones siguientes para invitados next_actions),
pipeline/retrieve.py (add_list_intent_sources), adminapp/diagnostics.py
(_check_site_url), JS del widget + home.html (renderizado de acciones + DOMPurify target=_blank hook),
aihub/prompt_registry.py (definiciones clave + acciones siguientes; actualizadas en Defaults sembrados inalterados a través de
aihub/0007).
Una barra de herramientas, no dos (2026-08-28)
Qué cambió. El widget solía dibujar la misma fila de botones — Nueva 8 · Historial · Copiar · Este chat — dos veces: una directamente debajo del encabezado púrpura, y nuevamente debajo de la caja de mensajes. Ahora hay una, debajo de la caja de mensajes.
Por qué. Informe #136, de un teléfono. Dos filas de chrome dentro de una ventana ya corta dejaban la conversación con lo que estaba más allá. El informe trataba sobre lo poco espacio que tenía la conversación.
Por qué la fila inferior es la que quedó. Es la que un pulgar llega en un teléfono, se encuentra al lado de la caja en la que escribes — donde "iniciar un nuevo chat" y "copiar este chat" pertenecen — y ya llevaba todos los botones que tenía la otra. Las instrucciones de la fila superior eran la redacción más clara, así que bajaron con ella.
Nada se volvió inaccesible. El encabezado conserva su propia , así que un widget colapsado aún se cierra; y Cerrar se mantiene fuera de la verificación de inicio de sesión, así que un invitado aún puede cerrar
el widget. Protegido por orbitpilot/test_widget_toolbar.py.
El micrófono: una intención, una sesión (BL-447, 2026-09-09)
El propietario reportó el micrófono del chat como "la primera vez funcionando, luego la segunda vez no funciona, y a veces se queda atascado". La producción ya estaba llevando la solución previa cuando lo dijo, así que esto fue lo que quedó debajo, no un despliegue fallido. Tres cosas estaban mal, y explican los tres síntomas diferentes.
1. El botón podría significar lo opuesto a lo que mostraba
El manejador de clics decidía qué hacer leyendo una variable
(wantListening, la solicitud del usuario) mientras que el estado de pulso rojo era pintado a partir de otro (listening, el estado real del motor). La llamada stop() del navegador es asíncrona, así que después del tiempo de silencio de seis segundos
y después de cada Enviar hay una ventana donde la solicitud ya está apagada mientras el motor
sigue funcionando — y el botón todavía parece activo. Una presión considerada como detener fue
por lo tanto leída como iniciar, y la solución anterior (correctamente, para su propio caso) mantuvo esa
presión activa para que el motor se reiniciara. Presionar detener volvió a encender el micrófono.
La imagen espejo de la misma brecha hizo que una segunda presión cancelara la primera presión del usuario, así que el
micrófono nunca comenzó en absoluto — eso es "la segunda vez no funciona".
Ahora hay una variable. micOn es lo que el usuario pidió; el botón se dibuja
con ello y el clic lo lee, así que no pueden discrepar.
2. Un objeto de reconocimiento sirvió toda la página
Un solo SpeechRecognition se construyó cuando se cargó la página y se reutilizó para siempre.
Esa es la única razón por la cual el inicio podría fallar con un error de "estado no válido" en absoluto, y un objeto
atascado por una interrupción de red o un micrófono utilizado por otra aplicación permaneció atascado hasta que se recargó la página
— "funciona una vez, luego nunca más". Cada otro micrófono en el sitio (el cuadro de captura de la página de inicio, el reportero de errores, la dictación de administrador) ya construye uno nuevo por presión. El widget de chat ahora hace lo mismo, así que cada presión obtiene un motor limpio.
3. Cerrar la ventana no liberaba el micrófono
close() nunca detenía el reconocimiento y nada lo detenía al dejar la página,
por lo que cerrar el chat a mitad de dictado dejaba el motor funcionando detrás de un panel oculto: el indicador de grabación del navegador se mantenía encendido, el bucle de reinicio seguía rearmándose, y las palabras seguían aterrizando en una caja
que nadie podía ver. Eso es "el atasco". Cerrar la ventana y dejar la página ahora liberan
el micrófono.
Qué más cambió que puedas ver
- Los fallos dicen lo que pasó. Permiso bloqueado, no se encontró el micrófono, y "esa presión no inició nada" solían parecer idénticos — un botón gris y el marcador de posición ordinario. Cada uno ahora escribe su propio mensaje corto en la caja de mensajes.
- Un motor roto se rinde en lugar de girar. La dictación todavía se reinicia a través de una pausa de pensamiento, que es el comportamiento que comparte cada micrófono aquí, pero una sesión que sigue cayendo sin escuchar nunca el habla se detiene después de un puñado de intentos y lo dice. El habla real rellena ese presupuesto, así que la dictación ordinaria nunca se agota.
- El cuadro de chat solo toma el teclado cuando está en la pantalla. Terminar la dictación solía enfocar la entrada incondicionalmente, lo que podría llevar el teclado a una ventana que acaba de cerrarse.
Para desarrolladores. La sesión que muere es huérfana — la referencia a la sesión actual
se establece en null y cada manejador verifica su propia identidad antes de tocar cualquier cosa — así que
un resultado final tardío después de Enviar es ignorado porque pertenece a una sesión que ya no pertenece a nadie.
Eso mantiene la garantía que los dos arreglos anteriores intentaban alcanzar sin una bandera que podría quedar
armada. La lógica vive en static/js/orbitpilot-widget.js (el
initVoice bloque) y la forma está fijada por
adminapp/test_widget_mic.py y adminapp/test_voice_parity.py. El habla del navegador
no puede ejecutarse en el conjunto de pruebas; una prueba de humo de Chromium real es un elemento propuesto separado.
This guide was translated automatically. Read the English original.