Skip to content

Angular Library

The chat UI is a standalone Angular component, <ast-assistant-chat>, published on npm as @aeontronix/aeon-assistant. It works with any backend that implements the REST API, such as the Spring Boot integration.

Installation

Install the library along with the markdown renderer it relies on:

npm install @aeontronix/aeon-assistant ngx-markdown marked

Peer dependencies:

Package Version
@angular/core, @angular/common, @angular/forms ^21.2.0 or ^22.0.0
ngx-markdown ^21.3.0 or ^22.0.0
marked ^17.0.0 or ^18.0.0

The component uses FormsModule for its prompt box. Angular CLI projects include all three Angular packages by default.

No extra setup

  • Styles are self-contained. The component ships its own CSS, so you don't need Tailwind or a global stylesheet.
  • Markdown is set up for you. The component registers provideMarkdown() itself, so you don't have to add it to your application providers.

Alpha, beta and snapshot versions

Stable releases are published on npmjs.com. Alpha and beta releases (for example 1.0.0-beta2), and the snapshots built from the default branch (for example 1.0.0-snapshot.2, dist-tag snapshot), are only published to the package registry of the aeontronix GitLab group. The registry is public, so you don't need a token.

1. Add the registry for the @aeontronix scope. Create an .npmrc file next to your package.json:

.npmrc
@aeontronix:registry=https://gitlab.com/api/v4/groups/5527027/-/packages/npm/

5527027 is the id of the aeontronix group. You can also write the same line with npm config set @aeontronix:registry https://gitlab.com/api/v4/groups/5527027/-/packages/npm/ --location project.

2. Install the version you want:

npm install @aeontronix/aeon-assistant@1.0.0-beta2
npm install @aeontronix/aeon-assistant@snapshot

Scoped registry

While the line is in .npmrc, npm downloads every @aeontronix package from the GitLab registry, and other packages from npmjs.com as before. Remove the line to install stable releases from npmjs.com again. Keep the exact pre-release version in package.json, because npm only installs pre-releases when you ask for them explicitly.

Quick start

1. Point the library at your backend. Provide ASSISTANT_CONFIG in your application config:

app.config.ts
import { ApplicationConfig } from '@angular/core';
import { ASSISTANT_CONFIG, AssistantConfig } from '@aeontronix/aeon-assistant';

export const appConfig: ApplicationConfig = {
  providers: [
    {
      provide: ASSISTANT_CONFIG,
      useValue: {
        apiUrl: 'http://localhost:8080/assistant/chat',
      } satisfies AssistantConfig,
    },
  ],
};

2. Import the standalone component wherever you want to show the chat:

app.ts
import { Component } from '@angular/core';
import { AssistantChat } from '@aeontronix/aeon-assistant';

@Component({
  selector: 'app-root',
  imports: [AssistantChat],
  templateUrl: './app.html',
})
export class App {}

3. Add it to the template. The chat fills its host element, so give the host a height:

app.html
<ast-assistant-chat style="height: 100vh"/>

That's all you need. If you don't set an agent, the chat uses the first agent the backend returns when you send the first message.

Sizing the chat

The host element is a flex column (display: flex) that stretches the message list between the header and the prompt box. Don't override its display. If the host has no fixed height (for example 100vh, or a sized parent with height: 100%), the chat grows with its content instead of scrolling.

Configuration

ASSISTANT_CONFIG is optional and holds the defaults for every <ast-assistant-chat> in the application. It is an AssistantConfig object:

Field Type Description
apiUrl string URL of the chat endpoint. All other endpoints are derived from it (see Backend URLs).
agentId string Agent selected at startup.
sessionId string Declared on the interface, but not read by the component. Use the sessionId input instead.
title string Header title shown before the backend sends one.
headers Record<string, string> Extra HTTP headers sent with every request, for example Authorization.

Each setting is resolved in this order:

  1. The component input, for example <ast-assistant-chat apiUrl="/other/chat" />.
  2. The matching field of ASSISTANT_CONFIG.
  3. The built-in default (see Inputs).

headers has no input. It is read from the config once, when the component is created. You can change it later with setHeader() or setHeaders() (see Programmatic control).

Inputs

