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
isStreamingprop 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
trackin @for loops - Virtual scroll for long conversations
- Lazy load conversation history
Accessibility
- Use
aria-livefor 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: