Skip to content

Commit 47eb6fd

Browse files
authored
Make it easy to choose dates. (#331)
1 parent 6c156d9 commit 47eb6fd

File tree

9 files changed

+322
-14
lines changed

9 files changed

+322
-14
lines changed

.vscode/launch.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
},
1919
{
2020
"name": "catalog_gallery",
21-
"cwd": "packages/catalog_gallery",
21+
"cwd": "examples/catalog_gallery",
2222
"request": "launch",
2323
"type": "dart"
2424
},
@@ -27,7 +27,10 @@
2727
"request": "launch",
2828
"program": "bin/fix_copyright.dart",
2929
"cwd": "tool/fix_copyright",
30-
"args": ["--force", "${workspaceFolder}"],
30+
"args": [
31+
"--force",
32+
"${workspaceFolder}"
33+
],
3134
"type": "dart",
3235
"console": "debugConsole"
3336
}

examples/travel_app/lib/main.dart

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ void main() async {
3434
runApp(const TravelApp());
3535
}
3636

37+
const _title = 'Agentic Travel Inc';
38+
3739
/// The root widget for the travel application.
3840
///
3941
/// This widget sets up the [MaterialApp], which configures the overall theme,
@@ -52,7 +54,7 @@ class TravelApp extends StatelessWidget {
5254
Widget build(BuildContext context) {
5355
return MaterialApp(
5456
debugShowCheckedModeBanner: false,
55-
title: 'Agentic Travel Inc.',
57+
title: _title,
5658
theme: ThemeData(
5759
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
5860
),
@@ -87,7 +89,7 @@ class _TravelAppBody extends StatelessWidget {
8789
children: <Widget>[
8890
Icon(Icons.local_airport),
8991
SizedBox(width: 16.0), // Add spacing between icon and text
90-
Text('Agentic Travel Inc.'),
92+
Text(_title),
9193
],
9294
),
9395
actions: [

examples/travel_app/lib/src/catalog.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import 'package:flutter_genui/flutter_genui.dart';
66

77
import 'catalog/checkbox_filter_chips_input.dart';
8+
import 'catalog/date_input_chip.dart';
89
import 'catalog/information_card.dart';
910
import 'catalog/input_group.dart';
1011
import 'catalog/itinerary_day.dart';
@@ -28,6 +29,7 @@ import 'catalog/travel_carousel.dart';
2829
/// and [inputGroup]. The AI selects from these components to build a dynamic
2930
/// and interactive UI in response to user prompts.
3031
final travelAppCatalog = CoreCatalogItems.asCatalog().copyWith([
32+
dateInputChip,
3133
inputGroup,
3234
optionsFilterChipInput,
3335
checkboxFilterChipsInput,
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
// Copyright 2025 The Flutter Authors.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
// ignore_for_file: avoid_dynamic_calls
6+
7+
import 'package:dart_schema_builder/dart_schema_builder.dart';
8+
import 'package:flutter/material.dart';
9+
import 'package:flutter_genui/flutter_genui.dart';
10+
import 'package:intl/intl.dart';
11+
12+
final _schema = S.object(
13+
properties: {
14+
'value': S.string(
15+
description: 'The initial date of the date picker in yyyy-mm-dd format.',
16+
),
17+
'label': S.string(description: 'Label for the date picker.'),
18+
},
19+
);
20+
21+
extension type _DatePickerData.fromMap(JsonMap _json) {
22+
factory _DatePickerData({String? value, String? label}) =>
23+
_DatePickerData.fromMap({'value': value, 'label': label});
24+
25+
String? get value => _json['value'] as String?;
26+
String? get label => _json['label'] as String?;
27+
}
28+
29+
class _DateInputChip extends StatefulWidget {
30+
const _DateInputChip({
31+
this.initialValue,
32+
this.label,
33+
required this.onChanged,
34+
});
35+
36+
final String? initialValue;
37+
final String? label;
38+
final void Function(String) onChanged;
39+
40+
@override
41+
State<_DateInputChip> createState() => _DateInputChipState();
42+
}
43+
44+
class _DateInputChipState extends State<_DateInputChip> {
45+
DateTime? _selectedDate;
46+
47+
@override
48+
void initState() {
49+
super.initState();
50+
if (widget.initialValue != null) {
51+
_selectedDate = DateTime.tryParse(widget.initialValue!);
52+
}
53+
}
54+
55+
@override
56+
void didUpdateWidget(_DateInputChip oldWidget) {
57+
super.didUpdateWidget(oldWidget);
58+
if (widget.initialValue != oldWidget.initialValue) {
59+
if (widget.initialValue != null) {
60+
_selectedDate = DateTime.tryParse(widget.initialValue!);
61+
} else {
62+
_selectedDate = null;
63+
}
64+
}
65+
}
66+
67+
@override
68+
Widget build(BuildContext context) {
69+
final text = _selectedDate == null
70+
? widget.label ?? 'Date'
71+
: '${widget.label}: ${DateFormat.yMMMd().format(_selectedDate!)}';
72+
return FilterChip(
73+
label: Text(text),
74+
selected: false,
75+
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20.0)),
76+
onSelected: (bool selected) {
77+
showModalBottomSheet<void>(
78+
context: context,
79+
builder: (BuildContext context) {
80+
return SizedBox(
81+
height: 300,
82+
child: Column(
83+
children: [
84+
Expanded(
85+
child: CalendarDatePicker(
86+
initialDate: _selectedDate ?? DateTime.now(),
87+
firstDate: DateTime(1700),
88+
lastDate: DateTime(2101),
89+
onDateChanged: (newDate) {
90+
setState(() {
91+
_selectedDate = newDate;
92+
});
93+
final formattedDate = DateFormat(
94+
'yyyy-MM-dd',
95+
).format(newDate);
96+
widget.onChanged(formattedDate);
97+
Navigator.pop(context);
98+
},
99+
),
100+
),
101+
],
102+
),
103+
);
104+
},
105+
);
106+
},
107+
);
108+
}
109+
}
110+
111+
final dateInputChip = CatalogItem(
112+
name: 'DateInputChip',
113+
dataSchema: _schema,
114+
exampleData: [
115+
() => {
116+
'root': 'date_picker',
117+
'widgets': [
118+
{
119+
'id': 'date_picker',
120+
'widget': {
121+
'DateInputChip': {
122+
'value': '1871-07-22',
123+
'label': 'Your birth date',
124+
},
125+
},
126+
},
127+
],
128+
},
129+
],
130+
131+
widgetBuilder:
132+
({
133+
required data,
134+
required id,
135+
required buildChild,
136+
required dispatchEvent,
137+
required context,
138+
required values,
139+
}) {
140+
final datePickerData = _DatePickerData.fromMap(data as JsonMap);
141+
return _DateInputChip(
142+
initialValue: datePickerData.value,
143+
label: datePickerData.label,
144+
onChanged: (newValue) => values[id] = newValue,
145+
);
146+
},
147+
);

examples/travel_app/lib/src/catalog/input_group.dart

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,53 @@ extension type _InputGroupData.fromMap(Map<String, Object?> _json) {
4141
/// to process the current selections from all the child chips at once, which
4242
/// is useful for refining a search or query with multiple parameters.
4343
final inputGroup = CatalogItem(
44+
exampleData: [
45+
() => {
46+
'root': 'input_group',
47+
'widgets': [
48+
{
49+
'id': 'input_group',
50+
'widget': {
51+
'Column': {
52+
'children': [
53+
'check_in',
54+
'check_out',
55+
'text_input1',
56+
'text_input2',
57+
],
58+
},
59+
},
60+
},
61+
{
62+
'id': 'check_in',
63+
'widget': {
64+
'DateInputChip': {'value': '2026-07-22', 'label': 'Check-in date'},
65+
},
66+
},
67+
{
68+
'id': 'check_out',
69+
'widget': {
70+
'DateInputChip': {'label': 'Check-out date'},
71+
},
72+
},
73+
{
74+
'id': 'text_input1',
75+
'widget': {
76+
'TextInputChip': {
77+
'initialValue': 'John Doe',
78+
'label': 'Enter your name',
79+
},
80+
},
81+
},
82+
{
83+
'id': 'text_input2',
84+
'widget': {
85+
'TextInputChip': {'label': 'Enter your friend\'s name'},
86+
},
87+
},
88+
],
89+
},
90+
],
4491
name: 'InputGroup',
4592
dataSchema: _schema,
4693
widgetBuilder:

examples/travel_app/lib/src/catalog/text_input_chip.dart

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,22 @@ extension type _TextInputChipData.fromMap(Map<String, Object?> _json) {
3333
final textInputChip = CatalogItem(
3434
name: 'TextInputChip',
3535
dataSchema: _schema,
36+
exampleData: [
37+
() => {
38+
'root': 'text_input',
39+
'widgets': [
40+
{
41+
'id': 'text_input',
42+
'widget': {
43+
'TextInputChip': {
44+
'initialValue': 'John Doe',
45+
'label': 'Enter your name',
46+
},
47+
},
48+
},
49+
],
50+
},
51+
],
3652
widgetBuilder:
3753
({
3854
required data,

examples/travel_app/lib/src/tools/booking/booking_service.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ class BookingService {
3434
String paymentMethodId,
3535
) async {
3636
// ignore: inference_failure_on_instance_creation
37-
await Future.delayed(const Duration(milliseconds: 100));
37+
await Future.delayed(const Duration(milliseconds: 400));
3838
}
3939

4040
/// Synchronous version for example data generation.

examples/travel_app/lib/src/travel_planner_page.dart

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -339,18 +339,12 @@ to the user.
339339
with chips to ask the user for preferences, and the `travelCarousel` to show
340340
the user different options. When the user chooses an option, you can confirm
341341
it has been chosen and immediately prompt the user to book the next detail,
342-
e.g. an activity, accommodation, transport etc. When a booking is confirmed,
342+
e.g. an activity, hotels, transport etc. When a booking is confirmed,
343343
update the original `itineraryWithDetails` to reflect the booking by
344344
updating the relevant `itineraryEntry` to have the status `chosen` and
345345
including the booking details in the `bodyText`.
346346
347-
When booking accommodation, you should use the `listHotels` tool to search
348-
for hotels, and then pass the listingSelectionId to `travelCarousel` of the selected hotel. You can then show the user the different options in a
349-
`travelCarousel`. When user selects a hotel, remember the listingSelectionId for the next step.
350-
351-
After selecting hotel, suggest the user to check out the
352-
itinerary and use `listingsBooker`, passing previously remembered listingSelectionId
353-
to the parameter listingSelectionIds.
347+
When booking a hotel, use inputGroup, providing initial values for check-in and check-out dates (nearest weekend). Then use the `listHotels` tool to search for hotels and pass the values listingSelectionId to `travelCarousel` to show the user different options. When user selects a hotel, pass the listingSelectionId of the selected hotel the parameter listingSelectionIds of `listingsBooker`.
354348
355349
IMPORTANT: The user may start from different steps in the flow, and it is your job to
356350
understand which step of the flow the user is at, and when they are ready to
@@ -416,7 +410,7 @@ transport for that day.
416410
417411
- Inputs: When you are asking for information from the user, you should always include a
418412
submit button of some kind so that the user can indicate that they are done
419-
providing information. The `InputGroup` has a submit button, but if
413+
providing information. Suggest initial values for number of people and travel dates (e.g. 2 guests, dates of nearest weekend). The `InputGroup` has a submit button, but if
420414
you are not using that, you can use an `ElevatedButton`. Only use
421415
`OptionsFilterChipInput` widgets inside of a `InputGroup`.
422416

0 commit comments

Comments
 (0)