Input Type Default Description
apiUrl string config apiUrl, else /assistant/chat Chat endpoint URL. Relative URLs are resolved against the current origin.
agentId string config agentId Agent to talk to. Changing it resets the selection.
sessionId string none Existing session to resume. Without it, the backend starts a new session.
loadHistory boolean true Load the conversation of sessionId from the backend when it is set.
title string config title, else new prompt Header title shown until the backend sends a title for the conversation.
subtitle string none Secondary line shown under the title.
placeholder string Type your task Placeholder text of the prompt box.
size 'small', 'medium' or 'large' 'medium' Overall scale of the UI: header, bubbles, controls and fonts.
autoScroll boolean true Scroll to the latest message when messages change.
showClearButton boolean true Show the button that clears the conversation. It only appears once there are messages.
showAgentSelector boolean false Load the agents from the backend and show a dropdown to switch between them, if there are any.
showStatus boolean true Show the status indicator (idle, loading, streaming, error).
showTimestamp boolean true Show the time of each message.
renderMarkdown boolean true Render assistant and system messages as markdown. User input is always plain text.
showThinking boolean true Show the reasoning the agent reports while it answers.
showTools boolean true Show the tools the agent runs while it answers.
promptSuggestions array of string or PromptSuggestion ['What can you help me with?'] Chips shown while the chat is empty. Clicking one sends its prompt. Pass [] to hide them.

A PromptSuggestion is { label: string; prompt?: string }. The chip shows label and sends prompt, or label if prompt is missing. A plain string is used as both.

<ast-assistant-chat
        style="height: 600px"
        agentId="support"
        title="Support"
        subtitle="Ask anything about your account"
        size="small"
        [showAgentSelector]="true"
        [promptSuggestions]="['Reset my password', { label: 'Billing', prompt: 'Show my last invoice' }]"
/>

Outputs

Output Payload Emitted when
messageSent ChatMessage The user sends a message. A message with both an image and text emits twice, once for each.
messageReceived ChatMessage The agent's answer is complete. Not emitted if the request fails or is aborted.
chatCleared void The conversation is cleared, from the clear button or clearMessages().
errorOccurred string A request fails. The payload is the error message, taken from the response body when there is one.
<ast-assistant-chat
  (messageReceived)="onAnswer($event)"
  (errorOccurred)="onError($event)"
/>

Programmatic control

Get the component instance with viewChild to call its public methods:

import {Component, viewChild} from '@angular/core';
import {AssistantChat} from '@aeontronix/aeon-assistant';

@Component({
    selector: 'app-support',
    imports: [AssistantChat],
    template: `<ast-assistant-chat #chat style="height: 100%" />`,
})
export class Support {
    readonly chat = viewChild.required(AssistantChat);

    reset(): void {
        this.chat().clearMessages();
    }
}
Method Description
setHeader(key, value) Adds or replaces one HTTP header sent with every request.
setHeaders(headers) Replaces all custom headers, including the ones from ASSISTANT_CONFIG.
setAgentId(agentId) Selects another agent. The next message starts a new session, because a session belongs to a single agent.
setAgents(agents) Sets the list of agents shown in the selector, instead of loading it from the backend.
loadAgents() Loads the agents from the backend and selects the first one if none is selected yet.
setTitle(title) Changes the header title. The backend may still replace it with the title it generates.
clearMessages() Aborts a running request, clears the conversation, resets the title and emits chatCleared.
addMessage(message) Appends a ChatMessage to the conversation. Nothing is sent to the backend.
setMessages(messages) Replaces the whole conversation. Nothing is sent to the backend.
loadSessionHistory(sessionId, agentId?) Reloads the history of the session bound to the sessionId input. The result is dropped if sessionId doesn't match the input, or a message is being sent.

The component also exposes its state as read-only signals:

Signal Description
messages() The conversation shown.
status() 'idle', 'loading', 'streaming' or 'error'.
isBusy() Whether a request is running.
agents() The agents loaded from the backend or set with setAgents().
selectedAgentId() The agent messages are sent to.
activeSessionId() The session the conversation is on.
displayTitle() The title shown in the header.

Authentication

All requests go through fetch with the custom headers, so you can pass a token either way:

app.config.ts
{
  provide: ASSISTANT_CONFIG,
  useValue: {
    apiUrl: '/assistant/chat',
    headers: { Authorization: 'Bearer my-token' },
  } satisfies AssistantConfig,
}
export class Support {
  private readonly auth = inject(AuthService);
  readonly chat = viewChild.required(AssistantChat);

  constructor() {
    effect(() => {
      this.chat().setHeader('Authorization', `Bearer ${this.auth.token()}`);
    });
  }
}

Interceptors and cookies

Requests don't go through Angular's HttpClient, so HTTP interceptors don't apply and cookies follow the default fetch rules. Pass credentials as headers. If the backend is on another origin, it must allow your headers through CORS.

Agents, sessions and history

Agents

Every message goes to one agent:

  • If agentId (input or config) is set, that agent is used.
  • With [showAgentSelector]="true", the component loads the agents from the backend, selects the first one if none is set, and shows a dropdown in the header to switch between them.
  • If no agent is selected when the user sends a message, the component loads the agents and uses the first one. If the backend returns none, the message fails with an error.

Sessions

The backend creates a session on the first message and returns its id. The component sends that id back with every later message, so the whole conversation stays on one session. Session ids are only unique per agent: switching to another agent starts a new session.

Resuming a conversation

Bind sessionId, together with agentId, to show an existing chat again:

<ast-assistant-chat agentId="support" [sessionId]="sessionId()"/>

With loadHistory left at true, the component loads the session's history from the backend and continues the conversation on that session. Set [loadHistory]="false" to keep sending on the session without showing its earlier messages. Changing sessionId resets the active session, and clearMessages() returns to it.

What is sent to the backend

Agents that keep memory (hasMemory: true in the agent list) only receive the new message. For other agents, the component sends the whole visible conversation with each request. Tool runs and thinking are never sent back.

Using AssistantService directly

AssistantService is provided in root and handles all communication with the backend. Use it to build your own UI, or to manage sessions outside the chat component. Every method takes the chat URL (apiUrl) and optional headers:

import { inject } from '@angular/core';
import { AssistantService } from '@aeontronix/aeon-assistant';

const service = inject(AssistantService);
const apiUrl = '/assistant/chat';
const headers = { Authorization: 'Bearer my-token' };

const agents = await service.loadAgents(apiUrl, headers);
const history = await service.loadHistory(apiUrl, 'support', sessionId, headers);
await service.renameSession(apiUrl, 'support', sessionId, 'Invoice question', headers);
await service.deleteSession(apiUrl, 'support', sessionId, headers);
Method Returns Description
loadAgents(apiUrl, headers?) Promise<AgentInfo[]> The public agents, or [] on failure.
sendMessage(content, options) Promise<ChatMessage> or null Sends a message and streams the answer through callbacks. Resolves to the answer, or null on error or abort.
loadHistory(apiUrl, agentId, sessionId, headers?, from?) Promise<ChatMessage[]> The messages of a session, optionally only those from a given time on. [] on failure.
answerQuestion(apiUrl, agentId, sessionId, questionId, answers, headers?) Promise<boolean> Answers a question the agent is waiting on.
renameSession(apiUrl, agentId, sessionId, title, headers?) Promise<boolean> Renames a session. Only its owner can do this.
deleteSession(apiUrl, agentId, sessionId, headers?) Promise<boolean> Deletes a session and its history.
clearSessionHistory(apiUrl, agentId, headers?) Promise<boolean> Deletes every session of an agent.

The methods don't throw. Failures resolve to false, [] or null, and are logged to the console.

sendMessage requires apiUrl and agentId in its options. Everything else is optional: sessionId, imageUrl, headers, agents, the previous messages, an abort signal, and callbacks such as onMessagesUpdate, onSessionId, onTitle, onToolExecution, onThinking, onQuestion, onLoading, onStreaming and onError.

const controller = new AbortController();
const answer = await service.sendMessage('Hello!', {
  apiUrl,
  agentId: 'support',
  sessionId,
  headers,
  signal: controller.signal,
  onSessionId: (id) => (sessionId = id),
  onMessagesUpdate: (messages) => render(messages),
  onError: (error) => error && console.error(error),
});

Backend URLs

apiUrl is the chat URL. The base URL is apiUrl with its trailing /chat removed, and every other endpoint is built from it. A relative apiUrl is resolved against the page's origin. With the default /assistant/chat, the base is /assistant:

Method Path Used by
GET {base}/agents Agent selector, loadAgents()
POST {base}/agents/{agentId}/chat Sending a message, sendMessage()
GET {base}/agents/{agentId}/sessions/{sessionId}/events?from= Resuming a session, loadHistory()
POST {base}/agents/{agentId}/sessions/{sessionId}/questions/{questionId} Answering a question card, answerQuestion()
PUT {base}/agents/{agentId}/sessions/{sessionId}/title renameSession()
DELETE {base}/agents/{agentId}/sessions/{sessionId} deleteSession()
DELETE {base}/agents/{agentId}/sessions clearSessionHistory()

The component renders each event of the chat stream as follows:

Event What the component does
title Stores the session id and, on the first turn, shows the generated title.
message Adds the agent's answer to the conversation.
text Shows text the model wrote between tool calls, in place.
tool Shows a tool run, then updates it when it completes or fails (hidden with showTools).
thinking Shows the model's reasoning in a collapsible block (hidden with showThinking).
question Shows a question card. The user's answers are posted to the questions endpoint.

A backend that answers with plain text or JSON instead of an event stream also works: the response body is shown as the answer.

See the REST API Specs for the full contract.