Skip to content
Open
4 changes: 2 additions & 2 deletions examples/catalog_gallery/lib/samples_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ class _SamplesViewState extends State<SamplesView> {
@override
void initState() {
super.initState();
_genUiManager = GenUiManager(catalog: widget.catalog);
_genUiManager = GenUiManager(catalogs: [widget.catalog]);
_loadSamples();
_setupSurfaceListener();
}
Expand Down Expand Up @@ -109,7 +109,7 @@ class _SamplesViewState extends State<SamplesView> {
});
// Re-create GenUiManager to ensure a clean state for the new sample.
_genUiManager.dispose();
_genUiManager = GenUiManager(catalog: widget.catalog);
_genUiManager = GenUiManager(catalogs: [widget.catalog]);
_setupSurfaceListener();

try {
Expand Down
2 changes: 1 addition & 1 deletion examples/custom_backend/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ class _IntegrationTesterState extends State<_IntegrationTester> {
final _controller = TextEditingController(text: requestText);

final _protocol = Backend(uiSchema);
late final GenUiManager _genUi = GenUiManager(catalog: _catalog);
late final GenUiManager _genUi = GenUiManager(catalogs: [_catalog]);
String? _selectedResponse;
bool _isLoading = false;
String? _errorMessage;
Expand Down
2 changes: 1 addition & 1 deletion examples/simple_chat/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ class _ChatScreenState extends State<ChatScreen> {
void initState() {
super.initState();
final Catalog catalog = CoreCatalogItems.asCatalog();
_genUiManager = GenUiManager(catalog: catalog);
_genUiManager = GenUiManager(catalogs: [catalog]);

final systemInstruction =
'''You are a helpful assistant who chats with a user,
Expand Down
49 changes: 17 additions & 32 deletions examples/travel_app/lib/src/catalog.dart
Original file line number Diff line number Diff line change
Expand Up @@ -24,35 +24,20 @@ import 'catalog/travel_carousel.dart';
/// for a travel planning experience, such as [travelCarousel], [itinerary],
/// and [inputGroup]. The AI selects from these components to build a dynamic
/// and interactive UI in response to user prompts.
final Catalog travelAppCatalog = CoreCatalogItems.asCatalog()
.copyWithout([
CoreCatalogItems.audioPlayer,
CoreCatalogItems.card,
CoreCatalogItems.checkBox,
CoreCatalogItems.dateTimeInput,
CoreCatalogItems.divider,
CoreCatalogItems.textField,
CoreCatalogItems.list,
CoreCatalogItems.modal,
CoreCatalogItems.multipleChoice,
CoreCatalogItems.slider,
CoreCatalogItems.tabs,
CoreCatalogItems.video,
CoreCatalogItems.icon,
CoreCatalogItems.row,
CoreCatalogItems.image,
])
.copyWith([
CoreCatalogItems.imageFixedSize,
checkboxFilterChipsInput,
dateInputChip,
informationCard,
inputGroup,
itinerary,
listingsBooker,
optionsFilterChipInput,
tabbedSections,
textInputChip,
trailhead,
travelCarousel,
]);
final Catalog travelAppCatalog = Catalog([
CoreCatalogItems.button,
CoreCatalogItems.column,
CoreCatalogItems.text,
CoreCatalogItems.imageFixedSize,
checkboxFilterChipsInput,
dateInputChip,
informationCard,
inputGroup,
itinerary,
listingsBooker,
optionsFilterChipInput,
tabbedSections,
textInputChip,
trailhead,
travelCarousel,
], catalogId: 'example.com:travel_v0');
11 changes: 1 addition & 10 deletions examples/travel_app/lib/src/travel_planner_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -63,16 +63,7 @@ class _TravelPlannerPageState extends State<TravelPlannerPage>
@override
void initState() {
super.initState();
final genUiManager = GenUiManager(
catalog: travelAppCatalog,
configuration: const GenUiConfiguration(
actions: ActionsConfig(
allowCreate: true,
allowUpdate: true,
allowDelete: true,
),
),
);
final genUiManager = GenUiManager(catalogs: [travelAppCatalog]);
_userMessageSubscription = genUiManager.onSubmit.listen(
_handleUserMessageFromUi,
);
Expand Down
2 changes: 1 addition & 1 deletion examples/travel_app/test/widgets/conversation_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ void main() {
late GenUiManager manager;

setUp(() {
manager = GenUiManager(catalog: CoreCatalogItems.asCatalog());
manager = GenUiManager(catalogs: [CoreCatalogItems.asCatalog()]);
});

testWidgets('renders a list of messages', (WidgetTester tester) async {
Expand Down
2 changes: 1 addition & 1 deletion examples/verdure/client/lib/features/ai/ai_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ class AiClientState {
class Ai extends _$Ai {
@override
Future<AiClientState> build() async {
final genUiManager = GenUiManager(catalog: CoreCatalogItems.asCatalog());
final genUiManager = GenUiManager(catalogs: [CoreCatalogItems.asCatalog()]);
final A2uiAgentConnector connector = await ref.watch(
a2uiAgentConnectorProvider.future,
);
Expand Down
4 changes: 4 additions & 0 deletions packages/genui/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## 0.5.2 (in progress)

- **Feature**: `GenUiManager` now supports multiple catalogs by accepting an `Iterable<Catalog>` in its constructor.
- **Feature**: `catalogId` property added to `UiDefinition` to specify which catalog a UI surface should use.
- **Refactor**: Moved `standardCatalogId` constant from `core_catalog.dart` to `primitives/constants.dart` for better organization and accessibility.

## 0.5.1

- Homepage URL was updated.
Expand Down
4 changes: 2 additions & 2 deletions packages/genui/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ provider.

// Create a GenUiManager with a widget catalog.
// The CoreCatalogItems contain basic widgets for text, markdown, and images.
_genUiManager = GenUiManager(catalog: CoreCatalogItems.asCatalog());
_genUiManager = GenUiManager(catalogs: [CoreCatalogItems.asCatalog()]);

// Create a ContentGenerator to communicate with the LLM.
// Provide system instructions and the tools from the GenUiManager.
Expand Down Expand Up @@ -378,7 +378,7 @@ Include your catalog items when instantiating `GenUiManager`.

```dart
_genUiManager = GenUiManager(
catalog: CoreCatalogItems.asCatalog().copyWith([riddleCard]),
catalogs: [CoreCatalogItems.asCatalog().copyWith([riddleCard])],
);
```

Expand Down
1 change: 1 addition & 0 deletions packages/genui/lib/genui.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export 'src/core/widgets/chat_primitives.dart';
export 'src/development_utilities/catalog_view.dart';
export 'src/facade/direct_call_integration/model.dart';
export 'src/facade/direct_call_integration/utils.dart';
export 'src/model/a2ui_client_capabilities.dart';
export 'src/model/a2ui_message.dart';
export 'src/model/a2ui_schemas.dart';
export 'src/model/catalog.dart';
Expand Down
3 changes: 2 additions & 1 deletion packages/genui/lib/src/catalog/core_catalog.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import '../model/catalog.dart';
import '../model/catalog_item.dart';
import '../primitives/constants.dart';
import 'core_widgets/audio_player.dart' as audio_player_item;
import 'core_widgets/button.dart' as button_item;
import 'core_widgets/card.dart' as card_item;
Expand Down Expand Up @@ -129,6 +130,6 @@ class CoreCatalogItems {
text,
textField,
video,
]);
], catalogId: standardCatalogId);
}
}
2 changes: 2 additions & 0 deletions packages/genui/lib/src/content_generator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import 'dart:async';

import 'package:flutter/foundation.dart';

import 'model/a2ui_client_capabilities.dart';
import 'model/a2ui_message.dart';
import 'model/chat_message.dart';

Expand Down Expand Up @@ -49,6 +50,7 @@ abstract interface class ContentGenerator {
Future<void> sendRequest(
ChatMessage message, {
Iterable<ChatMessage>? history,
A2UiClientCapabilities? clientCapabilities,
});

/// Disposes of the resources used by this generator.
Expand Down
14 changes: 13 additions & 1 deletion packages/genui/lib/src/conversation/gen_ui_conversation.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import 'package:flutter/foundation.dart';

import '../content_generator.dart';
import '../core/genui_manager.dart';
import '../model/a2ui_client_capabilities.dart';
import '../model/a2ui_message.dart';
import '../model/chat_message.dart';
import '../model/ui_models.dart';
Expand Down Expand Up @@ -149,7 +150,18 @@ class GenUiConversation {
if (message is! UserUiInteractionMessage) {
_conversation.value = [...history, message];
}
return contentGenerator.sendRequest(message, history: history);
final clientCapabilities = A2UiClientCapabilities(
supportedCatalogIds: genUiManager.catalogs
.map((c) => c.catalogId)
.where((id) => id != null)
.cast<String>()
.toList(),
);
return contentGenerator.sendRequest(
message,
history: history,
clientCapabilities: clientCapabilities,
);
}

void _handleTextResponse(String text) {
Expand Down
17 changes: 8 additions & 9 deletions packages/genui/lib/src/core/genui_manager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,8 @@ abstract interface class GenUiHost {
/// Returns a [ValueNotifier] for the surface with the given [surfaceId].
ValueNotifier<UiDefinition?> getSurfaceNotifier(String surfaceId);

/// The catalog of UI components available to the AI.
Catalog get catalog;
/// The catalogs of UI components available to the AI.
Iterable<Catalog> get catalogs;

/// A map of data models for storing the UI state of each surface.
Map<String, DataModel> get dataModels;
Expand All @@ -85,16 +85,17 @@ abstract interface class GenUiHost {
/// `beginRendering`) that the AI uses to manipulate the UI. It exposes a stream
/// of `GenUiUpdate` events so that the application can react to changes.
class GenUiManager implements GenUiHost {
/// Creates a new [GenUiManager].
///
/// The [catalog] defines the set of widgets available to the AI.
/// Creates a new [GenUiManager] with a list of supported widget catalogs.
GenUiManager({
required this.catalog,
required this.catalogs,
this.configuration = const GenUiConfiguration(),
});

final GenUiConfiguration configuration;

@override
final Iterable<Catalog> catalogs;

final _surfaces = <String, ValueNotifier<UiDefinition?>>{};
final _surfaceUpdates = StreamController<GenUiUpdate>.broadcast();
final _onSubmit = StreamController<UserUiInteractionMessage>.broadcast();
Expand Down Expand Up @@ -129,9 +130,6 @@ class GenUiManager implements GenUiHost {
_onSubmit.add(UserUiInteractionMessage.text(eventJsonString));
}

@override
final Catalog catalog;

@override
ValueNotifier<UiDefinition?> getSurfaceNotifier(String surfaceId) {
if (!_surfaces.containsKey(surfaceId)) {
Expand Down Expand Up @@ -192,6 +190,7 @@ class GenUiManager implements GenUiHost {
notifier.value ?? UiDefinition(surfaceId: message.surfaceId);
final UiDefinition newUiDefinition = uiDefinition.copyWith(
rootComponentId: message.root,
catalogId: message.catalogId,
);
notifier.value = newUiDefinition;
genUiLogger.info('Started rendering ${message.surfaceId}');
Expand Down
47 changes: 45 additions & 2 deletions packages/genui/lib/src/core/genui_surface.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

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

import '../core/genui_manager.dart';
import '../model/catalog.dart';
import '../model/catalog_item.dart';
import '../model/data_model.dart';
import '../model/tools.dart';
import '../model/ui_models.dart';
import '../primitives/constants.dart';
import '../primitives/logging.dart';
import '../primitives/simple_items.dart';

Expand Down Expand Up @@ -58,8 +61,15 @@ class _GenUiSurfaceState extends State<GenUiSurface> {
genUiLogger.warning('Surface ${widget.surfaceId} has no widgets.');
return const SizedBox.shrink();
}

final Catalog? catalog = _findCatalogForDefinition(definition);
if (catalog == null) {
return Container();
}

return _buildWidget(
definition,
catalog,
rootId,
DataContext(widget.host.dataModelForSurface(widget.surfaceId), '/'),
);
Expand All @@ -73,6 +83,7 @@ class _GenUiSurfaceState extends State<GenUiSurface> {
/// and constructs the corresponding Flutter widget.
Widget _buildWidget(
UiDefinition definition,
Catalog catalog,
String widgetId,
DataContext dataContext,
) {
Expand All @@ -84,12 +95,17 @@ class _GenUiSurfaceState extends State<GenUiSurface> {

final JsonMap widgetData = data.componentProperties;
genUiLogger.finest('Building widget $widgetId');
return widget.host.catalog.buildWidget(
return catalog.buildWidget(
CatalogItemContext(
id: widgetId,
data: widgetData,
buildChild: (String childId, [DataContext? childDataContext]) =>
_buildWidget(definition, childId, childDataContext ?? dataContext),
_buildWidget(
definition,
catalog,
childId,
childDataContext ?? dataContext,
),
dispatchEvent: _dispatchEvent,
buildContext: context,
dataContext: dataContext,
Expand All @@ -106,6 +122,16 @@ class _GenUiSurfaceState extends State<GenUiSurface> {
.getSurfaceNotifier(widget.surfaceId)
.value;
if (definition == null) return;

final Catalog? catalog = _findCatalogForDefinition(definition);
if (catalog == null) {
genUiLogger.severe(
'Cannot show modal for surface "${widget.surfaceId}" because '
'a catalog was not found.',
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe log the catalog ID of the catalog it was looking for?

);
return;
}

final modalId = event.context['modalId'] as String;
final Component? modalComponent = definition.components[modalId];
if (modalComponent == null) return;
Expand All @@ -116,6 +142,7 @@ class _GenUiSurfaceState extends State<GenUiSurface> {
context: context,
builder: (context) => _buildWidget(
definition,
catalog,
contentChildId,
DataContext(widget.host.dataModelForSurface(widget.surfaceId), '/'),
),
Expand All @@ -133,4 +160,20 @@ class _GenUiSurfaceState extends State<GenUiSurface> {
: UiEvent.fromMap(eventMap);
widget.host.handleUiEvent(newEvent);
}

Catalog? _findCatalogForDefinition(UiDefinition definition) {
final String catalogId = definition.catalogId ?? standardCatalogId;
final Catalog? catalog = widget.host.catalogs.firstWhereOrNull(
(c) => c.catalogId == catalogId,
);

if (catalog == null) {
genUiLogger.severe(
'Catalog with id "$catalogId" not found for surface '
'"${widget.surfaceId}". Ensure the catalog is provided to '
'GenUiManager.',
);
}
return catalog;
}
}
Loading
Loading