Build Your First AI Chat App

Create a fully functional AI chat application using the Fiberwise platform. This beginner-friendly tutorial walks you through the real activation-chat app code step by step.

15-20 minutes Beginner AI Chat App

What You'll Build

A production-ready chat application that demonstrates core Fiberwise patterns:

  • Web Components: Modern custom elements with Shadow DOM encapsulation
  • FIBER SDK Integration: Initialize and use the Fiberwise SDK
  • Agent Activations: Store and retrieve chat messages as agent activations
  • Real-time Updates: WebSocket-powered live message updates
  • LLM Provider Selection: Support for multiple AI model providers

Prerequisites

Before you begin, ensure you have a working Fiberwise environment. This tutorial builds on the core platform setup.

🔧 Required Setup

✅ All Set?

Once your environment is ready, you can start building the chat app.

Step 1: Understanding the App Manifest

Every Fiberwise app starts with an app_manifest.yaml file. This declarative configuration defines your app's metadata, data models, routes, and AI agents. Here is the actual manifest from the activation-chat app:

app:
  name: Activation Chat
  app_slug: activation-chat
  version: 0.0.3
  description: A chat application that uses agent activations and dynamic data as
    the message store
  entryPoint: index.js
  icon: fas fa-comments
  category: simple
  publisher: Fiberwise
  user_isolation: enforced
  models:
  - name: Chat
    model_slug: chats
    description: Simple chat session container
    fields:
    - name: Chat ID
      field_column: chat_id
      type: uuid
      required: true
      is_primary_key: true
      description: Primary key used as chat_id in agent activation context
    - name: Title
      field_column: title
      type: string
      required: true
      default: New Chat
    - name: Created At
      field_column: created_at
      type: timestamp
      default: CURRENT_TIMESTAMP
      is_system_field: true
  routes:
  - path: /
    component: chat-app
    title: Chat
    icon: fas fa-comments
agents:
- name: Chat Agent
  agent_slug: chat-agent
  agent_type_id: llm
  version: 1.0.0
  description: AI agent for handling chat conversations
  system_prompt: You are a helpful AI assistant for chat conversations.
  temperature: 0.7
  max_tokens: 2048

Key Manifest Concepts

  • app section: Defines app metadata, entry point, and data models
  • models: Database tables the app uses (here, a simple Chat session tracker)
  • routes: Maps URL paths to Web Components
  • agents: AI agents your app can activate (the chat-agent handles conversations)

Step 2: The App Entry Point (index.js)

The entry point initializes the FIBER SDK and exports the required initialize() and render() functions that the platform calls. Here is the actual index.js:

/**
 * Activation Chat App
 *
 * A chat application that uses agent activations as the message store.
 * Demonstrates how to use agent activations for chat functionality.
 */
import FiberWise from 'fiberwise';

// Import FontAwesome CSS
import '@fortawesome/fontawesome-free/css/all.min.css';

// Create a FIBER SDK instance for this app using new constructor pattern
export const FIBER = new FiberWise();


/**
 * Standard initialize function for app platform integration
 * @param {Object} appBridge - AppBridge instance from platform
 * @param {Object} manifest - The application manifest
 * @returns {Promise<boolean>} Success status
 */
export async function initialize(appBridge, manifest) {
  console.log('[ActivationChat] Initializing app...');
  FIBER.initialize(appBridge, manifest);
  return true;
}

/**
 * Standard render function for app platform integration
 * @param {HTMLElement} mountPoint - Where to mount the app
 */
export function render(mountPoint) {
  console.log('[ActivationChat] Rendering app...');
  const el = document.createElement('chat-app');
  mountPoint.appendChild(el);
}

// Register the main component
import './chat-app.js';

Platform Integration Pattern

Every Fiberwise app must export two functions:

  • initialize(appBridge, manifest): Called once when the app loads. Initialize the FIBER SDK here.
  • render(mountPoint): Called to render your app. Create and append your root Web Component.

Step 3: The Main Chat Component (chat-app.js)

The chat-app Web Component orchestrates the entire chat application. Let's examine the key patterns used:

Component Structure

import htmlTemplate from './chat-app.html?raw';
import cssStyles from './chat-app.css?inline';
import { FIBER } from './index.js';
import './chat-messages.js';
import './chat-list.js';
import './chat-input.js';

