Documentation

Building a Chat App

Step-by-step guide to building a complete AI chat application with Angular AI Kit.

What We'll Build

Features of the complete chat application

By the end of this guide, you'll have a chat app with:

  • Message display - User and AI messages with proper styling
  • Streaming responses - Real-time text streaming with typing indicator
  • Message actions - Copy, edit, regenerate, and feedback
  • Conversation management - Multiple conversations with sidebar
  • Prompt suggestions - Empty state with starter prompts

Step 1: Basic Chat Container

Create the foundation with message display and input

Start by creating a basic chat component that displays messages and accepts user input:

1// chat.component.ts2import { Component, signal } from '@angular/core';3import {4  AiResponseComponent,5  UserMessageComponent,6  ChatInputComponent,7  TypingIndicatorComponent,8} from '@angular-ai-kit/core';9 10interface Message {11  id: string;12  role: 'user' | 'assistant';13  content: string;14  timestamp: Date;15}16 17@Component({18  selector: 'app-chat',19  imports: [20    AiResponseComponent,21    UserMessageComponent,22    ChatInputComponent,23    TypingIndicatorComponent,24  ],25  template: `26    <div class="flex h-screen flex-col">27      <!-- Messages area -->28      <div class="flex-1 overflow-y-auto p-4 space-y-4">29        @for (message of messages(); track message.id) {30          @if (message.role === 'user') {31            <ai-user-message [message]="message" />32          } @else {33            <ai-response [content]="message.content" />34          }35        }36 37        @if (isLoading()) {38          <ai-typing-indicator />39        }40      </div>41 42      <!-- Input area -->43      <div class="border-t border-border p-4">44        <ai-chat-input45          [disabled]="isLoading()"46          (messageSend)="handleSubmit($event)"47        />48      </div>49    </div>50  `,51})52export class ChatComponent {53  messages = signal<Message[]>([]);54  isLoading = signal(false);55 56  handleSubmit(content: string) {57    // Add user message58    const userMessage: Message = {59      id: crypto.randomUUID(),60      role: 'user',61      content,62      timestamp: new Date(),63    };64    this.messages.update(msgs => [...msgs, userMessage]);65 66    // Simulate AI response (replace with real API)67    this.isLoading.set(true);68    setTimeout(() => {69      const aiMessage: Message = {70        id: crypto.randomUUID(),71        role: 'assistant',72        content: 'This is a simulated response.',73        timestamp: new Date(),74      };75      this.messages.update(msgs => [...msgs, aiMessage]);76      this.isLoading.set(false);77    }, 1500);78  }79}

Key Points

  • Use signal() for reactive state management
  • Track messages by ID for efficient rendering
  • Show typing indicator while loading
  • Disable input during AI response

Step 2: Add Streaming Support

Display AI responses as they stream in

Add a service to handle streaming responses and update the UI in real-time:

1// chat.service.ts2import { Injectable, signal } from '@angular/core';3 4@Injectable({ providedIn: 'root' })5export class ChatService {6  private streamingContent = signal('');7  readonly streamingContent$ = this.streamingContent.asReadonly();8 9  async streamResponse(userMessage: string): Promise<string> {10    // Simulate streaming (replace with real API)11    const fullResponse = `Here's a response to: "${userMessage}"`;12    let currentContent = '';13 14    for (const char of fullResponse) {15      await new Promise(resolve => setTimeout(resolve, 20));16      currentContent += char;17      this.streamingContent.set(currentContent);18    }19 20    this.streamingContent.set('');21    return fullResponse;22  }23}24 25// chat.component.ts (updated)26@Component({27  // ... same imports28  template: `29    <div class="flex h-screen flex-col">30      <div class="flex-1 overflow-y-auto p-4 space-y-4">31        @for (message of messages(); track message.id) {32          @if (message.role === 'user') {33            <ai-user-message [message]="message" />34          } @else {35            <ai-response [content]="message.content" />36          }37        }38 39        <!-- Streaming response -->40        @if (streamingContent()) {41          <ai-response42            [content]="streamingContent()"43            [isStreaming]="true"44          />45        }46 47        @if (isLoading() && !streamingContent()) {48          <ai-typing-indicator />49        }50      </div>51 52      <div class="border-t border-border p-4">53        <ai-chat-input54          [disabled]="isLoading()"55          (messageSend)="handleSubmit($event)"56        />57      </div>58    </div>59  `,60})61export class ChatComponent {62  private chatService = inject(ChatService);63 64  messages = signal<Message[]>([]);65  isLoading = signal(false);66  streamingContent = this.chatService.streamingContent$;67 68  async handleSubmit(content: string) {69    // Add user message70    const userMessage: Message = {71      id: crypto.randomUUID(),72      role: 'user',73      content,74      timestamp: new Date(),75    };76    this.messages.update(msgs => [...msgs, userMessage]);77 78    // Stream AI response79    this.isLoading.set(true);80    const response = await this.chatService.streamResponse(content);81 82    const aiMessage: Message = {83      id: crypto.randomUUID(),84      role: 'assistant',85      content: response,86      timestamp: new Date(),87    };88    this.messages.update(msgs => [...msgs, aiMessage]);89    this.isLoading.set(false);90  }91}

