Skip to main content

Build Your First Chat Application

In this tutorial, you’ll learn how to build a production-ready chat application using Azure AI Foundry. We’ll create a web application with conversation memory, user authentication, and safety controls.

What you’ll build

By completing this tutorial, you will have created:
  • A multi-user chat application with persistent conversations
  • User authentication and session management
  • Conversation memory that maintains context across messages
  • Built-in content filtering and safety controls
  • A responsive web interface that works on desktop and mobile

Prerequisites

Before starting, ensure you have:
  • Completed the Getting Started tutorial
  • An Azure AI Foundry project with a deployed chat model
  • Node.js 18+ installed on your machine
  • Basic familiarity with JavaScript and web development

Learning objectives

Through this hands-on tutorial, you will learn:
  • How to build stateful chat applications with conversation memory
  • Implementing user authentication in AI applications
  • Designing responsive chat interfaces
  • Adding safety controls and content moderation
  • Deploying your application to production

Step 1: Set up your development environment

Let’s start by creating a new project and installing the required dependencies.
# Create a new project directory
mkdir foundry-chat-app
cd foundry-chat-app

# Initialize a new Node.js project
npm init -y

# Install required dependencies
npm install express socket.io openai uuid bcryptjs jsonwebtoken dotenv
npm install --save-dev nodemon
Create your project structure:
foundry-chat-app/
├── server.js
├── public/
│   ├── index.html
│   ├── chat.html
│   ├── style.css
│   └── script.js
├── .env
└── package.json

Step 2: Configure your environment

Create a .env file with your Azure AI Foundry credentials:
# Azure AI Foundry Configuration
AZURE_ENDPOINT=https://your-endpoint.inference.ml.azure.com
AZURE_API_KEY=your-api-key-here
AZURE_API_VERSION=2024-02-01

# Application Configuration
JWT_SECRET=your-jwt-secret-here
PORT=3000
Never commit your .env file to version control. Add it to your .gitignore file.

Step 3: Build the server foundation

Create server.js with the basic Express server setup:
const express = require('express');
const http = require('http');
const socketIo = require('socket.io');
const { AzureOpenAI } = require('openai');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');
const { v4: uuidv4 } = require('uuid');
require('dotenv').config();

const app = express();
const server = http.createServer(app);
const io = socketIo(server);

// Initialize Azure OpenAI client
const openai = new AzureOpenAI({
    apiKey: process.env.AZURE_API_KEY,
    apiVersion: process.env.AZURE_API_VERSION,
    endpoint: process.env.AZURE_ENDPOINT
});

// Middleware
app.use(express.json());
app.use(express.static('public'));

// In-memory storage (use a real database in production)
const users = new Map();
const conversations = new Map();

// User authentication
const hashPassword = async (password) => {
    return await bcrypt.hash(password, 10);
};

const verifyPassword = async (password, hash) => {
    return await bcrypt.compare(password, hash);
};

const generateToken = (userId) => {
    return jwt.sign({ userId }, process.env.JWT_SECRET, { expiresIn: '24h' });
};

const verifyToken = (token) => {
    try {
        return jwt.verify(token, process.env.JWT_SECRET);
    } catch (error) {
        return null;
    }
};

// Routes
app.post('/api/register', async (req, res) => {
    const { username, password } = req.body;
    
    if (users.has(username)) {
        return res.status(400).json({ error: 'Username already exists' });
    }
    
    const hashedPassword = await hashPassword(password);
    const userId = uuidv4();
    
    users.set(username, {
        id: userId,
        username,
        password: hashedPassword,
        createdAt: new Date()
    });
    
    const token = generateToken(userId);
    res.json({ token, userId, username });
});

app.post('/api/login', async (req, res) => {
    const { username, password } = req.body;
    
    const user = users.get(username);
    if (!user || !(await verifyPassword(password, user.password))) {
        return res.status(401).json({ error: 'Invalid credentials' });
    }
    
    const token = generateToken(user.id);
    res.json({ token, userId: user.id, username: user.username });
});

