diff --git a/examples/travel_app/assets/booking_service/the_grand_flutter_hotel.jpeg b/examples/travel_app/assets/booking_service/flutter_hotel.jpeg similarity index 100% rename from examples/travel_app/assets/booking_service/the_grand_flutter_hotel.jpeg rename to examples/travel_app/assets/booking_service/flutter_hotel.jpeg diff --git a/examples/travel_app/lib/src/catalog/travel_carousel.dart b/examples/travel_app/lib/src/catalog/travel_carousel.dart index 92e79d3b1..163f27f8c 100644 --- a/examples/travel_app/lib/src/catalog/travel_carousel.dart +++ b/examples/travel_app/lib/src/catalog/travel_carousel.dart @@ -8,6 +8,9 @@ import 'package:dart_schema_builder/dart_schema_builder.dart'; import 'package:flutter/material.dart'; import 'package:flutter_genui/flutter_genui.dart'; +import '../tools/booking/booking_service.dart'; +import '../tools/booking/model.dart'; + final _schema = S.object( properties: { 'title': S.string( @@ -17,7 +20,13 @@ final _schema = S.object( description: 'A list of items to display in the carousel.', items: S.object( properties: { - 'title': S.string(description: 'The title of the carousel item.'), + 'description': S.string( + description: + 'The short description of the carousel item. ' + 'It may include the price and location if applicable. ' + 'It should be very concise. ' + 'Example: "The Dart Inn in Sunnyvale, CA for \$150"', + ), 'imageChildId': S.string( description: 'The ID of the Image widget to display as the carousel item ' @@ -30,7 +39,7 @@ final _schema = S.object( 'a list of hotels or other bookable items.', ), }, - required: ['title', 'imageChildId'], + required: ['description', 'imageChildId'], ), ), }, @@ -67,7 +76,7 @@ final travelCarousel = CatalogItem( items: items .map( (e) => _TravelCarouselItemData( - title: e.title, + description: e.description, imageChild: buildChild(e.imageChildId), listingSelectionId: e.listingSelectionId, ), @@ -77,95 +86,7 @@ final travelCarousel = CatalogItem( dispatchEvent: dispatchEvent, ); }, - exampleData: [ - () => { - 'root': 'greece_inspiration_column', - 'widgets': [ - { - 'id': 'greece_inspiration_column', - 'widget': { - 'Column': { - 'children': ['inspiration_title', 'inspiration_carousel'], - }, - }, - }, - { - 'id': 'inspiration_title', - 'widget': { - 'Text': { - 'text': - "Let's plan your dream trip to Greece! " - 'What kind of experience' - ' are you looking for?', - }, - }, - }, - { - 'widget': { - 'TravelCarousel': { - 'items': [ - { - 'title': 'Relaxing Beach Holiday', - 'imageChildId': 'santorini_beach_image', - 'listingSelectionId': '12345', - }, - { - 'imageChildId': 'akrotiri_fresco_image', - 'title': 'Cultural Exploration', - 'listingSelectionId': '12346', - }, - { - 'imageChildId': 'santorini_caldera_image', - 'title': 'Adventure & Outdoors', - 'listingSelectionId': '12347', - }, - {'title': 'Foodie Tour', 'imageChildId': 'greece_food_image'}, - ], - }, - }, - 'id': 'inspiration_carousel', - }, - { - 'id': 'santorini_beach_image', - 'widget': { - 'Image': { - 'fit': 'cover', - 'assetName': 'assets/travel_images/santorini_panorama.jpg', - }, - }, - }, - { - 'id': 'akrotiri_fresco_image', - 'widget': { - 'Image': { - 'fit': 'cover', - 'assetName': - 'assets/travel_images/akrotiri_spring_fresco_santorini.jpg', - }, - }, - }, - { - 'id': 'santorini_caldera_image', - 'widget': { - 'Image': { - 'assetName': 'assets/travel_images/santorini_from_space.jpg', - 'fit': 'cover', - }, - }, - }, - { - 'widget': { - 'Image': { - 'fit': 'cover', - 'assetName': - 'assets/travel_images/saffron_gatherers_fresco_santorini.jpg', - }, - }, - 'id': 'greece_food_image', - }, - ], - }, - ], + exampleData: [_inspirationExample, _hotelExample], ); extension type _TravelCarouselData.fromMap(Map _json) { @@ -189,16 +110,16 @@ extension type _TravelCarouselItemSchemaData.fromMap( Map _json ) { factory _TravelCarouselItemSchemaData({ - required String title, + required String description, required String imageChildId, String? listingSelectionId, }) => _TravelCarouselItemSchemaData.fromMap({ - 'title': title, + 'description': description, 'imageChildId': imageChildId, if (listingSelectionId != null) 'listingSelectionId': listingSelectionId, }); - String get title => _json['title'] as String; + String get description => _json['description'] as String; String get imageChildId => _json['imageChildId'] as String; String? get listingSelectionId => _json['listingSelectionId'] as String?; } @@ -240,7 +161,7 @@ class _TravelCarousel extends StatelessWidget { const SizedBox(height: 16.0), ], SizedBox( - height: 220, + height: 240, child: ScrollConfiguration( behavior: _DesktopAndWebScrollBehavior(), child: ListView.separated( @@ -264,12 +185,12 @@ class _TravelCarousel extends StatelessWidget { } class _TravelCarouselItemData { - final String title; + final String description; final Widget imageChild; final String? listingSelectionId; _TravelCarouselItemData({ - required this.title, + required this.description, required this.imageChild, this.listingSelectionId, }); @@ -297,7 +218,7 @@ class _TravelCarouselItem extends StatelessWidget { widgetId: widgetId, eventType: 'itemSelected', value: { - 'title': data.title, + 'description': data.description, if (data.listingSelectionId != null) 'listingSelectionId': data.listingSelectionId, }, @@ -312,12 +233,16 @@ class _TravelCarouselItem extends StatelessWidget { borderRadius: BorderRadius.circular(10.0), child: SizedBox(height: 150, width: 190, child: data.imageChild), ), - Padding( + Container( + height: 90, padding: const EdgeInsets.all(8.0), + alignment: Alignment.center, child: Text( - data.title, + data.description, + textAlign: TextAlign.center, style: Theme.of(context).textTheme.titleMedium, - maxLines: 1, + softWrap: true, + maxLines: 3, overflow: TextOverflow.ellipsis, ), ), @@ -327,3 +252,141 @@ class _TravelCarouselItem extends StatelessWidget { ); } } + +JsonMap _hotelExample() { + final hotels = BookingService.instance.listHotelsSync( + HotelSearch( + query: '', + checkIn: DateTime.now(), + checkOut: DateTime.now().add(const Duration(days: 7)), + guests: 2, + ), + ); + final hotel1 = hotels.listings[0]; + final hotel2 = hotels.listings[1]; + + return { + 'root': 'hotel_carousel', + 'widgets': [ + { + 'widget': { + 'TravelCarousel': { + 'items': [ + { + 'description': hotel1.description, + 'imageChildId': 'image_1', + 'listingSelectionId': '12345', + }, + { + 'description': hotel2.description, + 'imageChildId': 'image_2', + 'listingSelectionId': '12346', + }, + ], + }, + }, + 'id': 'hotel_carousel', + }, + { + 'id': 'image_1', + 'widget': { + 'Image': {'fit': 'cover', 'assetName': hotel1.images[0]}, + }, + }, + { + 'id': 'image_2', + 'widget': { + 'Image': {'fit': 'cover', 'assetName': hotel2.images[0]}, + }, + }, + ], + }; +} + +JsonMap _inspirationExample() => { + 'root': 'greece_inspiration_column', + 'widgets': [ + { + 'id': 'greece_inspiration_column', + 'widget': { + 'Column': { + 'children': ['inspiration_title', 'inspiration_carousel'], + }, + }, + }, + { + 'id': 'inspiration_title', + 'widget': { + 'Text': { + 'text': + "Let's plan your dream trip to Greece! " + 'What kind of experience' + ' are you looking for?', + }, + }, + }, + { + 'widget': { + 'TravelCarousel': { + 'items': [ + { + 'description': 'Relaxing Beach Holiday', + 'imageChildId': 'santorini_beach_image', + 'listingSelectionId': '12345', + }, + { + 'imageChildId': 'akrotiri_fresco_image', + 'description': 'Cultural Exploration', + 'listingSelectionId': '12346', + }, + { + 'imageChildId': 'santorini_caldera_image', + 'description': 'Adventure & Outdoors', + 'listingSelectionId': '12347', + }, + {'description': 'Foodie Tour', 'imageChildId': 'greece_food_image'}, + ], + }, + }, + 'id': 'inspiration_carousel', + }, + { + 'id': 'santorini_beach_image', + 'widget': { + 'Image': { + 'fit': 'cover', + 'assetName': 'assets/travel_images/santorini_panorama.jpg', + }, + }, + }, + { + 'id': 'akrotiri_fresco_image', + 'widget': { + 'Image': { + 'fit': 'cover', + 'assetName': + 'assets/travel_images/akrotiri_spring_fresco_santorini.jpg', + }, + }, + }, + { + 'id': 'santorini_caldera_image', + 'widget': { + 'Image': { + 'assetName': 'assets/travel_images/santorini_from_space.jpg', + 'fit': 'cover', + }, + }, + }, + { + 'widget': { + 'Image': { + 'fit': 'cover', + 'assetName': + 'assets/travel_images/saffron_gatherers_fresco_santorini.jpg', + }, + }, + 'id': 'greece_food_image', + }, + ], +}; diff --git a/examples/travel_app/lib/src/tools/booking/booking_service.dart b/examples/travel_app/lib/src/tools/booking/booking_service.dart index 68a52fd2a..0e08878ea 100644 --- a/examples/travel_app/lib/src/tools/booking/booking_service.dart +++ b/examples/travel_app/lib/src/tools/booking/booking_service.dart @@ -54,11 +54,11 @@ class BookingService { ), _rememberListing( HotelListing( - name: 'The Grand Flutter Hotel', + name: 'The Flutter Hotel', location: 'Mountain View, CA', pricePerNight: 250.0, listingSelectionId: _generateListingSelectionId(), - images: ['assets/booking_service/the_grand_flutter_hotel.jpeg'], + images: ['assets/booking_service/flutter_hotel.jpeg'], search: search, ), ), diff --git a/examples/travel_app/lib/src/tools/booking/list_hotels_tool.dart b/examples/travel_app/lib/src/tools/booking/list_hotels_tool.dart index 910cea368..a76c0d45d 100644 --- a/examples/travel_app/lib/src/tools/booking/list_hotels_tool.dart +++ b/examples/travel_app/lib/src/tools/booking/list_hotels_tool.dart @@ -43,6 +43,6 @@ class ListHotelsTool extends AiTool> { @override Future invoke(JsonMap args) async { final search = HotelSearch.fromJson(args); - return (await onListHotels(search)).toJson(); + return (await onListHotels(search)).toAiInput(); } } diff --git a/examples/travel_app/lib/src/tools/booking/model.dart b/examples/travel_app/lib/src/tools/booking/model.dart index bd29fd5bc..ce7fb1ff2 100644 --- a/examples/travel_app/lib/src/tools/booking/model.dart +++ b/examples/travel_app/lib/src/tools/booking/model.dart @@ -20,6 +20,10 @@ class HotelSearchResult { JsonMap toJson() { return {'listings': listings.map((e) => e.toJson()).toList()}; } + + JsonMap toAiInput() { + return {'listings': listings.map((e) => e.toAiInput()).toList()}; + } } abstract class Listing { @@ -45,6 +49,9 @@ class HotelListing implements Listing { required this.search, }); + late final String description = + '$name in $location, \$${pricePerNight.ceil()}'; + static HotelListing fromJson(JsonMap json) { return HotelListing( name: json['name'] as String, @@ -66,6 +73,14 @@ class HotelListing implements Listing { 'search': search.toJson(), }; } + + JsonMap toAiInput() { + return { + 'description': description, + 'images': images, + 'listingSelectionId': listingSelectionId, + }; + } } class HotelSearch { diff --git a/examples/travel_app/test/tools/hotels/list_hotels_tool_test.dart b/examples/travel_app/test/tools/hotels/list_hotels_tool_test.dart index ac80b3411..da313eab8 100644 --- a/examples/travel_app/test/tools/hotels/list_hotels_tool_test.dart +++ b/examples/travel_app/test/tools/hotels/list_hotels_tool_test.dart @@ -123,8 +123,9 @@ void main() { }; final result = await tool.invoke(args); - final searchResult = HotelSearchResult.fromJson(result); - expect(searchResult.listings.first.listingSelectionId, isNotEmpty); + final listings = result['listings'] as List; + final listing = listings.first as Map; + expect(listing['listingSelectionId'], isNotEmpty); }); }); }); diff --git a/examples/travel_app/test/travel_carousel_test.dart b/examples/travel_app/test/travel_carousel_test.dart index 3cd0debad..9f0ec3219 100644 --- a/examples/travel_app/test/travel_carousel_test.dart +++ b/examples/travel_app/test/travel_carousel_test.dart @@ -16,8 +16,8 @@ void main() { await mockNetworkImagesFor(() async { final data = { 'items': [ - {'title': 'Item 1', 'imageChildId': 'imageId1'}, - {'title': 'Item 2', 'imageChildId': 'imageId2'}, + {'description': 'Item 1', 'imageChildId': 'imageId1'}, + {'description': 'Item 2', 'imageChildId': 'imageId2'}, ], }; UiEvent? dispatchedEvent; @@ -58,7 +58,7 @@ void main() { final actionEvent = dispatchedEvent as UiActionEvent; expect(actionEvent.widgetId, 'testId'); expect(actionEvent.eventType, 'itemSelected'); - expect(actionEvent.value, {'title': 'Item 1'}); + expect(actionEvent.value, {'description': 'Item 1'}); }); }); @@ -69,11 +69,11 @@ void main() { final data = { 'items': [ { - 'title': 'Item 1', + 'description': 'Item 1', 'imageChildId': 'imageId1', 'listingSelectionId': 'listing1', }, - {'title': 'Item 2', 'imageChildId': 'imageId2'}, + {'description': 'Item 2', 'imageChildId': 'imageId2'}, ], }; UiEvent? dispatchedEvent; @@ -108,7 +108,7 @@ void main() { final actionEvent = dispatchedEvent as UiActionEvent; expect(actionEvent.value, { - 'title': 'Item 1', + 'description': 'Item 1', 'listingSelectionId': 'listing1', }); });