Streaming Tips

  • Use a separate signal for streaming content
  • Show the response as it arrives, character by character
  • Clear streaming content after completing
  • The isStreaming prop shows a cursor animation

Step 3: Add Message Actions

Enable copy, edit, regenerate, and feedback

Add interactive actions to messages for a better user experience:

1// chat.component.ts (with actions)2@Component({3  template: `4    <div class="flex h-screen flex-col">5      <div class="flex-1 overflow-y-auto p-4 space-y-4">6        @for (message of messages(); track message.id; let i = $index) {7          @if (message.role === 'user') {8            <ai-user-message9              [message]="message"10              (edit)="handleEdit(i, $event)"11              (copy)="handleCopy($event)"12            />13          } @else {14            <ai-response15              [content]="message.content"16              (copy)="handleCopy($event)"17              (regenerate)="handleRegenerate(i)"18            >19              <!-- Custom actions slot -->20              <ai-feedback-buttons21                slot="actions"22                (thumbsUp)="handleFeedback(message.id, 'up')"23                (thumbsDown)="handleFeedback(message.id, 'down')"24              />25            </ai-response>26          }27        }28      </div>29 30      <div class="border-t border-border p-4">31        <ai-chat-input32          [disabled]="isLoading()"33          (messageSend)="handleSubmit($event)"34        />35      </div>36    </div>37  `,38})39export class ChatComponent {40  // ... previous code41 42  handleCopy(content: string) {43    navigator.clipboard.writeText(content);44    // Show toast notification45  }46 47  handleEdit(index: number, newContent: string) {48    this.messages.update(msgs => {49      const updated = [...msgs];50      updated[index] = { ...updated[index], content: newContent };51      // Optionally regenerate responses after this message52      return updated.slice(0, index + 1);53    });54    // Trigger new AI response55    this.handleSubmit(newContent);56  }57 58  handleRegenerate(index: number) {59    const previousUserMessage = this.messages()60      .slice(0, index)61      .reverse()62      .find(m => m.role === 'user');63 64    if (previousUserMessage) {65      // Remove current AI response and regenerate66      this.messages.update(msgs => msgs.slice(0, index));67      this.handleSubmit(previousUserMessage.content);68    }69  }70 71  handleFeedback(messageId: string, type: 'up' | 'down') {72    console.log(`Feedback for ${messageId}: ${type}`);73    // Send to analytics/API74  }75}

Action Handlers

  • Copy: Use navigator.clipboard.writeText()
  • Edit: Update message and optionally regenerate
  • Regenerate: Find previous user message and resend
  • Feedback: Track thumbs up/down for analytics

Step 4: Conversation Management

Support multiple conversations with a sidebar

Create a service to manage multiple conversations:

