Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
265 changes: 164 additions & 101 deletions examples/travel_app/lib/src/catalog/travel_carousel.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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(
Copy link
Collaborator

Choose a reason for hiding this comment

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

Optional: How about having a separate subtitle field or something which is shown in a different color, like a grey or something? While it's a small thing, I think it makes the UI look quite a bit more polished to have visually separated sections like that.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I am afraid it may make it more complicated for AI. So, we may loose reliability.
I like the idea. But I would make it P2 and test it carefully. BTW, can be a good item for external contributors.

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 '
Expand Down Expand Up @@ -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,
),
Expand All @@ -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<String, Object?> _json) {
Expand All @@ -189,16 +110,16 @@ extension type _TravelCarouselItemSchemaData.fromMap(
Map<String, Object?> _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?;
}
Expand Down Expand Up @@ -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(
Expand All @@ -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,
});
Expand Down Expand Up @@ -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,
},
Expand All @@ -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,
),
),
Expand All @@ -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',
},
],
};
Original file line number Diff line number Diff line change
Expand Up @@ -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,
),
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,6 @@ class ListHotelsTool extends AiTool<Map<String, Object?>> {
@override
Future<JsonMap> invoke(JsonMap args) async {
final search = HotelSearch.fromJson(args);
return (await onListHotels(search)).toJson();
return (await onListHotels(search)).toAiInput();
}
}
Loading
Loading