// Start server
const PORT = process.env.PORT || 3000;
server.listen(PORT, () => {
    console.log(`Server running on port ${PORT}`);
});
Notice how we’ve set up:
  • Authentication with JWT tokens
  • In-memory storage for users and conversations
  • Azure OpenAI client initialization
  • Basic Express server structure

Step 4: Implement conversation management

Add conversation management to your server:
// Add this after the authentication functions

class ConversationManager {
    constructor() {
        this.conversations = new Map();
    }
    
    createConversation(userId) {
        const conversationId = uuidv4();
        const conversation = {
            id: conversationId,
            userId,
            messages: [],
            createdAt: new Date(),
            updatedAt: new Date()
        };
        
        this.conversations.set(conversationId, conversation);
        return conversationId;
    }
    
    getConversation(conversationId) {
        return this.conversations.get(conversationId);
    }
    
    getUserConversations(userId) {
        return Array.from(this.conversations.values())
            .filter(conv => conv.userId === userId)
            .sort((a, b) => b.updatedAt - a.updatedAt);
    }
    
    addMessage(conversationId, message) {
        const conversation = this.conversations.get(conversationId);
        if (!conversation) return false;
        
        conversation.messages.push({
            id: uuidv4(),
            ...message,
            timestamp: new Date()
        });
        conversation.updatedAt = new Date();
        return true;
    }
    
    getMessages(conversationId, limit = 50) {
        const conversation = this.conversations.get(conversationId);
        if (!conversation) return [];
        
        return conversation.messages
            .slice(-limit)
            .map(msg => ({
                id: msg.id,
                role: msg.role,
                content: msg.content,
                timestamp: msg.timestamp
            }));
    }
}

const conversationManager = new ConversationManager();

// Add conversation routes
app.get('/api/conversations', (req, res) => {
    const authHeader = req.headers.authorization;
    if (!authHeader) {
        return res.status(401).json({ error: 'No token provided' });
    }
    
    const token = authHeader.split(' ')[1];
    const decoded = verifyToken(token);
    if (!decoded) {
        return res.status(401).json({ error: 'Invalid token' });
    }
    
    const conversations = conversationManager.getUserConversations(decoded.userId);
    res.json(conversations.map(conv => ({
        id: conv.id,
        createdAt: conv.createdAt,
        updatedAt: conv.updatedAt,
        messageCount: conv.messages.length,
        lastMessage: conv.messages[conv.messages.length - 1]?.content?.substring(0, 100) || ''
    })));
});

app.post('/api/conversations', (req, res) => {
    const authHeader = req.headers.authorization;
    const token = authHeader?.split(' ')[1];
    const decoded = verifyToken(token);
    
    if (!decoded) {
        return res.status(401).json({ error: 'Invalid token' });
    }
    
    const conversationId = conversationManager.createConversation(decoded.userId);
    res.json({ conversationId });
});

Step 5: Add real-time chat functionality

Now let’s add Socket.IO for real-time messaging:
// Add this after the conversation routes

// Socket.IO authentication middleware
io.use((socket, next) => {
    const token = socket.handshake.auth.token;
    const decoded = verifyToken(token);
    
    if (!decoded) {
        return next(new Error('Authentication error'));
    }
    
    socket.userId = decoded.userId;
    next();
});

