AbsoluteJS

HTML / Vanilla AI

The createAIStream function from @absolutejs/ai/client is the framework-agnostic AI client. It works with plain HTML, HTMX, or any environment without a framework: same API surface as the React, Vue, Svelte, and Angular bindings.

The framework hooks (useAIStream, createAIStream, AIStreamService) are thin wrappers around this same client. The only addition here is a subscribe callback so you can react to state changes without framework reactivity.

#createAIStream

Call createAIStream with the WebSocket path. Use subscribe() to re-render when state changes, then read messages, isStreaming, and error as getter properties.

TS
import { createAIStream } from '@absolutejs/ai/client';

const ai = createAIStream('/chat');

// Subscribe to state changes
ai.subscribe(() => {
  renderMessages(ai.messages);
  toggleSpinner(ai.isStreaming);
  showError(ai.error);
});

// Send a message
document.querySelector('#send')!.addEventListener('click', () => {
  const input = document.querySelector<HTMLInputElement>('#input')!;
  ai.send(input.value);
  input.value = '';
});

// Cancel streaming
document.querySelector('#cancel')!.addEventListener('click', () => {
  ai.cancel();
});

#Return Type

TS
// createAIStream return type
{
  send: (content: string, attachments?: AIAttachment[]) => void;
  cancel: () => void;
  branch: (messageId: string, content: string) => void;
  destroy: () => void;

  // Subscribe to state changes : returns an unsubscribe function
  subscribe: (callback: () => void) => () => void;

  get messages(): AIMessage[];    // Current conversation messages
  get isStreaming(): boolean;     // Whether a response is streaming
  get error(): string | null;    // Error message or null
}

#Rendering Messages

Build your own render loop inside the subscribe callback. Each message has content, thinking, and toolCalls you can display however you want.

TS
import { createAIStream } from '@absolutejs/ai/client';
import type { AIMessage } from '@absolutejs/absolute';

const ai = createAIStream('/chat');
const container = document.querySelector<HTMLElement>('#messages')!;

const renderMessage = (msg: AIMessage) => {
  const div = document.createElement('div');
  div.className = `message ${msg.role}`;
  div.textContent = msg.content;

  if (msg.thinking) {
    const details = document.createElement('details');
    details.innerHTML = `<summary>Thinking</summary><p>${msg.thinking}</p>`;
    div.appendChild(details);
  }

  if (msg.toolCalls) {
    for (const tool of msg.toolCalls) {
      const details = document.createElement('details');
      details.innerHTML = `<summary>Tool: ${tool.name}</summary><pre>${tool.result ?? ''}</pre>`;
      div.appendChild(details);
    }
  }

  return div;
};

ai.subscribe(() => {
  container.replaceChildren(...ai.messages.map(renderMessage));
});

#File Attachments

Send images and PDFs alongside messages by converting files to base64 and passing them as the second argument to send().

TS
import { createAIStream } from '@absolutejs/ai/client';
import type { AIAttachment } from '@absolutejs/absolute';

const ai = createAIStream('/chat');

const sendWithFile = async (text: string, file: File) => {
  const buffer = await file.arrayBuffer();
  const base64 = btoa(String.fromCharCode(...new Uint8Array(buffer)));

  const attachment: AIAttachment = {
    data: base64,
    media_type: file.type as AIAttachment['media_type'],
    name: file.name,
  };

  ai.send(text, [attachment]);
};