Building a Vue 3 Chat UI with Streaming LLM Responses (OpenClaw + Node.js + MariaDB)

Written by

in

, ,

The Chat UI Project Timeline (May–July 2026)

What started as a quick experiment to talk to a local LLM through OpenClaw slowly turned into something much larger than expected — a full chat application with streaming responses, authentication, persistent sessions, and eventually a modular Vue 3 frontend backed by Node.js and MariaDB.

None of this was planned in advance. It grew the way most working systems do: one useful feature at a time, until the structure had to catch up.


When it was just a single file (May 17–30)

The first version was almost embarrassingly simple.

A single HTML file. No framework. No build system. Just direct calls to OpenClaw’s /v1/chat/completions endpoint and a bit of JavaScript to make it feel interactive.

It had just enough to feel real:

  • dark/light mode toggle using localStorage
  • a basic settings panel (API URL, model name, gateway config)
  • markdown rendering for responses
  • syntax highlighting for code blocks
  • streaming output that felt surprisingly responsive

It ran locally through:

python -m http.server

And for a moment, that was enough.

The one obvious limitation was also the one you could ignore at first: nothing persisted. Refresh the page, everything was gone. But at that stage, persistence wasn’t the point — proving the streaming pipeline was.


The moment it started growing out of control (June 1)

The problem wasn’t any single feature — it was that everything kept feeling “just one more step away” from being useful.

So features accumulated:

  • a “thinking…” indicator while waiting for first token
  • streaming cursor animation
  • copy buttons (both plain and formatted text)
  • export options (HTML / JSON / text)
  • inline chat renaming
  • light mode fixes caused by CSS variable timing issues
  • session persistence bugs from object rehydration mismatches

At the same time, the backend quietly stopped being a toy.

The original Python proxy started showing instability, so it was replaced with a Node.js server. That shift ended up being bigger than expected:

  • stable API routing
  • proper REST endpoints
  • static file serving
  • rate limiting for login attempts
  • MariaDB-backed session storage
  • bcrypt password hashing

At that point, the project stopped feeling like a frontend experiment and started feeling like a real system.

But the frontend was clearly reaching its limit. Everything lived together in the same place — streaming logic, UI state, DOM manipulation, and configuration handling all overlapping in ways that made every change slightly risky.


Vue 3 changes the way everything feels (Mid-June)

Moving to Vue wasn’t just a refactor — it changed the mental model of the entire app.

Instead of one growing script, there were now components:

  • Login handled authentication cleanly
  • Chat became an orchestrator instead of a dumping ground
  • routing replaced hash-based navigation
  • reactive state replaced manual DOM updates

Streaming responses were the first thing that immediately improved. Instead of manually pushing chunks into the DOM, Vue reacted to state changes naturally.

It wasn’t perfect — there were still rough edges:

  • thinking animation behavior needed adjustment
  • partial stream chunks occasionally rendered awkwardly
  • old imperative patterns kept bleeding into components

But the structure was finally strong enough that problems became fixable instead of structural.


When it stopped being single-user (June 12–17)

Authentication changed the shape of the project.

What started as “just let me log in” turned into a full user system:

  • session cookies (HTTP-only)
  • bcrypt password hashing
  • rate limiting on login attempts
  • MariaDB-backed auth sessions
  • per-user configuration

And with that, the UI started to shift subtly too.

“Model selection” stopped feeling like the right term. It became “Agent selection” — a small naming change, but it made the interface feel more aligned with how it was actually being used.

The UI also started consolidating. Export and print features that were previously scattered across the interface were pulled into a single clean dropdown in the header. Small change, but it reduced a lot of visual noise.


The point where refactoring became unavoidable (July 6)

By early July, Chat.vue had grown past 1600 lines.

At that point it wasn’t a question of whether to refactor — it was just a question of how painful it would be.

Everything lived in it:

  • message rendering
  • streaming logic
  • session management
  • input handling
  • header + sidebar coordination
  • UI state transitions

So the breakdown started slowly.

HeaderBar was extracted first — taking title editing, theme toggles, settings, logout, export, and user display with it. Then the input area. Then thinking indicator. Then sidebar components were split into smaller responsibilities.

Once that happened, something interesting became obvious: the complexity didn’t disappear — it just stopped colliding with itself.

Even utility logic followed the same pattern:

  • theme handling moved into theme.js
  • settings and persistence moved into settings.js

Nothing revolutionary — just finally separating things that had no business living together.


Small UX fixes that mattered more than expected (July 6 — current)

Once the structure stabilized, the changes became smaller but more noticeable.

Copy buttons inside messages were redesigned into subtle hover-only controls:

  • 📋 copy full message
  • 📝 copy formatted text
  • ✓ temporary confirmation feedback

Instead of taking up space, they now only appear when needed. The conversation stays visually clean, and the actions stay available without being intrusive.


The sidebar bug that revealed the real issue

One bug stood out because it felt like it shouldn’t have been a bug.

Renaming a chat from the header didn’t immediately update the sidebar.

Nothing was wrong with the API. The backend was updating correctly. The issue was simpler: the sidebar and header were not actually looking at the same reactive source.

Once the session object inside the shared array was mutated directly, Vue handled the rest automatically. No extra events, no forced refresh — just proper state flow.

It was one of those fixes that feels almost obvious in hindsight.


Where it ended up

As of July 6, 2026, the system has become a full-stack chat application:

Frontend (Vue 3)

  • Login system
  • Header controls
  • Chat orchestration layer
  • Markdown + streaming message renderer
  • Input system
  • Sidebar session manager

Backend

  • Node.js proxy server (REST + static hosting)
  • MariaDB session storage (JSON chat history)
  • bcrypt authentication system
  • OpenClaw gateway integration
  • llama.cpp backend (Qwen3.6-35B-A3B model)

Everything runs containerized with multiple services coordinated together.


Closing reflection

The project didn’t follow a planned architecture. It didn’t start with a design document or a long-term structure in mind.

It evolved through pressure.

Features got added because they were useful. Structure got introduced when the lack of it started to hurt. Refactoring happened when change became too expensive without it.

And the most consistent lesson throughout wasn’t about Vue, or Node, or even system design.

It was this:

You don’t really design systems like this in advance.
You discover their shape by pushing them until they break — and then reshaping them just enough so they stop breaking in the same way.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *