Ir al contenido

Frontend del Asesor de IA

La UI del asesor sigue la misma forma hexagonal que el resto de apps/frontend: un puerto, un adaptador REST, DTOs validados con Zod, Pinia Colada para el estado del servidor y componentes organizados por funcionalidad. Ver Arquitectura Hexagonal.

flowchart TD
    Header["AppHeader.vue<br/>Botón Sparkles, Mod+/"] --> PanelState["useAdvisorPanel<br/>singleton de módulo"]
    PanelState --> Panel["AdvisorPanel.vue (montado una vez en App.vue)"]
    Panel --> ChatC["useAdvisorChat(port)"]
    Panel --> CfgQ["useAdvisorConfigQuery<br/>activa mientras está abierto"]
    ChatC --> Turn["AdvisorTurn.vue"]
    Turn --> MD["AdvisorMarkdown.vue<br/>markdown-it"]
    Turn --> Tools["AdvisorToolActivity.vue"]
    Turn --> Outcome["AdvisorOutcome.vue"]
    Panel --> Empty["AdvisorEmptyState.vue"]
    ChatC --> PortF["IAdvisorPort (ADVISOR_PORT_KEY)"]
    CfgQ --> PortF
    PortF --> RestA["RestAdvisorAdapter"]
    SettingsView["SettingsView.vue"] --> AS["AdvisorSettings.vue"]
    AS --> ModelEd["ModelChainEditor + useModelChainEditor"]
    AS --> ProfEd["ExecutionProfilesEditor + useExecutionProfilesEditor"]
    ModelEd --> Mut["useSetModelChainMutation"]
    ProfEd --> Mut2["useSetExecutionProfilesMutation"]
    Mut --> PortF
    Mut2 --> PortF

IAdvisorPort (frontend) declara el ask(request, signal) con streaming junto a los métodos de configuración, siguiendo el precedente de market-data. RestAdvisorAdapter lee el stream a mano: el EventSource del navegador no puede hacer POST y se reconecta por sí solo, lo que volvería a ejecutar (y a facturar) en silencio una llamada al LLM ya terminada. Todo reintento es una acción explícita del usuario. El adaptador informa cada fallo de transporte al errorBus exactamente una vez; el consumidor nunca vuelve a mostrar un toast. Las llamadas de configuración usan bffClient y validan con AdvisorConfigSchema / executionProfilesSchema.


  • useAdvisorChat mantiene los turnos y el id de hilo solo en memoria. Expone send, abort, retry y newConversation. Los resultados de turno son los resultados de red más dos locales, transport-lost y user-aborted.
  • isRetryableOutcome permite reintentar tras un error de red, fin de stream, HTTP 5xx o 429, una ejecución fallida cuya causa es temporal (rate-limited, provider-unavailable, network, unknown) o una parada del usuario; no tras un frame malformado, un cuerpo ausente, un HTTP 4xx distinto de 429, una clave rechazada o un modelo desconocido.
  • El estado del servidor (configuración, perfiles) pasa por Pinia Colada (useAdvisorQueries.ts, useAdvisorMutations.ts); no hay store global de Pinia. La query de configuración solo está activa mientras el panel está abierto.
  • useAdvisorPanel es un singleton de módulo porque el botón de la cabecera y el panel viven en subárboles no relacionados.

  • Una hoja lateral derecha que se abre desde el botón de la cabecera o con Mod + / (Cmd en macOS, Ctrl en el resto). Escape la cierra y el foco vuelve al disparador.
  • Las respuestas se transmiten como Markdown (markdown-it): HTML crudo desactivado, imágenes desactivadas, sin autoenlazado, solo las rutas internas de la app son clicables; el código en línea se renderiza con font-mono tabular-nums.
  • Cada llamada a herramienta muestra una fila de actividad (en curso, terminada o fallida).
  • La cabecera muestra el id del modelo activo y una insignia local / cloud obtenida de classifyExecutionProfile, la misma regla que el backend. El modelo activo es el que respondió el último turno o, en su defecto, la primera entrada de la cadena.
  • Bajo una respuesta: una insignia “Cifras incompletas”, el pie del descargo (solo cuando disclaimer es true, nunca en línea) y un pie atenuado stepsUsed / maxSteps una vez terminada la ejecución.
  • Una ejecución refused muestra una alerta “Respuesta retenida” con un chip de reformulación. Una ejecución failed muestra un texto por código; NO_MODEL_AVAILABLE y VAULT_LOCKED enlazan a Ajustes, y ALL_PROVIDERS_FAILED elige el texto según cause.kind nombrando el proveedor y el modelo (auth-rejected apunta a la clave API, model-not-found al id del modelo).
  • Enter envía, Shift+Enter inserta un salto de línea, se respeta la composición IME; Stop aborta de inmediato; un segundo envío durante una ejecución se ignora. El autoscroll se pausa mientras el usuario se desplaza hacia arriba (useStickToBottom). El modo de movimiento reducido desactiva la animación de deslizamiento y del cursor.
  • AppToaster.vue desplaza los toasts por debajo de la cabecera de la hoja mientras el panel está abierto para que no tapen el botón de cierre.
  • El estado vacío ofrece preguntas sugeridas en dos grupos, “Impuestos” y “Tu cartera”.

  • Editor de la cadena de modelos: añadir, quitar, reordenar; selector de proveedor, id de modelo y una entrada contextWindow solo para una entrada ollama local; etiquetas derivadas local/nube y de perfil; indicador de credencial para proveedores con clave; una cadena vacía no se puede guardar.
  • Editor de perfiles de ejecución: límites numéricos con los techos del esquema aplicados en el cliente, presupuestos por herramienta medidos editables, presupuestos locales mostrados en solo lectura como derivados de la ventana declarada, restablecimiento a valores por defecto. El estado del formulario es texto, de modo que un número a medio escribir nunca se coacciona antes de tiempo.
  • Las claves API de proveedores se introducen en Ajustes → Bóveda, en el grupo “AI Providers”.

  • i18n: todas las cadenas usan useI18n(); las claves viven bajo advisor.*, settings.advisor.* y vault.category.* en en.ts y es.ts. El helper interpolate rellena los marcadores {name} en una sola pasada para que los valores que contengan $& o {other} permanezcan literales.
  • Sistema de diseño: solo se usan tokens de DESIGN.md; designTokens.spec.ts escanea los componentes del asesor en busca de clases inventadas. Los colores de ganancia y pérdida nunca colorean prosa. Ver Diseño Institucional Claro.