// Socket.IO connection handling
io.on('connection', (socket) => {
    console.log(`User ${socket.userId} connected`);
    
    socket.on('join-conversation', (conversationId) => {
        // Verify user owns this conversation
        const conversation = conversationManager.getConversation(conversationId);
        if (!conversation || conversation.userId !== socket.userId) {
            socket.emit('error', 'Unauthorized');
            return;
        }
        
        socket.join(conversationId);
        
        // Send conversation history
        const messages = conversationManager.getMessages(conversationId);
        socket.emit('conversation-history', messages);
    });
    
    socket.on('send-message', async (data) => {
        const { conversationId, message } = data;
        
        // Verify conversation ownership
        const conversation = conversationManager.getConversation(conversationId);
        if (!conversation || conversation.userId !== socket.userId) {
            socket.emit('error', 'Unauthorized');
            return;
        }
        
        try {
            // Add user message to conversation
            const userMessage = {
                role: 'user',
                content: message
            };
            conversationManager.addMessage(conversationId, userMessage);
            
            // Emit user message to the conversation
            socket.to(conversationId).emit('new-message', {
                id: uuidv4(),
                role: 'user',
                content: message,
                timestamp: new Date()
            });
            
            // Get conversation history for AI context
            const messages = conversationManager.getMessages(conversationId);
            
            // Call Azure OpenAI
            const response = await openai.chat.completions.create({
                messages: messages.map(msg => ({
                    role: msg.role,
                    content: msg.content
                })),
                max_tokens: 500,
                temperature: 0.7,
                stream: true
            });
            
            let assistantMessage = '';
            
            // Stream the response
            for await (const chunk of response) {
                const content = chunk.choices[0]?.delta?.content || '';
                if (content) {
                    assistantMessage += content;
                    socket.emit('message-chunk', {
                        conversationId,
                        content: content
                    });
                }
            }
            
            // Add complete assistant message to conversation
            const completeAssistantMessage = {
                role: 'assistant',
                content: assistantMessage
            };
            conversationManager.addMessage(conversationId, completeAssistantMessage);
            
            // Emit complete message
            socket.emit('message-complete', {
                id: uuidv4(),
                role: 'assistant',
                content: assistantMessage,
                timestamp: new Date()
            });
            
        } catch (error) {
            console.error('Error generating response:', error);
            socket.emit('error', 'Failed to generate response');
        }
    });
    
    socket.on('disconnect', () => {
        console.log(`User ${socket.userId} disconnected`);
    });
});
Notice how we’ve implemented:
  • Real-time message streaming
  • Conversation history management
  • User authorization for conversations
  • Error handling for AI generation

Step 6: Create the frontend

Create public/index.html for the login/register page:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Foundry Chat - Login</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <div class="container">
        <div class="auth-card">
            <h1>Foundry Chat</h1>
            <div class="auth-tabs">
                <button class="tab-button active" data-tab="login">Login</button>
                <button class="tab-button" data-tab="register">Register</button>
            </div>
            
            <form id="login-form" class="auth-form active">
                <input type="text" id="login-username" placeholder="Username" required>
                <input type="password" id="login-password" placeholder="Password" required>
                <button type="submit">Login</button>
            </form>
            
            <form id="register-form" class="auth-form">
                <input type="text" id="register-username" placeholder="Username" required>
                <input type="password" id="register-password" placeholder="Password" required>
                <button type="submit">Register</button>
            </form>
            
            <div id="auth-error" class="error"></div>
        </div>
    </div>
    
    <script src="auth.js"></script>
</body>
</html>
Create public/chat.html for the main chat interface:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Foundry Chat</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <div class="chat-container">
        <div class="sidebar">
            <div class="sidebar-header">
                <h2>Conversations</h2>
                <button id="new-conversation">+</button>
            </div>
            <div class="conversations-list" id="conversations-list">
                <!-- Conversations will be loaded here -->
            </div>
            <div class="user-info">
                <span id="username"></span>
                <button id="logout">Logout</button>
            </div>
        </div>
        
        <div class="chat-area">
            <div class="chat-header">
                <h3 id="conversation-title">Select a conversation</h3>
            </div>
            <div class="messages-container" id="messages-container">
                <!-- Messages will appear here -->
            </div>
            <div class="input-area">
                <input type="text" id="message-input" placeholder="Type your message..." disabled>
                <button id="send-button" disabled>Send</button>
            </div>
        </div>
    </div>
    
    <script src="/socket.io/socket.io.js"></script>
    <script src="chat.js"></script>
</body>
</html>

Step 7: Style your application

Create public/style.css:
* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
    background-color: #f5f5f5;
    height: 100vh;
}

/* Authentication Styles */
.container {
    display: flex;
    justify-content: center;
    align-items: center;
    height: 100vh;
}

.auth-card {
    background: white;
    padding: 2rem;
    border-radius: 12px;
    box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
    width: 100%;
    max-width: 400px;
}

.auth-card h1 {
    text-align: center;
    margin-bottom: 2rem;
    color: #2563eb;
}