1// conversation.service.ts2import { Injectable, signal, computed } from '@angular/core';3 4interface Conversation {5  id: string;6  title: string;7  messages: Message[];8  createdAt: Date;9  updatedAt: Date;10}11 12@Injectable({ providedIn: 'root' })13export class ConversationService {14  private conversations = signal<Conversation[]>([]);15  private activeId = signal<string | null>(null);16 17  readonly conversations$ = this.conversations.asReadonly();18  readonly activeConversation = computed(() => {19    const id = this.activeId();20    return this.conversations().find(c => c.id === id) ?? null;21  });22 23  createConversation(): string {24    const conversation: Conversation = {25      id: crypto.randomUUID(),26      title: 'New Chat',27      messages: [],28      createdAt: new Date(),29      updatedAt: new Date(),30    };31 32    this.conversations.update(convs => [conversation, ...convs]);33    this.activeId.set(conversation.id);34    return conversation.id;35  }36 37  selectConversation(id: string) {38    this.activeId.set(id);39  }40 41  addMessage(conversationId: string, message: Message) {42    this.conversations.update(convs =>43      convs.map(c => {44        if (c.id !== conversationId) return c;45        return {46          ...c,47          messages: [...c.messages, message],48          updatedAt: new Date(),49          // Auto-generate title from first user message50          title: c.messages.length === 0 && message.role === 'user'51            ? message.content.slice(0, 30) + '...'52            : c.title,53        };54      })55    );56  }57 58  deleteConversation(id: string) {59    this.conversations.update(convs => convs.filter(c => c.id !== id));60    if (this.activeId() === id) {61      const remaining = this.conversations();62      this.activeId.set(remaining[0]?.id ?? null);63    }64  }65}

Conversation Features

  • Create, select, and delete conversations
  • Auto-generate title from first message
  • Track active conversation with computed signal
  • Persist to localStorage (optional enhancement)

Step 5: Final Layout

Put it all together with sidebar and main area

Create the final layout with conversation sidebar and prompt suggestions:

1// app.component.ts (final layout)2@Component({3  selector: 'app-root',4  template: `5    <div class="flex h-screen">6      <!-- Sidebar -->7      <aside class="w-64 border-r border-border bg-card p-4">8        <button9          class="mb-4 w-full rounded-lg bg-primary px-4 py-2 text-primary-foreground"10          (click)="createNewChat()"11        >12          New Chat13        </button>14 15        <div class="space-y-2">16          @for (conv of conversations(); track conv.id) {17            <button18              class="w-full rounded-lg px-3 py-2 text-left hover:bg-accent"19              [class.bg-accent]="conv.id === activeId()"20              (click)="selectConversation(conv.id)"21            >22              {{ conv.title }}23            </button>24          }25        </div>26      </aside>27 28      <!-- Main chat area -->29      <main class="flex-1">30        @if (activeConversation(); as conversation) {31          <app-chat [conversation]="conversation" />32        } @else {33          <div class="flex h-full items-center justify-center">34            <ai-prompt-suggestions35              [suggestions]="suggestions"36              (selectPrompt)="startChat($event)"37            />38          </div>39        }40      </main>41    </div>42  `,43})44export class AppComponent {45  private convService = inject(ConversationService);46 47  conversations = this.convService.conversations$;48  activeConversation = this.convService.activeConversation;49  activeId = computed(() => this.activeConversation()?.id);50 51  suggestions = [52    { label: 'Write code', prompt: 'Help me write a function that...' },53    { label: 'Explain concept', prompt: 'Explain how ... works' },54    { label: 'Debug issue', prompt: 'I have a bug where...' },55  ];56 57  createNewChat() {58    this.convService.createConversation();59  }60 61  selectConversation(id: string) {62    this.convService.selectConversation(id);63  }64 65  startChat(prompt: string) {66    this.createNewChat();67    // Trigger message submission68  }69}

Styling Tips

CSS patterns for chat interfaces

Key CSS patterns for a polished chat UI:

1/* styles.css - Key styles for chat UI */2 3/* Message container with auto-scroll */4.messages-container {5  scroll-behavior: smooth;6}7 8/* Keep input fixed at bottom */9.chat-layout {10  display: flex;11  flex-direction: column;12  height: 100vh;13}14 15.chat-messages {16  flex: 1;17  overflow-y: auto;18  padding: 1rem;19}20 21.chat-input {22  flex-shrink: 0;23  border-top: 1px solid var(--border);24  padding: 1rem;25}

Auto-Scroll

Scroll to bottom when new messages arrive using scrollTop = scrollHeight

Fixed Input

Keep the input fixed at the bottom using flex layout with flex-shrink: 0

Best Practices

Tips for production-ready chat apps

Error Handling

  • Show error states gracefully
  • Allow retry on failed requests
  • Don't lose user input on errors

Performance

  • Use track in @for loops
  • Virtual scroll for long conversations
  • Lazy load conversation history

Accessibility

  • Use aria-live for new messages
  • Keyboard navigation support
  • Screen reader announcements

Persistence

  • Save to localStorage or IndexedDB
  • Sync with backend API
  • Handle offline gracefully

Next Steps

Now that you have a working chat app, explore more: