AI Shopping Assistant Demo

May 21, 2026 · View on GitHub

A fully self-contained Flutter demo that showcases every major feature of agentivity_ag_ui in a single, polished dark-themed screen — no backend required.


What does this demo actually do?

When you open the app the AI agent auto-plays a scripted run. Watch it:

StepWhat you seeAG-UI feature
1A progress bar appears: "Scanning your preferences…"ACTIVITY_SNAPSHOT
2A collapsible "Thinking…" chip streams inREASONING_MESSAGE_*
3Two tool-call cards animate through pending → running → completedTOOL_CALL_START/END/RESULT
4Assistant text streams in word by wordTEXT_MESSAGE_*
5Three product cards pop in, one by oneCUSTOM ag-ui:render
6A call-to-action text streams inTEXT_MESSAGE_*
7A purple "Tap to choose" banner slides upRUN_FINISHED with interrupt
8A bottom sheet lists the 3 products — you tap oneHuman-in-the-loop
9The cart bar bounces at the bottom with the item and totalSTATE_DELTAAgUiStateController
10A confirmation message streams inResume run

Hit the ↺ replay button in the top-right to run it again.


Do I still need to build the UI myself?

Yes — and that's the whole point.

The library does not ship domain-specific widgets. There is no ProductCard in the package, just as there is no DoctorAppointmentCard, FlightResultCard, or InvoiceLineItem. Those are your design, your brand.

What the library gives you is the plumbing between the agent and your widgets:

Your Agent (backend)          agentivity_ag_ui              Your Flutter UI
─────────────────────    ──────────────────────────    ──────────────────────
Emit SSE events          Parse & route every event     Render what the agent
                         Reconnect on drop              decided to show
TOOL_CALL_START      →   AgUiToolCallItem (card)    →  ToolCallCard (built-in)
TEXT_MESSAGE_*       →   AgUiTextItem               →  Chat bubble (built-in)
CUSTOM ag-ui:render  →   Look up "ProductCard"      →  YOUR ProductCard widget
STATE_DELTA          →   Apply JSON Patch            →  YOUR CartBar re-renders
RUN_FINISHED(int.)   →   isInterrupted = true       →  YOUR bottom sheet

You register one line per component in AgUiWidgetRegistry:

final registry = AgUiWidgetRegistry({
  'ProductCard': (props) => ShoppingProductCard(
    product: productById(jsonStr(props, 'id'))!,
    onAddToCart: () => _showPicker(),
  ),
});

From that point on, every time the agent emits:

{ "type": "CUSTOM", "name": "ag-ui:render",
  "value": { "component": "ProductCard", "props": { "id": "p2" } } }

…your ShoppingProductCard widget appears in the conversation — with exactly the data the agent chose, at exactly the moment the agent decided to show it.

The agent controls the when and what. You control the how it looks.


Why is this more than a regular app?

In a conventional app, the UI flow is hardcoded:

// Traditional approach — you decide everything at compile time
if (searchResults.length > 0) {
  showProductList(searchResults.take(3).toList());
}

With agentivity_ag_ui, the agent decides at runtime:

  • Show 3 products, or 1, or a comparison table, or a bundle offer — whatever its reasoning concluded was best for this user at this moment.
  • Interrupt the flow to ask for approval before proceeding.
  • Update the cart state atomically via JSON Patch without a page reload.
  • Stream its reasoning so users understand why they're seeing what they see.
  • Retry gracefully if the SSE connection drops mid-sentence.

None of that logic lives in your Flutter code. It lives in the agent. Your UI stays thin and declarative; the agent stays in control of the experience.


Code map

example_shopping/
├── lib/
│   ├── main.dart                    # Dark theme, app entry point
│   ├── data/
│   │   ├── products.dart            # ShoppingProduct model + kProducts list
│   │   └── demo_stream.dart         # Scripted AG-UI event sequences
│   │       ├── buildDemoStream()    # Phase 1: discovery → interrupt
│   │       └── buildResumeStream()  # Phase 2: cart update → confirmation
│   ├── screens/
│   │   └── home_screen.dart         # All 4 controllers wired in parallel
│   └── widgets/
│       ├── product_card.dart        # ShoppingProductCard (YOUR widget)
│       └── cart_bar.dart            # AnimatedCartBar (YOUR widget)

demo_stream.dart — the scripted agent

The demo has no live backend. buildDemoStream() is an async* generator that yields AgUiEvent objects with realistic delays, producing exactly the same event sequence a real LangGraph or CrewAI agent would emit.

This is how you develop and test your UI before your backend is ready.

home_screen.dart — four controllers, one stream

// One broadcast stream feeds all four controllers simultaneously.
// Each controller handles only the events it cares about.
final broadcast = buildDemoStream().asBroadcastStream();

_lifecycle = AgUiRunLifecycleController(events: broadcast); // RUN_* STEP_*
_activity  = AgUiActivityController(events: broadcast);     // ACTIVITY_*
_stateCtrl = AgUiStateController(events: broadcast);        // STATE_*
_genCtrl   = AgUiGenerativeController(events: broadcast);   // TEXT_* TOOL_* CUSTOM

The cart bar reads from _stateCtrl.state['itemCount'] and _stateCtrl.state['total']. When the agent emits a STATE_DELTA after the user picks a product, the JSON Patch is applied and the cart bar re-renders — without any manual state management code.

product_card.dart — your widget, agent's data

ShoppingProductCard is a plain Flutter widget. It does not know about AgUiGenerativeController or any AG-UI type. It receives a ShoppingProduct and an onAddToCart callback, exactly like any other widget in your app.

The binding happens only inside the AgUiWidgetRegistry closure in home_screen.dart. That's the seam between the agent world and your UI world.


Running the demo

cd example_shopping
flutter pub get
flutter run

No API keys, no backend, no environment variables needed.


Adapting this to your product

What to changeWhere
Your own widgetsReplace ShoppingProductCard / CartBar with your own
Real backend SSEReplace buildDemoStream() with AgUiSseChannel pointed at your endpoint
Your product dataReplace kProducts / productById() with your own model
Your state shapeThe STATE_SNAPSHOT / STATE_DELTA keys are yours to define
Interrupt handlingSwap the bottom sheet for your own approval UI

The controllers, the protocol, the streaming, the reconnection, the JSON Patch — all of that stays exactly as-is.


Further reading