.auth-tabs {
    display: flex;
    margin-bottom: 1.5rem;
}

.tab-button {
    flex: 1;
    padding: 0.75rem;
    border: none;
    background: #f3f4f6;
    cursor: pointer;
    border-radius: 6px 6px 0 0;
}

.tab-button.active {
    background: #2563eb;
    color: white;
}

.auth-form {
    display: none;
}

.auth-form.active {
    display: block;
}

.auth-form input {
    width: 100%;
    padding: 0.75rem;
    margin-bottom: 1rem;
    border: 1px solid #d1d5db;
    border-radius: 6px;
    font-size: 1rem;
}

.auth-form button {
    width: 100%;
    padding: 0.75rem;
    background: #2563eb;
    color: white;
    border: none;
    border-radius: 6px;
    font-size: 1rem;
    cursor: pointer;
}

.auth-form button:hover {
    background: #1d4ed8;
}

.error {
    color: #ef4444;
    text-align: center;
    margin-top: 1rem;
}

/* Chat Interface Styles */
.chat-container {
    display: flex;
    height: 100vh;
}

.sidebar {
    width: 300px;
    background: white;
    border-right: 1px solid #e5e7eb;
    display: flex;
    flex-direction: column;
}

.sidebar-header {
    padding: 1rem;
    border-bottom: 1px solid #e5e7eb;
    display: flex;
    justify-content: space-between;
    align-items: center;
}

.sidebar-header h2 {
    font-size: 1.25rem;
    color: #374151;
}

#new-conversation {
    width: 32px;
    height: 32px;
    border-radius: 50%;
    border: none;
    background: #2563eb;
    color: white;
    cursor: pointer;
    font-size: 1.25rem;
}

.conversations-list {
    flex: 1;
    overflow-y: auto;
}

.conversation-item {
    padding: 1rem;
    border-bottom: 1px solid #f3f4f6;
    cursor: pointer;
    transition: background-color 0.2s;
}

.conversation-item:hover {
    background: #f9fafb;
}

.conversation-item.active {
    background: #eff6ff;
    border-right: 3px solid #2563eb;
}

.user-info {
    padding: 1rem;
    border-top: 1px solid #e5e7eb;
    display: flex;
    justify-content: space-between;
    align-items: center;
}

#logout {
    padding: 0.5rem 1rem;
    background: #ef4444;
    color: white;
    border: none;
    border-radius: 4px;
    cursor: pointer;
}

.chat-area {
    flex: 1;
    display: flex;
    flex-direction: column;
}

.chat-header {
    padding: 1rem;
    background: white;
    border-bottom: 1px solid #e5e7eb;
}

.messages-container {
    flex: 1;
    overflow-y: auto;
    padding: 1rem;
    background: #f9fafb;
}

.message {
    margin-bottom: 1rem;
    display: flex;
    align-items: flex-start;
}

.message.user {
    justify-content: flex-end;
}

.message-content {
    max-width: 70%;
    padding: 0.75rem 1rem;
    border-radius: 12px;
    line-height: 1.5;
}

.message.user .message-content {
    background: #2563eb;
    color: white;
}

.message.assistant .message-content {
    background: white;
    border: 1px solid #e5e7eb;
}

.input-area {
    padding: 1rem;
    background: white;
    border-top: 1px solid #e5e7eb;
    display: flex;
    gap: 0.5rem;
}

#message-input {
    flex: 1;
    padding: 0.75rem;
    border: 1px solid #d1d5db;
    border-radius: 6px;
    font-size: 1rem;
}

#send-button {
    padding: 0.75rem 1.5rem;
    background: #2563eb;
    color: white;
    border: none;
    border-radius: 6px;
    cursor: pointer;
}

#send-button:disabled {
    background: #9ca3af;
    cursor: not-allowed;
}

/* Responsive Design */
@media (max-width: 768px) {
    .chat-container {
        flex-direction: column;
    }
    
    .sidebar {
        width: 100%;
        height: auto;
        order: 2;
    }
    
    .chat-area {
        order: 1;
        height: 70vh;
    }
}

Step 8: Add JavaScript functionality

