Skip to content
← All projects

AI · Business Intelligence · Helios Solutions · 2024 – 2026

SolBI

Ask a business question in plain English, then watch the answer and chart stream in.

Role
Software Engineer · Full stack
Timeline
At Helios Solutions, 2024 – 2026
Stack focus
Next.js, SSE, Plotly, React-Leaflet, Express, MongoDB
Outcome
Natural-language queries to streamed answers and interactive charts and maps, with saved dashboards.

Next.js, React, SSE, Plotly, React-Leaflet, Express, MongoDB, IndexedDB

01Overview

Ask in plain English, get a visual representation with AI insights

SolBI is a chat-driven business intelligence tool I worked on at Helios Solutions. A user types a business question in plain English and the answer streams back as it is generated. When the answer includes data, it turns into an interactive Plotly chart or a Leaflet map, and the user can save it to a dashboard.

Calling a model was the easy part. The hard part was the frontend: keeping the interface responsive while many small chunks arrive, deciding when partial data is safe to draw, and keeping sessions available when the network is not. I built the Next.js and React client and contributed to the Express and MongoDB backend behind it.

02Try it

Try it: a replay of the streaming answer

This is a recorded answer replayed through the same rendering rules, using mock data and no live model. Start it and watch the order: text appears token by token first, and the chart only appears once its structured payload has fully arrived and passed validation. Replay it and compare how it feels to an answer that shows nothing until it is done.

Loading streaming replay…

03Pipeline

From question to dashboard

SolBI streaming pipelineA plain-English question goes through a validated query editor to an Express API that stores the chat session in MongoDB and calls the LLM. The answer comes back as a Server-Sent Events stream, is published on a client-side mitt event bus, and renders progressively: text as it streams, then a Plotly chart or Leaflet map once the structured data validates. Answers can be saved as dashboards.BROWSERSERVERQuestionplain EnglishQuery editorvalidated before sendExpress APIsessions → MongoDBLLMtext + structured dataSSE streamtext/event-streammitt event bustoken + data eventsProgressive render1 · text, streaming2 · chart, once validSaved dashboardsMongoDB + IndexedDBBROWSERSERVER
The top row runs left to right, and the answer comes back right to left on the bottom row. The dashed line separates browser from server. On narrow screens the diagram scrolls sideways.

A question starts in the query editor, which validates it before anything is sent. The Express API records it in the chat session in MongoDB and calls the model.

The answer returns as a Server-Sent Events stream. On the client, the stream handler doesn't touch React state. It publishes each chunk on a mitt event bus, and only the message being written subscribes to it. Text renders as it arrives. Structured data for a chart or map is held back until it is complete and valid, and only then rendered with Plotly or React-Leaflet. A finished answer can be saved as a dashboard, and sessions are kept in IndexedDB so they reopen quickly and survive a dropped connection.

04Decisions

Five decisions behind the stream

Decision 01

Server-Sent Events, not WebSockets

The question goes up once, and the answer comes down as a stream. Nothing needs to travel from client to server while the answer is being written.

Chose

SSE: a long-lived HTTP response with the text/event-stream content type. Express writes events as the model produces them, and the browser reads them over plain HTTP.

Rejected

WebSockets. They give a two-way channel we had no use for, plus a separate connection lifecycle, reconnection logic and proxy setup to own.

Trade-off · SSE only goes one way, so cancelling an answer is a separate request. The native EventSource API only makes GET requests without custom headers, which affects how the question and auth are sent. On HTTP/1.1, open streams also count against the browser's per-host connection limit.

Decision 02

Tokens on an event bus, not in Redux

App state such as sessions, dashboards and settings lived in Redux Toolkit. A streamed answer arrives as many small chunks, and dispatching each one would run every subscribed selector and re-render far more of the tree than the one message being written.

Chose

The SSE handler emits chunks on a mitt event bus. Only the streaming message subscribes and appends to its own local state. Redux receives the message once, when it is complete.

Rejected

Dispatching a Redux action per token. It is simple and easy to inspect in DevTools, and it causes a re-render storm on long answers.

Trade-off · While an answer is streaming there are two sources of truth, the bus and the store, and stream events don't appear in Redux DevTools. Subscribers have to unsubscribe on unmount, or a closed chat keeps listening.

Decision 03

Progressive render: text first, chart only when valid

One answer can carry both prose and structured data for a chart or map. The prose is useful immediately. Half of a chart specification is not.

Chose

Render text as it streams. Hold the structured payload until it is complete and passes validation, then mount the Plotly chart or Leaflet map.

Rejected

Waiting for the full response before showing anything, which leaves a blank screen. Or drawing the chart from partial data, which makes axes jump and series appear and vanish.

Trade-off · The chart lands after the text, so the layout has to make room for it without pushing the user's reading position around. A payload that fails validation needs its own error state instead of a broken chart.

Decision 04

IndexedDB for sessions, not server-only

Chat sessions and their answers live in MongoDB. Loading every session from the server on each visit is slow for long histories, and a dropped connection would leave the user with an empty screen.

Chose

A local copy of sessions in IndexedDB. The UI opens from the local copy and reconciles with the server, which remains the source of truth.

Rejected

Server-only persistence. It is simpler, with one copy and no sync, but every reload is a network round trip and offline means nothing to show.

Trade-off · Two copies can disagree, especially across devices, so the reconcile rules matter more than the storage. IndexedDB storage limits and browsers that clear site data also mean the local copy is a cache, never the record.

Decision 05

A validated query editor alongside free-form chat

Free-form questions are flexible and often ambiguous. A vague or malformed question still costs a full model call and returns an answer the user can't trust.

Chose

A query editor that validates input before it is sent, used as a guardrail next to free-form chat rather than in place of it.

Rejected

Free-form chat only, which leaves the model to interpret every question and the user to spot when it guessed wrong.

Trade-off · It is more UI to build and keep aligned with the data the backend can actually answer about, and it can feel restrictive to experienced users. In return, bad questions fail fast and cost nothing.

05Backend

The API behind the chat

On the backend I contributed REST APIs and data models in Express and MongoDB for the two things users keep: chat sessions and saved dashboards. Modelling a session is a read-pattern question. A session is almost always loaded as a whole, which argues for embedding its messages in one document. The pressure in the other direction is MongoDB's document size limit on very long sessions, along with any query that needs individual messages across sessions.

Saved dashboards raise a similar question: store the rendered chart definition, a snapshot of the data, or the query to run again. Each one trades freshness against cost and against whether the dashboard looks the same the next time it is opened.

I also built localization for the interface and the PayPal payment integration.

06Next

What I'd do differently

  • Validate the payload on the server too. The client only draws a chart once the payload validates. Running the same schema on the server before the payload is sent would mean the client never receives a malformed chart spec at all.

  • Make streams resumable. SSE supports event IDs and Last-Event-ID on reconnect. Using them would let a dropped connection pick up mid-answer instead of starting over.

  • Make “stop” a first-class action. A clear way to cancel a long answer saves model cost and respects the user's time.

  • Measure what I argued for. I chose the event bus to avoid re-render storms, but I don't have production numbers I can quote. Next time I'd record time to first token and render cost per stream from the start.