Skip to content

AI Advisor Frontend

The advisor UI follows the same hexagonal shape as the rest of apps/frontend: a port, a REST adapter, Zod-validated DTOs, Pinia Colada for server state, and feature-sliced components. See Hexagonal Architecture.

flowchart TD
    Header["AppHeader.vue<br/>Sparkles button, Mod+/"] --> PanelState["useAdvisorPanel<br/>module singleton"]
    PanelState --> Panel["AdvisorPanel.vue (mounted once in App.vue)"]
    Panel --> ChatC["useAdvisorChat(port)"]
    Panel --> CfgQ["useAdvisorConfigQuery<br/>enabled while open"]
    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) declares the streaming ask(request, signal) alongside the config methods, following the market-data precedent. RestAdvisorAdapter reads the stream by hand: the browser EventSource cannot POST and reconnects on its own, which would silently re-run (and re-bill) a finished LLM call. Every retry is an explicit user action. The adapter reports each transport failure to the errorBus exactly once; the consumer never re-toasts. Config calls use bffClient and validate with AdvisorConfigSchema / executionProfilesSchema.


  • useAdvisorChat holds turns and the thread id in memory only. It exposes send, abort, retry, and newConversation. Turn outcomes are the wire outcomes plus two local ones, transport-lost and user-aborted.
  • isRetryableOutcome permits retry after a network error, stream end, HTTP 5xx or 429, a failed run whose cause is temporary (rate-limited, provider-unavailable, network, unknown), or a user stop; not after a malformed frame, a missing body, an HTTP 4xx other than 429, a rejected key, or an unknown model.
  • Server state (config, profiles) goes through Pinia Colada (useAdvisorQueries.ts, useAdvisorMutations.ts); there is no global Pinia store. The config query is enabled only while the panel is open.
  • useAdvisorPanel is a module singleton because the header button and the panel live in unrelated subtrees.

  • A right-hand sheet opened from the header button or Mod + / (Cmd on macOS, Ctrl elsewhere). Escape closes it and focus returns to the trigger.
  • Answers stream as Markdown (markdown-it): raw HTML off, images off, no auto-linking, only in-app paths stay clickable; inline code renders font-mono tabular-nums.
  • Each tool call shows an activity row (running, finished, or failed).
  • The header shows the active model id and a local / cloud badge from classifyExecutionProfile, the same rule as the backend. The active model is the one that answered the latest turn, else the first chain entry.
  • Below an answer: a “Figures incomplete” badge, the disclaimer footer (only when disclaimer is true, never inline), and a muted stepsUsed / maxSteps footer once the run is done.
  • A refused run shows an “Answer withheld” alert with one reformulation chip. A failed run shows copy per code; NO_MODEL_AVAILABLE and VAULT_LOCKED link to Settings, and ALL_PROVIDERS_FAILED picks copy per cause.kind naming the provider and model (auth-rejected points at the API key, model-not-found at the model id).
  • Enter sends, Shift+Enter inserts a newline, IME composition is respected; Stop aborts immediately; a second send during a run is ignored. Autoscroll pauses while the user scrolls up (useStickToBottom). Reduced-motion disables the slide and caret animation.
  • AppToaster.vue offsets toasts below the sheet header while the panel is open so they do not cover the close button.
  • The empty state offers suggested questions in two groups, “Taxes” and “Your portfolio”.

  • Model-chain editor: add, remove, reorder; provider select, model id, and a contextWindow input only for a local ollama entry; derived local/cloud and profile labels; a credential flag for keyed providers; an empty chain cannot be saved.
  • Execution-profile editor: numeric limits with the schema ceilings enforced client-side, metered per-tool budgets editable, local budgets shown read-only as derived from the declared window, reset to defaults. Form state is text, so a half-typed number is never coerced early.
  • Provider API keys are entered under Settings → Vault, in the “AI Providers” group.

  • i18n: every string uses useI18n(); keys live under advisor.*, settings.advisor.*, and vault.category.* in en.ts and es.ts. The interpolate helper fills {name} placeholders in one pass so values containing $& or {other} stay literal.
  • Design system: only DESIGN.md tokens are used; designTokens.spec.ts scans the advisor components for invented classes. Profit and loss colours never colour prose. See Institutional Light Design.