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.
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_idto 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
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
Navigate to the App
cd fiber-apps/activation-chat
Install Dependencies
npm install
Install to Fiberwise Platform
fiber app install ./
Expected Output
Installing app: Activation Chat
App installed successfully!
App ID: activation-chat
Status: Active
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
- Frontend calls
FIBER.agents.activate(agentId, input, context, metadata) - Platform creates an activation record with status "pending"
- Server-side agent's
execute()method is called with the input - Agent processes input and returns output
- Activation updated to "completed" with output data
- WebSocket event sent to connected clients
Complete Data Flow
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 |