+
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 18 additions & 2 deletions examples/catalog_gallery/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,25 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:flutter_genui/flutter_genui.dart';

void main() {
runApp(CatalogGalleryApp(CoreCatalogItems.asCatalog()));
}

class CatalogGalleryApp extends StatelessWidget {
class CatalogGalleryApp extends StatefulWidget {
const CatalogGalleryApp(this.catalog, {super.key});

final Catalog catalog;

@override
State<CatalogGalleryApp> createState() => _CatalogGalleryAppState();
}

class _CatalogGalleryAppState extends State<CatalogGalleryApp> {
@override
Widget build(BuildContext context) {
return MaterialApp(
Expand All @@ -25,7 +32,16 @@ class CatalogGalleryApp extends StatelessWidget {
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: const Text('Catalog items that has "exampleData" field set'),
),
body: DebugCatalogView(catalog: catalog),
body: DebugCatalogView(
catalog: widget.catalog,
onSubmit: (message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('User action: ${jsonEncode(message.parts.last)}'),
),
);
},
),
),
);
}
Expand Down
6 changes: 4 additions & 2 deletions examples/simple_chat/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';

import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter_genui/flutter_genui.dart';
Expand Down Expand Up @@ -141,7 +143,7 @@ class _ChatScreenState extends State<ChatScreen> {
);
}

