Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

HTMX Architecture

Overview

General Bots Suite uses HTMX for its user interface - a modern approach that delivers the interactivity of a single-page application without the complexity of JavaScript frameworks like React, Vue, or Angular.

Why HTMX?

  • Simpler code, easier maintenance
  • Server-rendered HTML (fast, SEO-friendly)
  • Progressive enhancement
  • No build step required
  • Smaller payload than SPA frameworks

How HTMX Works

Traditional Web vs HTMX

Traditional (Full Page Reload):

User clicks → Browser requests full page → Server returns entire HTML → Browser replaces everything

HTMX (Partial Update):

User clicks → HTMX requests fragment → Server returns HTML snippet → HTMX updates only that part

Core Concept

HTMX extends HTML with attributes that define:

  1. What triggers the request (hx-trigger)
  2. Where to send it (hx-get, hx-post)
  3. What to update (hx-target)
  4. How to update it (hx-swap)

HTMX Attributes Reference

Request Attributes

AttributePurposeExample
hx-getGET request to URLhx-get="/api/tasks"
hx-postPOST requesthx-post="/api/tasks"
hx-putPUT requesthx-put="/api/tasks/1"
hx-patchPATCH requesthx-patch="/api/tasks/1"
hx-deleteDELETE requesthx-delete="/api/tasks/1"

Trigger Attributes

AttributePurposeExample
hx-triggerEvent that triggers requesthx-trigger="click"
Load on pagehx-trigger="load"
Periodic pollinghx-trigger="every 5s"
Keyboard eventhx-trigger="keyup changed delay:300ms"

Target & Swap Attributes

AttributePurposeExample
hx-targetElement to updatehx-target="#results"
hx-swapHow to insert contenthx-swap="innerHTML"
hx-swap="outerHTML"
hx-swap="beforeend"
hx-swap="afterbegin"

Suite Architecture

File Structure

ui/suite/
├── index.html          # Main entry point with navigation
├── base.html           # Base template
├── home.html           # Home page
├── default.gbui        # Full desktop layout
├── single.gbui         # Simple chat layout
├── designer.html       # Visual dialog designer
├── editor.html         # Code editor
├── settings.html       # User settings
├── css/
│   ├── app.css         # Application styles
│   ├── apps-extended.css # Extended app styles
│   ├── components.css  # UI components
│   └── global.css      # Global styles
├── js/
│   ├── htmx-app.js     # HTMX application logic
│   ├── theme-manager.js # Theme switching
│   └── vendor/         # Third-party libraries
├── partials/           # Reusable HTML fragments
├── auth/               # Authentication views
├── attendant/          # Attendant interface
├── chat/
│   ├── chat.html       # Chat component
│   ├── chat.css        # Chat styles
│   └── projector.html  # Projector view
├── drive/              # File manager
├── tasks/              # Task manager
├── mail/               # Email client
├── calendar/           # Calendar view
├── meet/               # Video meetings
├── paper/              # Document editor
├── research/           # AI search
├── analytics/          # Dashboards
├── sources/            # Prompts & templates
├── tools/              # Developer tools
└── monitoring/         # System monitoring

Loading Pattern

The Suite uses lazy loading - components load only when needed:

<!-- Main navigation in index.html -->
<a href="#chat" 
   data-section="chat"
   hx-get="/ui/suite/chat/chat.html" 
   hx-target="#main-content"
   hx-swap="innerHTML">
    Chat
</a>

When user clicks “Chat”:

  1. HTMX requests /ui/suite/chat/chat.html
  2. Server returns the Chat HTML fragment
  3. HTMX inserts it into #main-content
  4. Only Chat code loads, not entire app

Component Patterns

1. Load on Page View

<!-- Tasks load immediately when component is shown -->
<div id="task-list"
     hx-get="/api/tasks"
     hx-trigger="load"
     hx-swap="innerHTML">
    <div class="loading">Loading tasks...</div>
</div>

2. Form Submission

