Show Product Cards While Your Flutter Voice AI Talks
A voice assistant that reads a product name out loud is a demo. A voice assistant that shows the product while it talks is a store. When someone asks "what running shoes do you have under $120?", the honest answer isn't a 15-second spoken monologue listing SKUs — it's three tappable cards on screen, each with an image, price, and an Add to Cart button, appearing while the assistant describes them out loud.
Voice-only replies are a dead end for shopping and support. Nobody memorizes a spoken price. Nobody buys a shoe they can't see. This post shows how to make your Flutter voice assistant show product cards on screen during a live voice call using WidgetChat — the same widget you already embed for text chat — so users can talk and tap at the same time, and interrupt (barge-in) whenever they want.
Why speech-to-speech alone breaks shopping
Modern voice AI is genuinely good now: real-time speech-to-speech, sub-second barge-in (the user interrupts and the assistant stops talking mid-sentence), and streaming ASR that keeps the conversation feeling live. That solves the conversation. It does not solve the catalog.
Three things go wrong when the reply is audio-only:
- No visual anchor. Prices, colors, and sizes evaporate the instant they're spoken.
- No tap target. "Add the blue one" requires a second round-trip when a button would take one tap.
- No comparison. Users shop by scanning options side by side, not by holding four of them in working memory.
The fix is a multimodal voice assistant in Flutter: keep the voice channel for natural conversation, and push structured product data to an on-screen card slot at the same time. WidgetChat's live voice chat is built for exactly this — it can show rich product cards on screen while it speaks, with live captions, inside the same widget and the same conversation as your text chat.
The architecture: one session, two output channels
Think of a single voice session with two synchronized outputs:
- Audio — the assistant's spoken reply, generated server-side. Your provider API keys stay on the server; nothing sensitive ships in the app binary.
- On-screen — a caption stream plus a card slot your Flutter UI owns and renders.
The assistant answers from your own content, so the products it surfaces are real catalog items, not hallucinations. Your job on the client is just to render whatever structured card data the session emits into a Flutter widget — a classic server-driven-UI pattern. You do not assemble a separate STT/TTS stack, wire up a WebSocket audio pipeline, or manage VAD for barge-in. That's the point of using the voice widget: the hard real-time audio work is already done.
Step 1 — Turn on voice in the dashboard
Before any code, enable voice for your project. In the WidgetChat dashboard's Voice section you configure, per project:
- Enable/disable voice for the widget
- Voice name (which spoken voice to use)
- Max session length
- Captions default (whether live captions show automatically)
Voice is plan-gated by a monthly voice-minute pool, so set your max session length sensibly for a shopping flow — long enough to browse, short enough to protect your minute budget. Turning on "captions default" here is what makes the live captions appear during the call without extra client code.
Step 2 — Build the on-screen card slot
This is pure Flutter and it's the part you own. Define a small model and a card widget, then render a list of them into a slot that sits above or beside the mic. Here's a self-contained ProductCard that maps structured product data into a tappable card:
class Product {
final String id, title, imageUrl;
final String priceLabel;
Product.fromJson(Map<String, dynamic> j)
: id = j['id'] as String,
title = j['title'] as String,
imageUrl = j['image_url'] as String,
priceLabel = j['price'] as String;
}
class ProductCard extends StatelessWidget {
final Product product;
final VoidCallback onAdd;
const ProductCard({super.key, required this.product, required this.onAdd});
@override
Widget build(BuildContext context) {
return Card(
clipBehavior: Clip.antiAlias,
child: SizedBox(
width: 160,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AspectRatio(
aspectRatio: 1,
child: Image.network(product.imageUrl, fit: BoxFit.cover),
),
Padding(
padding: const EdgeInsets.all(8),
child: Text(product.title, maxLines: 2,
overflow: TextOverflow.ellipsis),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Text(product.priceLabel,
style: const TextStyle(fontWeight: FontWeight.bold)),
),
TextButton(onPressed: onAdd, child: const Text('Add to cart')),
],
),
),
);
}
}
Hold the current cards in state and render them in a horizontal strip that lives on top of the voice UI, so the user can scroll and tap while the assistant keeps talking:
class CardSlot extends StatelessWidget {
final List<Product> products;
final void Function(Product) onAdd;
const CardSlot({super.key, required this.products, required this.onAdd});
@override
Widget build(BuildContext context) {
if (products.isEmpty) return const SizedBox.shrink();
return SizedBox(
height: 260,
child: ListView.separated(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.all(12),
itemCount: products.length,
separatorBuilder: (_, __) => const SizedBox(width: 12),
itemBuilder: (_, i) => ProductCard(
product: products[i],
onAdd: () => onAdd(products[i]),
),
),
);
}
}
Because this slot is your own widget tree, tapping a card never touches the audio session — the call keeps running, and barge-in still works if the user starts talking again.
Step 3 — Feed the slot from the conversation
WidgetChat is the same conversation and same dashboard across text and voice, and it integrates over a plain HTTP client — no proprietary SDK required. The text side streams token-by-token over Server-Sent Events, which is a clean way to see the shape of what the assistant returns and to update the same card slot from typed queries. The streaming endpoint is POST https://api.widgetchat.app/v1/chat/stream, returning data: SSE lines:
import 'dart:convert';
import 'package:http/http.dart' as http;
Stream<Map<String, dynamic>> streamReply(String userText) async* {
final req = http.Request(
'POST', Uri.parse('https://api.widgetchat.app/v1/chat/stream'),
)
..headers['Authorization'] = 'Bearer $widgetChatKey'
..headers['Content-Type'] = 'application/json'
..body = jsonEncode({'message': userText, 'session_id': sessionId});
final res = await http.Client().send(req);
final lines = res.stream
.transform(utf8.decoder)
.transform(const LineSplitter());
await for (final line in lines) {
if (!line.startsWith('data:')) continue;
final payload = line.substring(5).trim();
if (payload.isEmpty || payload == '[DONE]') continue;
yield jsonDecode(payload) as Map<String, dynamic>;
}
}
The exact request fields for your project are shown in your dashboard — treat the body above as the shape, not gospel. What's guaranteed is the token-by-token
data:SSE response you loop over here.
When a chunk carries product data, map it into your Product model and push it into the same CardSlot state you built in Step 2. During a live voice call, the identical slot fills in while the assistant speaks — captions handle the words, the cards handle the merchandise. One rendering path, two entry points (mic and keyboard).
Why this beats rolling your own
You could assemble STT + an LLM + TTS + a barge-in VAD loop yourself. You'd then spend weeks tuning false-barge-in (coughs and background music tripping the interrupt), keeping captions in sync with audio, and hiding provider keys. WidgetChat ships all of that live today — speech-to-speech with barge-in, live captions, on-screen product cards, server-side keys — across iOS, Android, and web Flutter apps, from the same widget and dashboard you already use for text.
For FlutterFlow builders, the same idea applies: wire the streaming call as a custom action, keep the card slot as a component, and flip on voice in the dashboard. No SDK to import, no audio plumbing to maintain — a real FlutterFlow voice AI visual response with cards that appear as it talks.
Try WidgetChat free
Give your shoppers a voice assistant that shows, not just tells. Try WidgetChat free — embed the widget, turn on voice in the dashboard, and let it render live product cards on screen while it speaks.





Comments
Comments are coming soon. We'd love to hear your thoughts!