TanStack
UI

Angular Chat UI

Install @tanstack/ai-angular. Import the UI factory from @tanstack/ai-angular/ui. Call createChatHook({ options, ...components }) once at module scope.

This is not a React port. Widgets are standalone Angular components. Chat state from injectAppChat() is signals. Layout slots are component classes. Your layout template uses NgComponentOutlet to render Messages, Interrupts, Queue, and Input.

The factory returns injectAppChat, injectChatContext, and Chat. Call injectAppChat() in a component injection context. Bind the instance with <ai-chat [chat]="chat" />.

The factory needs a toolsComponents entry for every tool name in chatOptions. It also needs an interruptsComponents.generic entry for every interrupt id. generic.fallback is optional.

The server route matches the React page. The chat UI recipes show the same option groups. The code there is React. On Angular, each widget is a component class with input().

Client

ts
import { Component, input } from '@angular/core'
import { NgComponentOutlet } from '@angular/common'
import type { Type } from '@angular/core'
import { fetchServerSentEvents } from '@tanstack/ai-client'
import { createChatHook } from '@tanstack/ai-angular/ui'
import { toolDefinition } from '@tanstack/ai'
import { z } from 'zod'

const getWeather = toolDefinition({
  name: 'getWeather',
  description: 'Look up weather',
  inputSchema: z.object({ city: z.string() }),
  outputSchema: z.object({ temperature: z.number() }),
}).client()

const chatOptions = {
  connection: fetchServerSentEvents('/api/chat'),
  tools: [getWeather],
}

@Component({
  selector: 'app-chat-layout',
  imports: [NgComponentOutlet],
  template: `
    <ng-container [ngComponentOutlet]="Messages()" />
    @if (Input(); as inputCmp) {
      <ng-container [ngComponentOutlet]="inputCmp" />
    }
  `,
})
export class ChatLayout {
  Messages = input.required<Type<unknown>>()
  Interrupts = input.required<Type<unknown>>()
  Queue = input.required<Type<unknown>>()
  Input = input<Type<unknown>>()
}

@Component({
  selector: 'app-chat-message',
  imports: [NgComponentOutlet],
  template: `
    <article>
      <ng-container
        [ngComponentOutlet]="Parts()"
        [ngComponentOutletInputs]="{ message: message() }"
      />
    </article>
  `,
})
export class ChatMessage {
  message = input.required<unknown>()
  Parts = input.required<Type<unknown>>()
}

@Component({
  selector: 'app-weather-tool',
  template: `<strong>{{ part().input?.city }}</strong>`,
})
export class WeatherTool {
  part = input.required<{ input?: { city?: string } }>()
  result = input<unknown>()
  interrupt = input<unknown>()
}

const { injectAppChat, Chat } = createChatHook({
  options: chatOptions,
  components: { layout: ChatLayout, message: ChatMessage },
  partsComponents: { fallback: ChatMessage },
  toolsComponents: { getWeather: WeatherTool },
})

@Component({
  selector: 'app-chat-screen',
  imports: [Chat],
  template: `<ai-chat [chat]="chat" />`,
})
export class ChatScreen {
  chat = injectAppChat()
}