<!-- Add task form -->
<form hx-post="/api/tasks"
      hx-target="#task-list"
      hx-swap="afterbegin"
      hx-on::after-request="this.reset()">
    <input type="text" name="text" placeholder="New task..." required>
    <button type="submit">Add</button>
</form>

Flow:

  1. User types task, clicks Add
  2. HTMX POSTs form data to /api/tasks
  3. Server creates task, returns HTML for new task item
  4. HTMX inserts at beginning of #task-list
  5. Form resets automatically

3. Click Actions

<!-- Task item with actions -->
<div class="task-item" id="task-123">
    <input type="checkbox" 
           hx-patch="/api/tasks/123"
           hx-vals='{"completed": true}'
           hx-target="#task-123"
           hx-swap="outerHTML">
    <span>Review quarterly report</span>
    <button hx-delete="/api/tasks/123"
            hx-target="#task-123"
            hx-swap="outerHTML"
            hx-confirm="Delete this task?">
        🗑
    </button>
</div>

4. Search with Debounce

<!-- Search input with 300ms delay -->
<input type="text" 
       name="q"
       placeholder="Search..."
       hx-get="/api/search"
       hx-trigger="keyup changed delay:300ms"
       hx-target="#search-results"
       hx-indicator="#search-spinner">

<span id="search-spinner" class="htmx-indicator">🔄</span>
<div id="search-results"></div>

Flow:

  1. User types in search box
  2. After 300ms of no typing, HTMX sends request
  3. Spinner shows during request
  4. Results replace #search-results content

5. Real-time Updates (WebSocket)

<!-- Chat with WebSocket -->
<div id="chat-app" hx-ext="ws" ws-connect="/ws">
    <div id="messages"
         hx-get="/api/sessions/current/history"
         hx-trigger="load"
         hx-swap="innerHTML">
    </div>
    
    <form ws-send>
        <input name="content" type="text">
        <button type="submit">Send</button>
    </form>
</div>

Flow:

  1. WebSocket connects on load
  2. History loads via HTMX GET
  3. New messages sent via WebSocket (ws-send)
  4. Server pushes updates to all connected clients

6. Polling for Updates

<!-- Analytics that refresh every 30 seconds -->
<div class="metric-card"
     hx-get="/api/analytics/messages/count"
     hx-trigger="load, every 30s"
     hx-swap="innerHTML">
    <!-- Content updates automatically -->
</div>

7. Infinite Scroll

<!-- File list with infinite scroll -->
<div id="file-list">
    <!-- Files here -->
    
    <div hx-get="/api/files?page=2"
         hx-trigger="revealed"
         hx-swap="afterend">
        Loading more...
    </div>
</div>

API Response Patterns

Server Returns HTML Fragments

The server doesn’t return JSON - it returns ready-to-display HTML:

Request:

GET /api/tasks

Response:

<div class="task-item" id="task-1">
    <input type="checkbox">
    <span>Review quarterly report</span>
</div>
<div class="task-item" id="task-2">
    <input type="checkbox">
    <span>Update documentation</span>
</div>

Swap Strategies

StrategyEffect
innerHTMLReplace contents of target
outerHTMLReplace entire target element
beforeendAppend inside target (at end)
afterbeginPrepend inside target (at start)
beforebeginInsert before target
afterendInsert after target
deleteDelete target element
noneDon’t swap (for side effects)

CSS Integration

Loading Indicators

/* Hidden by default */
.htmx-indicator {
    display: none;
}

/* Shown during request */
.htmx-request .htmx-indicator {
    display: inline-block;
}

/* Or when indicator IS the requesting element */
.htmx-request.htmx-indicator {
    display: inline-block;
}

Transition Effects

/* Fade in new content */
.htmx-settling {
    opacity: 0;
}

.htmx-swapping {
    opacity: 0;
    transition: opacity 0.2s ease-out;
}

JavaScript Integration

HTMX Events