Create public/auth.js for authentication:
document.addEventListener('DOMContentLoaded', function() {
    const tabButtons = document.querySelectorAll('.tab-button');
    const authForms = document.querySelectorAll('.auth-form');
    const loginForm = document.getElementById('login-form');
    const registerForm = document.getElementById('register-form');
    const authError = document.getElementById('auth-error');
    
    // Tab switching
    tabButtons.forEach(button => {
        button.addEventListener('click', () => {
            const tab = button.dataset.tab;
            
            tabButtons.forEach(btn => btn.classList.remove('active'));
            authForms.forEach(form => form.classList.remove('active'));
            
            button.classList.add('active');
            document.getElementById(`${tab}-form`).classList.add('active');
            
            authError.textContent = '';
        });
    });
    
    // Login form
    loginForm.addEventListener('submit', async (e) => {
        e.preventDefault();
        
        const username = document.getElementById('login-username').value;
        const password = document.getElementById('login-password').value;
        
        try {
            const response = await fetch('/api/login', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json'
                },
                body: JSON.stringify({ username, password })
            });
            
            const data = await response.json();
            
            if (response.ok) {
                localStorage.setItem('token', data.token);
                localStorage.setItem('username', data.username);
                window.location.href = '/chat.html';
            } else {
                authError.textContent = data.error;
            }
        } catch (error) {
            authError.textContent = 'Login failed. Please try again.';
        }
    });
    
    // Register form
    registerForm.addEventListener('submit', async (e) => {
        e.preventDefault();
        
        const username = document.getElementById('register-username').value;
        const password = document.getElementById('register-password').value;
        
        if (password.length < 6) {
            authError.textContent = 'Password must be at least 6 characters long.';
            return;
        }
        
        try {
            const response = await fetch('/api/register', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json'
                },
                body: JSON.stringify({ username, password })
            });
            
            const data = await response.json();
            
            if (response.ok) {
                localStorage.setItem('token', data.token);
                localStorage.setItem('username', data.username);
                window.location.href = '/chat.html';
            } else {
                authError.textContent = data.error;
            }
        } catch (error) {
            authError.textContent = 'Registration failed. Please try again.';
        }
    });
});
Create public/chat.js for the chat functionality:
class ChatApp {
    constructor() {
        this.socket = null;
        this.currentConversationId = null;
        this.token = localStorage.getItem('token');
        this.username = localStorage.getItem('username');
        
        if (!this.token) {
            window.location.href = '/';
            return;
        }
        
        this.initializeElements();
        this.connectSocket();
        this.loadConversations();
        this.setupEventListeners();
    }
    
    initializeElements() {
        this.usernameDisplay = document.getElementById('username');
        this.conversationsList = document.getElementById('conversations-list');
        this.messagesContainer = document.getElementById('messages-container');
        this.messageInput = document.getElementById('message-input');
        this.sendButton = document.getElementById('send-button');
        this.newConversationButton = document.getElementById('new-conversation');
        this.logoutButton = document.getElementById('logout');
        this.conversationTitle = document.getElementById('conversation-title');
        
        this.usernameDisplay.textContent = this.username;
    }
    
    connectSocket() {
        this.socket = io({
            auth: {
                token: this.token
            }
        });
        
        this.socket.on('connect', () => {
            console.log('Connected to server');
        });
        
        this.socket.on('conversation-history', (messages) => {
            this.displayMessages(messages);
        });
        
        this.socket.on('new-message', (message) => {
            this.displayMessage(message);
        });
        
        this.socket.on('message-chunk', (data) => {
            this.appendToLastMessage(data.content);
        });
        
        this.socket.on('message-complete', (message) => {
            this.finalizeLastMessage(message);
        });
        
        this.socket.on('error', (error) => {
            console.error('Socket error:', error);
            alert('Connection error: ' + error);
        });
        
        this.socket.on('disconnect', () => {
            console.log('Disconnected from server');
        });
    }
    
