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.
03Pipeline
From question to dashboard
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
text/event-stream content type. Express writes events as the model produces them, and the browser reads them over plain HTTP.Rejected
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
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
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
Rejected
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
Rejected
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
Rejected
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-IDon 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.