// After any HTMX swap
document.body.addEventListener('htmx:afterSwap', (e) => {
    console.log('Content updated:', e.detail.target);
});

// Before request
document.body.addEventListener('htmx:beforeRequest', (e) => {
    console.log('Sending request to:', e.detail.pathInfo.path);
});

// After request completes
document.body.addEventListener('htmx:afterRequest', (e) => {
    if (e.detail.successful) {
        console.log('Request succeeded');
    } else {
        console.error('Request failed');
    }
});

// On WebSocket message
document.body.addEventListener('htmx:wsAfterMessage', (e) => {
    console.log('Received:', e.detail.message);
});

Triggering HTMX from JavaScript

// Trigger an HTMX request programmatically
htmx.trigger('#task-list', 'load');

// Make an AJAX request
htmx.ajax('GET', '/api/tasks', {
    target: '#task-list',
    swap: 'innerHTML'
});

// Process new HTMX content
htmx.process(document.getElementById('new-content'));

Designer Page Architecture

The visual dialog designer uses a hybrid approach:

Canvas Management (JavaScript)

// State managed in JavaScript
const state = {
    nodes: new Map(),      // Node data
    connections: [],       // Connections between nodes
    zoom: 1,               // Canvas zoom level
    pan: { x: 0, y: 0 }    // Canvas position
};

File Operations (HTMX)

<!-- Load file via HTMX -->
<button hx-get="/api/v1/designer/files"
        hx-target="#file-list-content">
    Open File
</button>

<!-- Save via HTMX -->
<button hx-post="/api/v1/designer/save"
        hx-include="#designer-data">
    Save
</button>

Drag-and-Drop (JavaScript)

// Toolbox items are draggable
toolboxItems.forEach(item => {
    item.addEventListener('dragstart', (e) => {
        e.dataTransfer.setData('nodeType', item.dataset.nodeType);
    });
});

// Canvas handles drop
canvas.addEventListener('drop', (e) => {
    const nodeType = e.dataTransfer.getData('nodeType');
    createNode(nodeType, e.clientX, e.clientY);
});

Performance Considerations

1. Minimize Request Size

Return only what’s needed:

<!-- Good: Return just the updated row -->
<tr id="row-123">...</tr>

<!-- Bad: Return entire table -->
<table>...</table>

2. Use Appropriate Triggers

<!-- Don't poll too frequently -->
hx-trigger="every 30s"  <!-- Good for dashboards -->
hx-trigger="every 1s"   <!-- Too frequent! -->

<!-- Debounce user input -->
hx-trigger="keyup changed delay:300ms"  <!-- Good -->
hx-trigger="keyup"                       <!-- Too many requests -->

3. Lazy Load Heavy Content

<!-- Load tab content only when tab is clicked -->
<div role="tabpanel" 
     hx-get="/api/heavy-content"
     hx-trigger="intersect once">
</div>

4. Use hx-boost for Navigation

<!-- Boost all links in nav -->
<nav hx-boost="true">
    <a href="/page1">Page 1</a>  <!-- Now uses HTMX -->
    <a href="/page2">Page 2</a>
</nav>

Security

CSRF Protection

HTMX automatically includes CSRF tokens:

<meta name="csrf-token" content="abc123...">
// Configure HTMX to send CSRF token
document.body.addEventListener('htmx:configRequest', (e) => {
    e.detail.headers['X-CSRF-Token'] = document.querySelector('meta[name="csrf-token"]').content;
});

Content Security

  • Server validates all inputs
  • HTML is sanitized before rendering
  • Authentication checked on every request

Comparison: HTMX vs React

AspectHTMXReact
Learning CurveLow (HTML attributes)High (JSX, hooks, state)
Bundle Size~14KB~40KB + app code
Build StepNoneRequired
Server LoadMore (renders HTML)Less (returns JSON)
Client LoadLessMore
SEOExcellentRequires SSR
ComplexitySimpleComplex
Best ForContent sites, dashboardsComplex SPAs, offline apps

Further Reading