genui_button
A GenUI-compatible button widget package, giving AI agents the ability to generate highly styled buttons connected to your app's interactions.
Features
- Provides a
GenUIButtonwidget defined as a GenUICatalogItem. - Theme Support: Seamlessly falls back to your app's Material 3
ThemeDataif properties are absent! - Deep UI Control: Supports full width layout, custom tooltips, shadowing, explicit border colors and widths.
- Typographic Control: Explicit font weight declarations and letter spacing customizations.
- Material Styles: Supports
elevated,filled,outlined, andtextvisual button types. - Integrates flawlessly with
genuiproperties to accept a label string and action ID payload. - Out-of-the-box
UserActionEventtriggering when tapping. - Handled Accessibility out-of-the-box with
Semantics.
Getting started
In your pubspec.yaml, add the dependency:
dependencies:
flutter:
sdk: flutter
genui_button: ^0.0.6
Usage
Simply register genUiButton with your GenUI catalogues:
import 'package:genui_button/genui_button.dart';
import 'package:genui/genui.dart';
final a2uiMessageProcessor = A2uiMessageProcessor(
catalogs: [
CoreCatalogItems.asCatalog().copyWith([genUiButton])
],
);
Then your FirebaseAiContentGenerator (or similar generator) system instruction can tell the agent to use GenUIButton globally.
final contentGenerator = FirebaseAiContentGenerator(
systemInstruction: 'Use GenUIButton to provide user actions with custom styling.',
additionalTools: a2uiMessageProcessor.getTools(),
);
Whenever the agent generates a UI containing a button, it can trigger UserActionEvent actions back to the app logic that you configure.
The AI can also deeply customize the button's look dynamically using the schema:
type:elevated,filled,outlined,textbackgroundColor:#FF5733foregroundColor:#FFFFFFborderColor:#000000borderWidth:1.5shadowColor:#FF0000borderRadius:8.0elevation:4.0padding:16.0fontSize:18.0fontWeight:bold,normal,w700letterSpacing:1.2gradientColors:"#FF0000, #00FF00"(Comma separated linear gradient)fullWidth:trueisLoading:true(Disables button and shows spinner)tooltip:"Click me to proceed!"data:{"id": 123}(Static payload parameters)contextMapping:{"user": "session/user_id"}(Dynamic context collection from DataContext)debounceMs:500(Debounce duration in milliseconds)onLongPressAction:"secondary_action"(Alternative action on long press)onDoubleClickAction:"like_action"(Action on double click/tap)onHoverEnterAction:"show_preview"(Action on mouse hover enter)onHoverExitAction:"hide_preview"(Action on mouse hover exit)confirmation:{"title": "Warning!", "message": "Delete?"}(Confirmation dialog setup)
Realistic Action Handling
GenUIButton supports advanced interaction patterns to build production-ready AI UIs:
Context & Data Mapping
You can now map button actions to specific keys in the GenUI DataContext. This allows the button to "collect" state from other components (like text fields or checkboxes) that have bound their values to the context.
{
"action": "submit_form",
"contextMapping": {
"email": "form/email_field",
"accept_terms": "form/terms_accepted"
}
}
Native Confirmation Dialogs
For destructive or important actions, you can configure a native confirmation dialog without any extra logic on the app side:
{
"action": "clear_data",
"confirmation": {
"isEnabled": true,
"title": "Clear all data?",
"message": "This action is permanent and cannot be reversed.",
"confirmText": "Delete All",
"cancelText": "Keep My Data",
"onConfirmAction": "explicitly_confirmed_delete",
"onCancelAction": "user_cancelled_delete"
}
}
Key confirmation properties:
isEnabled: Set tofalseto conditionally disable the dialog.onConfirmAction: Override the defaultactionwhen the user confirms.onCancelAction: Trigger a specific event if the user cancels.
### Handling Actions (Developer Guide)
When a developer uses this package, the button triggers a `UserActionEvent`. Here is the best way to handle these actions in your Flutter app using a `GenUiSurface`:
```dart
GenUiSurface(
message: lastAiMessage,
onEvent: (UiEvent event) {
if (event is UserActionEvent) {
// 1. Identify the action triggered by the AI
final actionName = event.name;
// 2. Access the data payload (static data + context mapping)
final payload = event.context;
switch (actionName) {
case 'submit_order':
_processOrder(payload['order_id'], payload['user_email']);
break;
case 'user_confirmed_delete':
_performDeletion(payload['id']);
break;
case 'analytics_cancel':
_logCancellation(payload['reason']);
break;
}
}
},
)
Advanced Confirmation Logic
The confirmation object allows for interactive safety checks before an action is dispatched.
| Property | Description |
|---|---|
isEnabled |
(Boolean) If false, the dialog is skipped and the action runs immediately. |
onConfirmAction |
(String) Overrides the primary action specifically when the user confirms. |
onCancelAction |
(String) A separate event to dispatch if the user clicks "Cancel". |
Conditional Confirmation Example
The AI can decide to only show a confirmation if a certain condition is met (e.g., "Are you sure you want to exit without saving?"):
{
"action": "exit_screen",
"confirmation": {
"isEnabled": true,
"title": "Unsaved Changes",
"message": "You have unsaved work. Exit anyway?",
"confirmText": "Exit",
"onConfirmAction": "FORCE_EXIT_CONFIRMED",
"onCancelAction": "STAY_ON_PAGE"
}
}
Handling Local Actions
This guide explains how to intercept and handle custom actions from AI-generated UI components (like buttons) using the GenUI framework. This is essential for executing local Flutter logic—such as navigation, showing SnackBars, or calling local APIs—without requiring a round-trip to the AI model.
Overview
When a user interacts with a component (e.g., clicking a GenUIButton), a UserActionEvent is dispatched. By default, this event is serialized to JSON and sent to the AI so it can generate the next turn of the conversation.
However, you often want to run Predefined Local Actions. There are two primary ways to do this.
Method 1: Subclassing A2uiMessageProcessor (Recommended)
Subclassing the A2uiMessageProcessor is the cleanest and most robust approach for "Local-Only" actions. It allows you to intercept specific events and prevent them from being sent to the AI.
1. Define your custom Processor
Create a class that extends A2uiMessageProcessor and override the handleUiEvent method.
import 'package:genui/genui.dart';
class MyLocalActionProcessor extends A2uiMessageProcessor {
MyLocalActionProcessor({required super.catalogs});
@override
void handleUiEvent(UiEvent event) {
if (event is UserActionEvent) {
// Logic for specific local actions
if (event.name == 'local:show_toast') {
_handleShowToast(event.context['message']);
// Return early to "consume" the event.
// It will NOT be sent to the AI.
return;
}
if (event.name == 'local:navigate') {
_handleNavigation(event.context['route']);
return;
}
}
// Forward all other events to the AI
super.handleUiEvent(event);
}
void _handleShowToast(dynamic message) {
print('Toast: $message');
}
void _handleNavigation(dynamic route) {
print('Navigating to: $route');
}
}
2. Implementation in your State
Use your custom processor instead of the default one when initializing your GenUiConversation.
@override
void initState() {
super.initState();
final catalog = CoreCatalogItems.asCatalog().copyWith([genUiButton]);
// Use your subclassed processor
final processor = MyLocalActionProcessor(catalogs: [catalog]);
_conversation = GenUiConversation(
a2uiMessageProcessor: processor,
contentGenerator: _generator,
// ...
);
}
Method 2: Listening to the onSubmit Stream
Use this method if you want the local action to occur, but you also want the AI to see the event so it can respond textually (e.g., "I've updated your settings as requested").
final processor = A2uiMessageProcessor(catalogs: [catalog]);
// Listen to the stream of UI events sent to the AI
processor.onSubmit.listen((message) {
// The 'message.text' contains a JSON string of the UserActionEvent
if (message.text.contains('my_predefined_action')) {
print('Local logic executed, and the AI is processing this too!');
}
});
Handling Data and Payloads
When the AI generates a button, it can pass data to your local action through the data and contextMapping properties of the GenUIButton.
Accessing Static Data
In your processor, access the event.context map:
if (event.name == 'local:add_item') {
final itemId = event.context['id'];
final quantity = event.context['qty'] ?? 1;
// Run your logic: cartService.add(itemId, quantity);
}
Prompting the AI
To ensure the AI uses your local actions correctly, include them in your System Instructions:
PREDEFINED LOCAL ACTIONS:
- 'local:show_snackbar': Displays a notification. Requires 'message' in data.
- 'local:navigate_to_profile': Opens the user profile.
Best Practices
Tip
Namespace your actions: Use a prefix like local: (e.g., local:save_draft) to clearly distinguish between actions handled by Flutter and actions handled by the AI.
Important
Use Context Mapping: If you need real-time app state in your action (like a userID stored in a DataContext), tell the AI to use contextMapping. This ensures the event payload contains the current value of that variable.
Caution
Memory Management: If using Method 2, remember to cancel your stream subscription in the dispose() method to avoid memory leaks. Subclassing (Method 1) is generally safer as it follows the lifecycle of the processor itself.
Example Application
See the example/ folder for a more comprehensive Flutter project demonstrating how genui_button parses styling under the hood, including its interactions with parent themes!
Libraries
- A highly customizable, GenUI-compatible button widget package.