export class ActivationChatApp extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });

    // App state
    this.isLoading = true;
    this.currentChatId = null;
    this.selectedAgentId = null;
    this.isStreaming = false;

    // LLM provider state
    this.providers = [];
    this.selectedProviderId = null;

    // Model settings
    this.modelSettings = {
      temperature: 0.7,
      maxTokens: 2048,
      systemPrompt: 'You are a helpful AI assistant.',
      topP: 1.0,
      frequencyPenalty: 0.0,
      presencePenalty: 0.0
    };
  }

  connectedCallback() {
    if (!this.initialized) {
      this.initialized = true;
      window.addEventListener('popstate', (event) => {
        if (event.state && event.state.chatId) {
          this.loadSessionMessages(event.state.chatId);
        }
      });
      this.init();
    }
  }

  // ... rest of component
}

customElements.define('chat-app', ActivationChatApp);

Initializing the App

The init() method connects to real-time updates and loads providers and agents:

async init() {
  try {
    // Connect to WebSocket for real-time updates
    await FIBER.realtime.connect();

    // Set up WebSocket event handler for activation updates
    FIBER.realtime.on('message', (message) => {
      if (message.type === 'activation_completed') {
        this.handleActivationCompleted(message);
      }
    });

    // Load available LLM providers
    await this.loadProviders();

    // Load agents defined in the manifest
    await this.loadAgents();

    // Check if we have a session ID in the URL
    const urlSessionId = this.getSessionIdFromUrl();

    if (urlSessionId) {
      this.currentChatId = urlSessionId;
      await this.loadSessionMessages(urlSessionId);
    } else {
      this.currentChatId = null;
      this.messages = [];
    }

    this.isLoading = false;
    this.render();
    this.setupEventListeners();
    this.scrollToBottom();
  } catch (error) {
    console.error('Error initializing chat app:', error);
    this.isLoading = false;
    this.render();
  }
}

Loading Providers and Agents

async loadProviders() {
  try {
    const response = await FIBER.apps.listModelProviders();
    this.providers = Array.isArray(response) ? response : (response.items || []);
    this.selectedProviderId = this.providers[0]?.provider_id || null;
    return this.providers;
  } catch (error) {
    console.error('Error loading providers:', error);
    return [];
  }
}

async loadAgents() {
  try {
    const response = await FIBER.agents.list();
    const agents = response.agents || response.items || response || [];
    this.selectedAgentId = agents.length > 0 ? (agents[0].agent_id || agents[0].id) : null;
    return agents;
  } catch (error) {
    console.error('Error loading agents:', error);
    return [];
  }
}

Step 4: Sending Messages (Agent Activation)

The core of the chat app is the sendMessage() method. This activates the AI agent with the user's message:

async sendMessage(content) {
  if (!content.trim()) return;

  // Validate we have a selected agent
  if (!this.selectedAgentId) {
    await this.loadAgents();
    if (!this.selectedAgentId) {
      console.error('[ActivationChat] No agent available');
      return;
    }
  }

  try {
    // Create a new chat session if needed
    if (!this.currentChatId) {
      await this.createNewSession(content.substring(0, 15));
    }

    // Build metadata with provider and model settings
    const metadata = {};
    if (this.selectedProviderId) {
      const provider = this.providers.find(p => p.provider_id === this.selectedProviderId);
      if (provider) {
        metadata.provider_id = provider.provider_id;
        metadata.model_id = provider.default_model;
        metadata.temperature = this.modelSettings.temperature;
        metadata.max_tokens = this.modelSettings.maxTokens;
        metadata.top_p = this.modelSettings.topP;
        metadata.frequency_penalty = this.modelSettings.frequencyPenalty;
        metadata.presence_penalty = this.modelSettings.presencePenalty;
      }
    }

    // Build context with chat_id and system prompt
    const context = {
      chat_id: this.currentChatId,
      system_prompt: this.modelSettings.systemPrompt
    };

    if (this.selectedProviderId) {
      context.provider_id = this.selectedProviderId;
    }

    // ACTIVATE THE AGENT - This is the core API call
    const activation = await FIBER.agents.activate(
      this.selectedAgentId,      // Agent ID from manifest
      { prompt: content },        // User's message as input
      context,                    // Context with chat_id and system_prompt
      metadata                    // LLM settings
    );

    if (activation) {
      this.render();
      this.scrollToBottom();
    }
  } catch (error) {
    console.error('Error sending message:', error);
    this.render();
  }
}

