Real-Time Generative User Interface Rendering & Processing Engine
A highly modular, event-driven generative UI layout engine for Flutter applications. Designed to decouple AI transport layers from front-end widget trees, it handles namespaced token streams, compiles partial JSON component schemas via reactive builders, manages multi-view target-routing tags, and dynamically activates passive inputs as streams finalize.
The Concept
Building generative AI interfaces usually involves tightly coupling LLM prompts, stream parsing logic, state management, and widget rendering. We wanted to decouple this entirely. The StreamingGenUi controller provides a clean boundary: we hand the developer the system prompt fragment, they manage their own LLM calls, and then they simply pipe the raw response stream back into the controller.
Schematic of the decoupled architecture: Developer provides the stream, Controller targets the View ID.
Syntax and Usage
The API is designed for minimal boilerplate and maximum flexibility. Views can be dynamically requested and populated with AI-generated widget trees across your entire application.
The standard layout for streaming and rendering. Set up the controller, pipe the stream in, save raw data on completion, and place the view anywhere in your widget tree.
// Setup
final genUi = StreamingGenUi(registry: myRegistry);
// Inject the prompt fragment into your LLM system prompt however you want
final systemPrompt = genUi.systemPrompt;
// Pipe the LLM response stream in, save raw response on completion
await genUi.stream(
llmStream,
viewId: 'message-42',
onComplete: (raw) => db.save(raw),
);
// Restore a past response from DB
genUi.restore(viewId: 'message-42', raw: db.load('message-42'));
// Place the view anywhere in your widget tree
genUi.view('message-42')
// Cleanup when permanently gone
genUi.disposeView('message-42'); The complete API layout showcasing custom text callbacks, multi-view target tag-routing, text streaming hooks, and component lifecycle management.
// Setup
final genUi = StreamingGenUi(registry: myRegistry);
// System prompt fragment to inject into your LLM
final systemPrompt = genUi.systemPrompt;
// Stream — all parameters
await genUi.stream(
llmStream,
viewId: 'side-panel', // optional — omit if only using callbacks
onText: (chunk) => ..., // text portions only, tags stripped, as they stream
onComplete: (raw) => ..., // full raw response (text + tags) for DB storage
);
// Multiple views from one response via <interface viewId="..."> tags
// The LLM can target any mounted view by its ID in the tag itself
await genUi.stream(
llmStream,
viewId: 'message-42', // default target for untagged <interface> blocks
onText: (chunk) => ...,
onComplete: (raw) => db.save(raw),
);
// Restore from saved raw response
genUi.restore(viewId: 'message-42', raw: savedRawString);
// Place views anywhere in the widget tree
genUi.view('side-panel') // Flowserract side panel case
genUi.view('message-42') // Chat bubble case
genUi.view('global-modal') // Global action case
// Cleanup
genUi.disposeView('message-42'); Separation of Concerns
This utility establishes clear responsibilities to ensure the developer maintains full control over the application's networking layer and conversational state.
The Transport Layer
You handle registering the initial UI components, maintaining the chat history state, calling the actual LLM API, and rendering the fallback text states.
The Parsing & Piping
We provide the system prompt instructions, parse the AI's complex stream responses, and automatically pipe the successfully parsed generative UI directly to the requested view ID.
Basically: We give them a prompt fragment, they share the response and which view we pipe the UI to, and we display the UI of a view ID requested.
Streaming View & Target Routing
When the LLM streams its response, the model can seamlessly embed a dynamic UI widget tree by enclosing a valid JSON payload within custom <interface> and </interface> tags. The engine isolates these tagged blocks to render the visual UI components on-the-fly, supporting two distinct routing modes:
Single-View / Inline Chat Mode
If the model streams an untagged <interface> block, all components are rendered inline. The default .view('message-42') widget handles the entire sandwich layout, automatically converting standard text portions to Markdown and wrapping the dynamic widget tree smoothly.
Multi-View / Target-Routed Mode
If the model streams an interface tag specifying a view (e.g., <interface viewId="side-panel">), the default view only displays text while the target view container (mounted elsewhere via genUi.view('side-panel')) dynamically catches and builds the layout.
* Graceful fallback: If an unknown widget is encountered, developers can supply a custom onUnknownWidget handler, which defaults to displaying a descriptive error layout.
JSON Protocol & ID Registry
Rather than using complex nested schemas that are highly prone to hallucination, the engine uses flat maps for each widget, allowing nesting to represent clean hierarchy structure.
// Nested column and text widget layout
{
"namespace": "core:column",
"children": [
{
"namespace": "core:text",
"text": "Do not press the button!"
},
{
"namespace": "core:elevated_button",
"child": {
"namespace": "core:text",
"text": "The button."
}
}
]
}
To resolve prompts overlapping and duplicate entries, built-in and custom widgets are registered with a strict namespacing ID system resembling a Minecraft-like format:
<provider>:name_in_snake_case
All built-in system widgets use the core provider. The Alpha V1 Built-In Widget Registry contains: Text, Button, Column, Row, Container, and Textfield.
Streaming Generative Architecture
Powered by llm_json_stream, the system rejects static decoding, building widgets progressively token-by-token. This updates the .register() signature to feed property streams:
void register(String namespace, Widget Function(MapPropertyStream mapStream) builder) Hierarchical Prop Passing
Sub-streams are extracted (MapPropertyStream / ListPropertyStream) and passed recursively. Parents immediately mount child layouts, allowing child nodes to update self-reactively on their own streams without blocking layout.
Accumulating String Builder
To eliminate jarring UI jumps or frame drops during parsing, text elements use an accumulating string builder that grows progressively on every raw text token chunk, maintaining perfect conversational context.
Dynamic Action Activation
Form buttons paint greyed out and disabled. The moment the LLM streams the action payload, the callback resolves the future (mapStream.getStringProperty('action').future), activating and transitioning the button dynamically.
Centralized Interactions
Interactions (like form submissions or button clicks) are mapped back to the app layer using a centralized action dispatcher. Developers register active callbacks on the controller to receive real-time updates.