    async loadConversations() {
        try {
            const response = await fetch('/api/conversations', {
                headers: {
                    'Authorization': `Bearer ${this.token}`
                }
            });
            
            if (response.ok) {
                const conversations = await response.json();
                this.displayConversations(conversations);
            } else {
                console.error('Failed to load conversations');
            }
        } catch (error) {
            console.error('Error loading conversations:', error);
        }
    }
    
    displayConversations(conversations) {
        this.conversationsList.innerHTML = '';
        
        conversations.forEach(conversation => {
            const conversationElement = document.createElement('div');
            conversationElement.className = 'conversation-item';
            conversationElement.dataset.conversationId = conversation.id;
            
            conversationElement.innerHTML = `
                <div class="conversation-preview">
                    <div class="conversation-time">${new Date(conversation.updatedAt).toLocaleDateString()}</div>
                    <div class="conversation-snippet">${conversation.lastMessage || 'New conversation'}</div>
                </div>
            `;
            
            conversationElement.addEventListener('click', () => {
                this.selectConversation(conversation.id);
            });
            
            this.conversationsList.appendChild(conversationElement);
        });
    }
    
    selectConversation(conversationId) {
        // Update UI
        document.querySelectorAll('.conversation-item').forEach(item => {
            item.classList.remove('active');
        });
        
        const selectedItem = document.querySelector(`[data-conversation-id="${conversationId}"]`);
        if (selectedItem) {
            selectedItem.classList.add('active');
        }
        
        // Enable input
        this.messageInput.disabled = false;
        this.sendButton.disabled = false;
        this.conversationTitle.textContent = 'Active Conversation';
        
        // Join conversation
        this.currentConversationId = conversationId;
        this.socket.emit('join-conversation', conversationId);
        
        // Clear messages
        this.messagesContainer.innerHTML = '';
    }
    
    async createNewConversation() {
        try {
            const response = await fetch('/api/conversations', {
                method: 'POST',
                headers: {
                    'Authorization': `Bearer ${this.token}`
                }
            });
            
            if (response.ok) {
                const data = await response.json();
                this.selectConversation(data.conversationId);
                this.loadConversations(); // Refresh the list
            } else {
                console.error('Failed to create conversation');
            }
        } catch (error) {
            console.error('Error creating conversation:', error);
        }
    }
    
    setupEventListeners() {
        this.sendButton.addEventListener('click', () => {
            this.sendMessage();
        });
        
        this.messageInput.addEventListener('keypress', (e) => {
            if (e.key === 'Enter' && !e.shiftKey) {
                e.preventDefault();
                this.sendMessage();
            }
        });
        
        this.newConversationButton.addEventListener('click', () => {
            this.createNewConversation();
        });
        
        this.logoutButton.addEventListener('click', () => {
            localStorage.removeItem('token');
            localStorage.removeItem('username');
            window.location.href = '/';
        });
    }
    
    sendMessage() {
        const message = this.messageInput.value.trim();
        if (!message || !this.currentConversationId) return;
        
        // Display user message immediately
        this.displayMessage({
            role: 'user',
            content: message,
            timestamp: new Date()
        });
        
        // Send to server
        this.socket.emit('send-message', {
            conversationId: this.currentConversationId,
            message: message
        });
        
        // Clear input
        this.messageInput.value = '';
        
        // Show typing indicator
        this.showTypingIndicator();
    }
    
    displayMessages(messages) {
        this.messagesContainer.innerHTML = '';
        messages.forEach(message => {
            this.displayMessage(message);
        });
        this.scrollToBottom();
    }
    
    displayMessage(message) {
        const messageElement = document.createElement('div');
        messageElement.className = `message ${message.role}`;
        
        messageElement.innerHTML = `
            <div class="message-content">
                ${this.formatMessage(message.content)}
            </div>
        `;
        
        this.messagesContainer.appendChild(messageElement);
        this.scrollToBottom();
    }
    
    showTypingIndicator() {
        const typingElement = document.createElement('div');
        typingElement.className = 'message assistant typing';
        typingElement.id = 'typing-indicator';
        
        typingElement.innerHTML = `
            <div class="message-content">
                <div class="typing-animation">
                    <span></span>
                    <span></span>
                    <span></span>
                </div>
            </div>
        `;
        
        this.messagesContainer.appendChild(typingElement);
        this.scrollToBottom();
    }
    