FIBER.agents.activate() Parameters

  • agentId: The agent to activate (from your manifest's agents section)
  • input_data: The input to the agent (e.g., { prompt: "Hello!" })
  • context: Metadata like chat_id to group related activations
  • metadata: LLM settings like temperature, max_tokens, provider_id

Step 5: Real-time Updates via WebSocket

When the agent completes processing, the platform sends a WebSocket event. Here's how to handle it:

// In init(), set up the WebSocket listener
FIBER.realtime.on('message', (message) => {
  if (message.type === 'activation_completed') {
    this.handleActivationCompleted(message);
  }
});

// Handler for activation completion
async handleActivationCompleted(message) {
  // Check if this update is for our current chat
  if (this.currentChatId && message.context && message.context.chat_id === this.currentChatId) {
    const chatMessages = this.shadowRoot.querySelector('chat-messages');
    if (chatMessages) {
      // Tell the chat-messages component to update this activation
      chatMessages.updateActivation(message.activation_id, message.status);
      chatMessages.scrollToBottom();
    }
  }
}

Real-time Message Flow

graph TD A[User Sends Message] --> B[FIBER.agents.activate] B --> C[API Creates Activation] C --> D[Agent Processes with LLM] D --> E[Activation Status: completed] E --> F[WebSocket: activation_completed] F --> G[handleActivationCompleted] G --> H[UI Updates with Response] classDef user fill:#e3f2fd classDef system fill:#e8f5e8 classDef ui fill:#f3e5f5 class A user class B,C,D,E,F system class G,H ui

Step 6: Displaying Messages (chat-messages.js)

The chat-messages component loads and displays activation history. Here's how it retrieves messages:

import { FIBER } from './index.js';
import htmlTemplate from './chat-messages.html?raw';
import cssStyles from './chat-messages.css?inline';

export class ChatMessages extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
    this.messages = [];
    this.chatId = null;
    this.loading = false;
    this.error = null;
    this.providers = [];
    this.selectedProviderId = null;
  }

  static get observedAttributes() {
    return ['chat-id', 'providers-data', 'selected-provider-id'];
  }

  attributeChangedCallback(name, oldValue, newValue) {
    if (name === 'chat-id') {
      if (newValue !== oldValue) {
        this.chatId = newValue;
        this.messages = [];
        this.render();
        if (newValue) {
          this.loadMessages();
        }
      }
    }
  }

  async loadMessages() {
    if (!this.chatId) {
      this.messages = [];
      this.render();
      return;
    }

    try {
      this.loading = true;
      this.render();

      // Query activations by chat_id context
      const response = await FIBER.agents.getActivations({
        context: { chat_id: this.chatId },
        limit: 100,
        sortBy: 'started_at',
        sortDir: 'asc'
      });

      // Map activations to message format
      this.messages = response.map(activation => ({
        id: activation.id,
        chat_id: activation.context?.chat_id,
        input_data: activation.input_data || { prompt: activation.input_summary },
        output_data: activation.output_data || (activation.output_summary ? { text: activation.output_summary } : null),
        status: activation.status,
        started_at: activation.started_at,
        context: activation.context || {},
        error: activation.error,
        error_message: activation.error_message
      })).sort((a, b) => new Date(b.started_at) - new Date(a.started_at));

      this.loading = false;
      this.render();
    } catch (error) {
      console.error('Error loading messages:', error);
      this.error = error;
      this.loading = false;
      this.render();
    }
  }
}

Updating a Single Activation

async updateActivation(activationId, status) {
  // Find the message that needs updating
  const messageIndex = this.messages.findIndex(msg => msg.id === activationId);
  if (messageIndex === -1) {
    return this.loadMessages(); // Reload all if not found
  }

  try {
    // Fetch the updated activation details
    const updatedActivation = await FIBER.agents.getActivation(activationId);

    // Update our local message copy
    this.messages[messageIndex] = {
      id: updatedActivation.id,
      chat_id: updatedActivation.context?.chat_id,
      input_data: updatedActivation.input_data || { prompt: updatedActivation.input_summary },
      output_data: updatedActivation.output_data || { text: updatedActivation.output_summary },
      status: updatedActivation.status,
      started_at: updatedActivation.started_at,
      context: updatedActivation.context || {}
    };

    // Re-render just this message or do full re-render
    const messageElement = this.shadowRoot.querySelector(`.activation-message[data-id="${activationId}"]`);
    if (messageElement) {
      messageElement.outerHTML = this.renderMessage(this.messages[messageIndex]);
    } else {
      this.render();
    }
  } catch (error) {
    console.error(`Error updating activation ${activationId}:`, error);
  }
}

