Skip to content

Frontend UI Architecture

Architectural reference for VIBE's frontend UI patterns. Optimized for LLM consumption.

See also:

  • core.md - Core VIBE engine (probing, sessions, handlers)

1. DESIGN PHILOSOPHY

VIBE uses a server-driven UI with minimal client-side JavaScript. Two lightweight libraries handle all frontend interactivity: htmx for server communication and Alpine.js for ephemeral UI state.

Principles:

  • Server is source of truth for application state
  • Client handles only transient UI state (dropdowns, tabs)
  • No heavy SPA framework -- minimal JS footprint

2. HTMX (SERVER-DRIVEN UPDATES)

Purpose: AJAX requests and DOM updates via HTML attributes -- no custom JavaScript needed for most interactions.

Key Patterns:

  • hx-post, hx-get -- Form submissions and data fetching
  • hx-swap="outerHTML" -- Replace elements with server response
  • hx-swap-oob="true" -- Out-of-band swaps for updating multiple elements
  • hx-trigger="change" -- Auto-submit on field changes
  • sse-connect, sse-swap -- Server-Sent Events for streaming

Primary Use Cases:

  • Interview form auto-submission (/process/<template_id>/@<version>/)
  • Partial page updates without reload
  • Real-time streaming for assistant chat
  • Dynamic question panel updates (add/remove/reorder via OOB swaps)

Key Files:

  • vibe/static/htmx.min.js -- htmx library
  • vibe/static/sse.min.js -- SSE extension for streaming
  • vibe/web/htmx.py -- OOB swap helpers (OOBSwap, render_oob_div)
  • vibe/web/core/html_fragments.py -- HTML fragment rendering utilities

3. ALPINE.JS (CLIENT-SIDE INTERACTIVITY)

Purpose: Lightweight reactive framework for ephemeral UI state that doesn't need server persistence.

CRITICAL: CSP Build Constraints

VIBE uses the CSP-compliant build (alpine-csp.min.js) -- no unsafe-eval required. This means inline JavaScript expressions are NOT supported:

NOT Allowed (Regular Alpine) Required (Alpine CSP)
x-data="{ count: 0 }" x-data="componentName" (registered component)
@click="count++" @click="increment" (method on component)
x-data="myFunction()" x-data="myComponent" (no parentheses)

How to Add New Alpine Components:

  1. Define in vibe/static/alpine-components.js using Alpine.data():

    document.addEventListener('alpine:init', () => {
        Alpine.data('myComponent', () => ({
            count: 0,
            increment() { this.count++; }
        }));
    });
    
  2. Use in templates WITHOUT parentheses:

    <div x-data="myComponent">
        <button @click="increment">Count: <span x-text="count"></span></button>
    </div>
    
  3. Pass dynamic data via data-* attributes, read with this.$el.dataset.

Common Mistake: Writing x-data="myComponent()" with parentheses will fail silently. The CSP build cannot evaluate function calls.

Custom Components: devControls, fileUpload, reviewWorkbench (in review.js) Custom Stores: $store.tabs, $store.assistantStatus Custom Directives: x-autogrow (textarea auto-expand), x-autoscroll (container auto-scroll)

Note: The Alpine Collapse plugin is NOT installed. Do not use x-collapse.

Key Files:

  • vibe/static/alpine-csp.min.js -- Alpine CSP build
  • vibe/static/alpine-components.js -- Custom stores, directives, and components
  • vibe/review/static/review.js -- Review module components

4. INTEGRATION PATTERN

User Action
    |
Alpine.js handles ephemeral state (dropdown open, tab active)
    |
htmx sends request to server (hx-post, hx-get)
    |
Server renders HTML fragment (Jinja templates)
    |
htmx swaps fragment into DOM (hx-swap, hx-swap-oob)
    |
Alpine.js reinitializes on new content

5. DEVELOPER MODE UI ENDPOINTS

Location: vibe/web/routes/ui_helpers.py

Server-rendered endpoints providing developer diagnostics and dynamic UI fragments. Most require developer mode (is_devel()) to be active.

Explain Relevance (/explain/<template_id>/<path:question_path>) -- Developer-mode endpoint showing why a question is relevant. Displays active/inactive usage paths with condition evaluation, probe access diagnostics (accessed_keys, accessed_locations, needed_vars), and parent-dependency ancestry. Uses _safe_eval_condition() with template_env.overlay(undefined=jinja2.Undefined) to safely evaluate compound conditions without ProbeMissingPathError.

Remaining Questions (/remaining_questions/<template_id>/) -- HTMX endpoint returning unanswered required questions using find_unanswered_required(). Questions are sorted by template config order.

Appendix Preview (/preview/<template_id>/appendix/<alias>) -- Renders preview of appendix content. Builds context mirroring insert() precedence (host defaults -> component defaults -> passthrough -> actual answers). Uses web preview mode (ChainableUndefined via _is_web_preview) for graceful degradation when not all variables are answered.

Locked Field Reasons -- _get_lock_source() (vibe/web/core/rendering.py) returns the ValueSource (SESSION_CONTEXT, LINKED) for locked questions. _locked_reason_text() provides localized tooltip text. Handlers pass locked_reason via QuestionRenderContext to templates for tooltip display on lock icons.

6. FILE LOCATION INDEX

What Where
htmx library vibe/static/htmx.min.js
SSE extension vibe/static/sse.min.js
Alpine.js CSP build vibe/static/alpine-csp.min.js
Alpine components vibe/static/alpine-components.js
Review components vibe/review/static/review.js
Main UI script vibe/static/script.js
HTML templates vibe/web/templates/
Layout templates vibe/web/templates/layout/
Question widgets vibe/web/templates/question_types/
OOB swap helpers vibe/web/htmx.py
HTML fragment utils vibe/web/core/html_fragments.py
UI helper routes vibe/web/routes/ui_helpers.py
Question rendering vibe/web/core/rendering.py

Document Version: 1.2 Last Updated: 2026-03-22 Notes: Updated file paths for package reorganization: vibe/web_core/ merged into vibe/web/core/ (Python) and vibe/web/templates/ (HTML)