    appendToLastMessage(content) {
        const typingIndicator = document.getElementById('typing-indicator');
        if (typingIndicator) {
            typingIndicator.remove();
        }
        
        let lastMessage = this.messagesContainer.lastElementChild;
        if (!lastMessage || !lastMessage.classList.contains('assistant')) {
            // Create new assistant message
            lastMessage = document.createElement('div');
            lastMessage.className = 'message assistant streaming';
            lastMessage.innerHTML = '<div class="message-content"></div>';
            this.messagesContainer.appendChild(lastMessage);
        }
        
        const contentElement = lastMessage.querySelector('.message-content');
        contentElement.textContent += content;
        this.scrollToBottom();
    }
    
    finalizeLastMessage(message) {
        const lastMessage = this.messagesContainer.lastElementChild;
        if (lastMessage && lastMessage.classList.contains('streaming')) {
            lastMessage.classList.remove('streaming');
            const contentElement = lastMessage.querySelector('.message-content');
            contentElement.innerHTML = this.formatMessage(message.content);
        }
        
        this.scrollToBottom();
        this.loadConversations(); // Refresh conversation list
    }
    
    formatMessage(content) {
        // Basic markdown formatting
        return content
            .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
            .replace(/\*(.*?)\*/g, '<em>$1</em>')
            .replace(/`(.*?)`/g, '<code>$1</code>')
            .replace(/\n/g, '<br>');
    }
    
    scrollToBottom() {
        this.messagesContainer.scrollTop = this.messagesContainer.scrollHeight;
    }
}

// Initialize the chat app when the page loads
document.addEventListener('DOMContentLoaded', () => {
    new ChatApp();
});

Step 9: Test your application

Now let’s test your complete chat application:
  1. Start your server:
    npm run dev
    # or
    node server.js
    
  2. Open your browser and navigate to http://localhost:3000
  3. Create a new account by clicking “Register” and filling in your details
  4. Once logged in, you should see the chat interface
  5. Click the ”+” button to create a new conversation
  6. Type a message and press Enter
You should see:
  • Your message appears immediately
  • A typing indicator shows while the AI is thinking
  • The AI response streams in real-time
  • Conversations are saved and can be accessed later

What you’ve accomplished

Congratulations! You’ve built a complete chat application with: User authentication - Secure login and registration ✅ Real-time messaging - Instant message delivery with Socket.IO ✅ Conversation memory - Chat history preserved across sessions ✅ Streaming responses - AI messages appear as they’re generated ✅ Responsive design - Works on desktop and mobile devices ✅ Production patterns - Error handling, authentication, and security

Key concepts you’ve learned

Through building this application, you’ve learned: State management: How to maintain conversation history and user sessions in AI applications Real-time communication: Using WebSockets for instant message delivery and streaming AI responses Authentication patterns: Implementing secure user authentication in AI applications UI/UX for AI: Creating interfaces that handle the unique challenges of AI-generated content Error handling: Managing network issues, AI generation failures, and user errors

Next steps

Your chat application is functional, but there’s more you can add:

Troubleshooting

Messages not sending?
  • Check that your Azure AI Foundry endpoint and API key are correct
  • Verify the model deployment is active and healthy
  • Check browser console for error messages
Authentication issues?
  • Ensure JWT_SECRET is set in your .env file
  • Check that tokens are being stored in localStorage
  • Verify token expiration settings
Real-time features not working?
  • Confirm Socket.IO is properly connected
  • Check for firewall or proxy issues
  • Verify WebSocket support in your browser

Production considerations

Before deploying this application to production:
  1. Use a real database instead of in-memory storage
  2. Implement rate limiting to prevent abuse
  3. Add input validation and sanitization
  4. Set up proper logging and monitoring
  5. Configure HTTPS and secure headers
  6. Implement backup and recovery procedures

This tutorial focused on the practical steps of building a chat application. You learned through doing - implementing authentication, managing state, handling real-time communication, and creating user interfaces. The conceptual understanding of these patterns will serve you well in building more complex AI applications.