Step 7: User Input (chat-input.js)

The chat-input component handles user input and dispatches custom events:

import htmlTemplate from './chat-input.html?raw';
import cssStyles from './chat-input.css?inline';

export class ChatInput extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
    this.value = '';
  }

  connectedCallback() {
    this.render();
    this.setupEventListeners();
  }

  render() {
    this.shadowRoot.innerHTML = `
      <style>${cssStyles}</style>
      ${htmlTemplate}
    `;
  }

  setupEventListeners() {
    const input = this.shadowRoot.getElementById('message-input');
    const sendButton = this.shadowRoot.getElementById('send-button');

    if (input) {
      input.addEventListener('input', (e) => {
        this.value = e.target.value;

        // Auto-resize textarea
        input.style.height = 'auto';
        input.style.height = (input.scrollHeight) + 'px';

        // Enable/disable send button based on content
        if (sendButton) {
          sendButton.disabled = !this.value.trim();
        }
      });

      input.addEventListener('keydown', (e) => {
        if (e.key === 'Enter' && !e.shiftKey) {
          e.preventDefault();
          this.sendMessage();
        }
      });
    }

    if (sendButton) {
      sendButton.addEventListener('click', () => {
        this.sendMessage();
      });
    }
  }

  sendMessage() {
    const input = this.shadowRoot.getElementById('message-input');
    const message = this.value.trim();

    if (message) {
      // Dispatch custom event that parent can listen to
      this.dispatchEvent(new CustomEvent('message-sent', {
        detail: { content: message },
        bubbles: true,
        composed: true
      }));

      // Clear input
      this.value = '';
      input.value = '';
      input.style.height = 'auto';
    }
  }
}

customElements.define('chat-input', ChatInput);

The HTML Template (chat-input.html)

<div class="chat-input-container">
  <textarea id="message-input" placeholder="Type your message here..." rows="1"></textarea>
  <button id="send-button" class="send-button">
    <i class="fas fa-paper-plane"></i>
  </button>
</div>

Custom Events Pattern

Web Components communicate via custom events. The chat-input dispatches a message-sent event that chat-app listens for:

// In chat-app.js render():
const chatInput = this.shadowRoot.querySelector('chat-input');
if (chatInput) {
  chatInput.addEventListener('message-sent', (e) => {
    this.sendMessage(e.detail.content);
  });
}

Step 8: The App Layout (chat-app.html)

The main app layout uses nested Web Components for a clean separation of concerns:

<div class="chat-app-container">
  <header class="chat-header">
    <h1>Activation Chat</h1>
    <div class="header-actions">
      <button id="settings-btn" class="button icon-button" title="Chat Settings">
        <i class="fas fa-cog"></i>
      </button>
      <button id="new-chat-btn" class="button">New Chat</button>
    </div>
  </header>

  <div class="chat-layout">
    <aside class="sidebar">
      <chat-list></chat-list>
    </aside>

    <main class="main-content">
      <div class="messages-wrapper">
        <chat-messages class="chat-messages"></chat-messages>
      </div>
      <chat-input class="chat-input"></chat-input>
    </main>
  </div>
</div>

Step 9: Managing Chat Sessions

The app creates and manages chat sessions using the data model and activation context:

// Create a new chat session
async createNewSession(title) {
  try {
    // Create the chat record in the data model
    const chatRecord = await FIBER.data.createItem('chats', { title: title });

    // Set as current chat and clear messages
    this.currentChatId = chatRecord.item_id;
    this.messages = [];

    // Update URL to reflect the new chat
    this.updateUrl(this.currentChatId);

    return chatRecord;
  } catch (error) {
    console.error('Error creating new session:', error);
    throw error;
  }
}

// Load existing chat sessions
async loadSessions() {
  try {
    // Get all sessions from the Chat model
    const response = await FIBER.data.listItems('chats', {
      limit: 100,
      sort: [{ field: 'created_at', direction: 'desc' }]
    });

    this.sessions = response.items || [];
    return this.sessions;
  } catch (error) {
    console.error('Error loading chat sessions:', error);
    return [];
  }
}