Future<void> _sendMessage() async {
void _sendMessage() {
final text = _textController.text;
if (text.isEmpty) {
return;
Expand All @@ -154,7 +156,7 @@ class _ChatScreenState extends State<ChatScreen> {

_scrollToBottom();

await _uiAgent.sendRequest(UserMessage([TextPart(text)]));
unawaited(_uiAgent.sendRequest(UserMessage([TextPart(text)])));
}

void _scrollToBottom() {
Expand Down
6 changes: 5 additions & 1 deletion examples/travel_app/IMPLEMENTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,9 +102,13 @@ This is the collection of predefined UI components that the AI can use to constr
- `TravelCarousel`: For displaying a horizontal list of selectable options.
- `ItineraryWithDetails`, `ItineraryDay`, `ItineraryEntry`: For building structured travel plans.
- `InputGroup`: A container for grouping various input widgets.
- `OptionsFilterChipInput`, `CheckboxFilterChipsInput`, `TextInputChip`: Different types of input chips for user selections.
- `OptionsFilterChipInput`, `CheckboxFilterChipsInput`, `TextInputChip`, `DateInputChip`: Different types of input chips for user selections.
- `InformationCard`: For displaying detailed information about a topic.
- `Trailhead`: For suggesting follow-up prompts to the user.
- `ListingsBooker`: For displaying a list of bookings to checkout.
- `PaddedBodyText`: For displaying a block of text with padding.
- `SectionHeader`: For displaying a section header.
- `TabbedSections`: For displaying a set of tabbed sections.
- **Standard Components**: It also uses standard, pre-built components from `flutter_genui` like `column`, `text`, `image`, etc.

## Data Flow: The Generative UI Cycle
Expand Down
4 changes: 2 additions & 2 deletions examples/travel_app/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ All of the UI is generated dynamically and streamed into a chat-like view, creat
This example highlights several core concepts of the `flutter_genui` package:

- **Dynamic UI Generation**: The entire user interface is constructed on-the-fly by the AI based on the conversation.
- **Component Catalog**: The AI builds the UI from a custom, domain-specific catalog of widgets defined in `lib/src/catalog.dart`. This includes widgets like `travel_carousel`, `itinerary_item`, and `options_filter_chip`.
- **System Prompt Engineering**: The behavior of the AI is guided by a detailed system prompt located in `lib/main.dart`. This prompt instructs the AI on how to act like a travel agent and which widgets to use in various scenarios.
- **Component Catalog**: The AI builds the UI from a custom, domain-specific catalog of widgets defined in `lib/src/catalog.dart`. This includes widgets like `TravelCarousel`, `ItineraryEntry`, and `OptionsFilterChipInput`.
- **System Prompt Engineering**: The behavior of the AI is guided by a detailed system prompt located in `lib/src/travel_planner_page.dart`. This prompt instructs the AI on how to act like a travel agent and which widgets to use in various scenarios.
- **Dynamic UI State Management**: The `GenUiManager` from `flutter_genui` handles the state of the dynamically generated UI surfaces, manages the widget tree, and processes events between the user and the AI. The application's main page (`TravelPlannerPage`) manages the overall conversation history.
- **Firebase Integration**: The application is configured to use Firebase for backend services, as shown in `lib/firebase_options.dart`.

Expand Down
36 changes: 28 additions & 8 deletions examples/travel_app/lib/src/catalog/input_group.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,31 @@ final _schema = S.object(
'be input types such as OptionsFilterChipInput.',
items: S.string(),
),
'action': A2uiSchemas.action(
description:
'The action to perform when the submit button is pressed. '
'The context for this action should include references to the values '
'of all the input chips inside this group, so that the model can '
'know what the user has selected.',
),
},
required: ['submitLabel', 'children'],
required: ['submitLabel', 'children', 'action'],
);

extension type _InputGroupData.fromMap(Map<String, Object?> _json) {
factory _InputGroupData({
required JsonMap submitLabel,
required List<String> children,
required JsonMap action,
}) => _InputGroupData.fromMap({
'submitLabel': submitLabel,
'children': children,
'action': action,
});

JsonMap get submitLabel => _json['submitLabel'] as JsonMap;
List<String> get children => (_json['children'] as List).cast<String>();
JsonMap get action => _json['action'] as JsonMap;
}

/// A container widget that visually groups a collection of input chips.
Expand Down Expand Up @@ -110,6 +120,10 @@ final inputGroup = CatalogItem(
);

final children = inputGroupData.children;
final actionData = inputGroupData.action;
final actionName = actionData['actionName'] as String;
final contextDefinition =
(actionData['context'] as List<Object?>?) ?? <Object?>[];

return Card(
color: Theme.of(context).colorScheme.primaryContainer,
Expand All @@ -128,13 +142,19 @@ final inputGroup = CatalogItem(
valueListenable: notifier,
builder: (context, submitLabel, child) {
return ElevatedButton(
onPressed: () => dispatchEvent(
UiActionEvent(
widgetId: id,
eventType: 'submit',
value: {},
),
),
onPressed: () {
final resolvedContext = resolveContext(
dataContext,
contextDefinition,
);
dispatchEvent(
UserActionEvent(
actionName: actionName,
sourceComponentId: id,
context: resolvedContext,
),
);
},
child: Text(submitLabel ?? ''),
style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(context).colorScheme.primary,
Expand Down
42 changes: 35 additions & 7 deletions examples/travel_app/lib/src/catalog/itinerary_entry.dart
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,12 @@ final _schema = S.object(
'is confirmed.',
enumValues: ItineraryEntryStatus.values.map((e) => e.name).toList(),
),
'choiceRequiredAction': A2uiSchemas.action(
description:
'The action to perform when the user needs to make a choice. '
'This is only used when the status is "choiceRequired". The context '
'for this action should include the title of this itinerary entry.',
),
},
required: ['title', 'bodyText', 'time', 'type', 'status'],
);
Expand All @@ -65,6 +71,7 @@ extension type _ItineraryEntryData.fromMap(Map<String, Object?> _json) {
JsonMap? totalCost,
required String type,
required String status,
JsonMap? choiceRequiredAction,
}) => _ItineraryEntryData.fromMap({
'title': title,
if (subtitle != null) 'subtitle': subtitle,
Expand All @@ -74,6 +81,8 @@ extension type _ItineraryEntryData.fromMap(Map<String, Object?> _json) {
if (totalCost != null) 'totalCost': totalCost,
'type': type,
'status': status,
if (choiceRequiredAction != null)
'choiceRequiredAction': choiceRequiredAction,
});

JsonMap get title => _json['title'] as JsonMap;
Expand All @@ -86,6 +95,8 @@ extension type _ItineraryEntryData.fromMap(Map<String, Object?> _json) {
ItineraryEntryType.values.byName(_json['type'] as String);
ItineraryEntryStatus get status =>
ItineraryEntryStatus.values.byName(_json['status'] as String);
JsonMap? get choiceRequiredAction =>
_json['choiceRequiredAction'] as JsonMap?;
}

final itineraryEntry = CatalogItem(
Expand Down Expand Up @@ -132,8 +143,10 @@ final itineraryEntry = CatalogItem(
totalCostNotifier: totalCostNotifier,
type: itineraryEntryData.type,
status: itineraryEntryData.status,
choiceRequiredAction: itineraryEntryData.choiceRequiredAction,
widgetId: id,
dispatchEvent: dispatchEvent,
dataContext: dataContext,
);
},
);
Expand All @@ -147,8 +160,10 @@ class _ItineraryEntry extends StatelessWidget {
final ValueNotifier<String?> totalCostNotifier;
final ItineraryEntryType type;
final ItineraryEntryStatus status;
final JsonMap? choiceRequiredAction;
final String widgetId;
final DispatchEventCallback dispatchEvent;
final DataContext dataContext;

const _ItineraryEntry({
required this.titleNotifier,
Expand All @@ -159,8 +174,10 @@ class _ItineraryEntry extends StatelessWidget {
required this.totalCostNotifier,
required this.type,
required this.status,
this.choiceRequiredAction,
required this.widgetId,
required this.dispatchEvent,
required this.dataContext,
});

IconData _getIconForType(ItineraryEntryType type) {
Expand All @@ -178,7 +195,7 @@ class _ItineraryEntry extends StatelessWidget {
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expand Down Expand Up @@ -207,13 +224,24 @@ class _ItineraryEntry extends StatelessWidget {
valueListenable: titleNotifier,
builder: (context, title, _) => FilledButton(
onPressed: () {
final actionData = choiceRequiredAction;
if (actionData == null) {
return;
}
final actionName =
actionData['actionName'] as String;
final contextDefinition =
(actionData['context'] as List<Object?>?) ??
<Object>[];
final resolvedContext = resolveContext(
dataContext,
contextDefinition,
);
dispatchEvent(
UiActionEvent(
widgetId: widgetId,
eventType: 'seeOptions',
value:
'Choose options in order to book '
'${title ?? ''}',
UserActionEvent(
actionName: actionName,
sourceComponentId: widgetId,
context: resolvedContext,
),
);
DismissNotification().dispatch(context);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -189,9 +189,7 @@ class _ItineraryWithDetails extends StatelessWidget {
child: Row(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(
8.0,
), // Adjust radius as needed
borderRadius: BorderRadius.circular(8.0),
child: SizedBox(height: 100, width: 100, child: imageChild),
),
const SizedBox(width: 8.0),
Expand Down
45 changes: 36 additions & 9 deletions examples/travel_app/lib/src/catalog/listings_booker.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,21 @@ import '../tools/booking/booking_service.dart';
import '../tools/booking/model.dart';

final _schema = S.object(
description: 'A widget to check out set of listings.',
description: 'A widget to select among a set of listings.',
properties: {
'listingSelectionIds': S.list(
description: 'Listings to checkout.',
description: 'Listings to select among.',
items: S.string(),
),
'itineraryName': A2uiSchemas.stringReference(
description: 'The name of the itinerary.',
),
'modifyAction': A2uiSchemas.action(
description:
'The action to perform when the user wants to modify a listing '
'selection. The listingSelectionId will be added to the context with '
'the key "listingSelectionId".',
),
},
required: ['listingSelectionIds'],
);
Expand All @@ -28,14 +34,17 @@ extension type _ListingsBookerData.fromMap(Map<String, Object?> _json) {
factory _ListingsBookerData({
required List<String> listingSelectionIds,
required JsonMap itineraryName,
JsonMap? modifyAction,
}) => _ListingsBookerData.fromMap({
'listingSelectionIds': listingSelectionIds,
'itineraryName': itineraryName,
if (modifyAction != null) 'modifyAction': modifyAction,
});

List<String> get listingSelectionIds =>
(_json['listingSelectionIds'] as List).cast<String>();
JsonMap get itineraryName => _json['itineraryName'] as JsonMap;
JsonMap? get modifyAction => _json['modifyAction'] as JsonMap?;
}

final listingsBooker = CatalogItem(
Expand Down Expand Up @@ -66,6 +75,8 @@ final listingsBooker = CatalogItem(
itineraryName: itineraryName ?? '',
dispatchEvent: dispatchEvent,
widgetId: id,
modifyAction: listingsBookerData.modifyAction,
dataContext: dataContext,
);
},
);
Expand Down Expand Up @@ -136,12 +147,16 @@ class _ListingsBooker extends StatefulWidget {
final String itineraryName;
final DispatchEventCallback dispatchEvent;
final String widgetId;
final JsonMap? modifyAction;
final DataContext dataContext;

const _ListingsBooker({
required this.listingSelectionIds,
required this.itineraryName,
required this.dispatchEvent,
required this.widgetId,
this.modifyAction,
required this.dataContext,
});

@override
Expand Down Expand Up @@ -328,14 +343,26 @@ class _ListingsBookerState extends State<_ListingsBooker> {
const SizedBox(width: 8),
TextButton(
onPressed: () {
final actionData = widget.modifyAction;
if (actionData == null) {
return;
}
final actionName =
actionData['actionName'] as String;
final contextDefinition =
(actionData['context'] as List<Object?>?) ??
<Object?>[];
final resolvedContext = resolveContext(
widget.dataContext,
contextDefinition,
);
resolvedContext['listingSelectionId'] =
listing.listingSelectionId;
widget.dispatchEvent(
UiActionEvent(
eventType: 'Modify',
widgetId: widget.widgetId,
value: {
'listingSelectionId':
listing.listingSelectionId,
},
UserActionEvent(
actionName: actionName,
sourceComponentId: widget.widgetId,
context: resolvedContext,
),
);
},
Expand Down
Loading
Loading
点击 这是indexloc提供的php浏览器服务,不要输入任何密码和下载