// URL management for deep linking
updateUrl(sessionId) {
  if (!sessionId) return;
  const url = new URL(window.location.href);
  url.searchParams.set('chatId', sessionId);
  history.pushState({ chatId: sessionId }, '', url);
}

getSessionIdFromUrl() {
  const urlParams = new URLSearchParams(window.location.search);
  return urlParams.get('chatId');
}

Step 10: Install and Run

1
Navigate to the App
cd fiber-apps/activation-chat
2
Install Dependencies
npm install
3
Install to Fiberwise Platform
fiber app install ./

Expected Output

Installing app: Activation Chat
App installed successfully!
App ID: activation-chat
Status: Active
4
Open in Browser
http://localhost:8000/activation-chat

Appendix: Server-Side Agent Pattern

For reference, here is the Python agent pattern used on the server side when an activation is processed. Your manifest's agent definition triggers this execution:

async def execute(self, input_data: Dict[str, Any], **kwargs) -> Dict[str, Any]:
    """
    Execute the agent with the given input data.

    Args:
        input_data: The input from FIBER.agents.activate(), e.g., {"prompt": "Hello!"}
        **kwargs: Additional context including:
            - fiber: FIBER SDK instance
            - llm_service: LLM service for AI calls
            - context: The context passed to activate()
            - metadata: The metadata passed to activate()

    Returns:
        Dict containing the response, e.g., {"response": "Hello! How can I help?"}
    """
    fiber = kwargs.get('fiber')
    llm_service = kwargs.get('llm_service')
    context = kwargs.get('context', {})

    # Get the user's message
    prompt = input_data.get('prompt', '')
    system_prompt = context.get('system_prompt', 'You are a helpful assistant.')

    # Call the LLM
    response = await llm_service.chat_completion(
        messages=[
            {"role": "system", "content": system_prompt},
            {"role": "user", "content": prompt}
        ],
        temperature=kwargs.get('metadata', {}).get('temperature', 0.7),
        max_tokens=kwargs.get('metadata', {}).get('max_tokens', 2048)
    )

    return {
        "response": response.content,
        "text": response.content
    }

Agent Execution Flow

  1. Frontend calls FIBER.agents.activate(agentId, input, context, metadata)
  2. Platform creates an activation record with status "pending"
  3. Server-side agent's execute() method is called with the input
  4. Agent processes input and returns output
  5. Activation updated to "completed" with output data
  6. WebSocket event sent to connected clients

Complete Data Flow

graph TD User[User Types Message] -->|Enter/Click Send| Input[chat-input.js] Input -->|CustomEvent: message-sent| ChatApp[chat-app.js] ChatApp -->|FIBER.agents.activate| SDK[FIBER SDK] SDK -->|POST /activations| API[Fiberwise API] API -->|Create Activation| DB[(Database)] API -->|Trigger Agent| Agent[Chat Agent] Agent -->|Call LLM| LLM[LLM Provider] LLM -->|Response| Agent Agent -->|Save Output| DB DB -->|Status: completed| WS[WebSocket] WS -->|activation_completed| ChatApp ChatApp -->|updateActivation| Messages[chat-messages.js] Messages -->|Render| UI[Updated UI] classDef user fill:#e3f2fd classDef component fill:#f3e5f5 classDef system fill:#e8f5e8 classDef storage fill:#fff3e0 class User user class Input,ChatApp,Messages,UI component class SDK,API,Agent,LLM,WS system class DB storage

Summary

You have learned the core patterns of Fiberwise app development:

Concept Implementation
App Configuration app_manifest.yaml defines models, routes, and agents
Platform Integration initialize() and render() functions in entry point
SDK Initialization new FiberWise() and FIBER.initialize(appBridge, manifest)
Agent Activation FIBER.agents.activate(agentId, input, context, metadata)
Real-time Updates FIBER.realtime.on('message', handler)
Data Storage FIBER.data.createItem() and FIBER.data.listItems()
Activation Queries FIBER.agents.getActivations({ context: { chat_id } })
Web Components Shadow DOM, custom events, observed attributes

Next Steps

Customize the Agent

Modify the system prompt and model settings in the manifest to change the AI's behavior.

Agent Guide

Add Data Models

Extend the app with additional data models for user preferences or conversation metadata.

Data Models

Explore Examples

Check out other apps in the fiber-apps directory for more patterns.

More Tutorials