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
6 changes: 6 additions & 0 deletions pkgs/example/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,9 @@ To regenerate diagrams:
dart pub global activate layerlens
layerlens
```

## TODO

TODO before productizing:

1. Make colors and sizes configurable.
25 changes: 15 additions & 10 deletions pkgs/example/lib/app/app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import 'package:flutter/material.dart';

import '../sdk/agent/agent.dart';
import '../sdk/catalog/shared/genui_widget.dart';
import '../sdk/model/agent.dart';

import '../sdk/model/controller.dart';
import '../sdk/model/input.dart';
import '../sdk/model/simple_items.dart';
Expand Down Expand Up @@ -36,13 +36,22 @@ class _MyHomePage extends StatefulWidget {
class _MyHomePageState extends State<_MyHomePage> {
final _scrollController = ScrollController();

late final GenUiAgent _agent = SimpleGenUiAgent(
late final GenUiAgent _agent = GenUiAgent(
GenUiController(
_scrollController,
imageCatalog: _myImageCatalog,
agentIconAsset: 'assets/agent_icon.png',
),
);
)..run();

@override
void initState() {
super.initState();

_agent.controller.state.input.complete(
InitialInput('Show invitations to create a vacation travel itinerary.'),
);
}

@override
Widget build(BuildContext context) {
Expand All @@ -60,15 +69,11 @@ class _MyHomePageState extends State<_MyHomePage> {
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Center(
child: Align(
alignment: Alignment.topLeft,
child: SingleChildScrollView(
controller: _scrollController,
child: GenUiWidget(
InitialInput(
'Invite user to create a vacation travel itinerary.',
),
_agent,
),
child: GenUiWidget(_agent.controller),
),
),
),
Expand Down
62 changes: 47 additions & 15 deletions pkgs/example/lib/sdk/agent/agent.dart
Original file line number Diff line number Diff line change
@@ -1,40 +1,72 @@
import 'dart:async';

import 'package:flutter/widgets.dart';

import '../catalog/messages/elicitation.dart';
import '../catalog/messages/invitation.dart';
import '../model/agent.dart';
import '../model/controller.dart';
import '../model/input.dart';
import '../model/simple_items.dart';
import '../primitives/utils.dart';
import 'fake_output.dart';

class SimpleGenUiAgent extends GenUiAgent {
SimpleGenUiAgent(super.controller);
class GenUiAgent {
GenUiAgent(this.controller);

GenUiController controller;

void run() => _startCycle();

Future<void> _startCycle() async {
while (true) {
await _handleNextInput();
}
}

void dispose() {
// TODO: stop cycle
}

final List<({Input input, WidgetData output})> _history = [];
Future<void> _handleNextInput() async {
final input = await controller.state.input.future;

@override
Future<WidgetBuilder> request(Input input) async {
// Simulate network delay
// Simulate network delay.
await Future<void>.delayed(const Duration(milliseconds: 1000));

late final WidgetData output;
late final WidgetBuilder result;
late final WidgetData data;
late final WidgetBuilder builder;

switch (input) {
case InitialInput():
output = fakeInvitationData;
result = (_) => Invitation(fakeInvitationData, this);
data = fakeInvitationData;
builder = (_) => Invitation(fakeInvitationData, controller);
case ChatBoxInput():
output = fakeElicitationData;
result = (_) => Elicitation(fakeElicitationData, this);
data = fakeElicitationData;
builder = (_) => Elicitation(fakeElicitationData, controller);
default:
throw UnimplementedError(
'The agent does not support input of type ${input.runtimeType}',
);
}

_history.add((input: input, output: output));
final newInput = await controller.state.input.future;
if (newInput != input) {
// If the input has changed, we throw away the results.
return;
}

// Provide the builder for the widget that wait for it.
controller.state.builder.complete(builder);

// Move the input and data to the history.
controller.state.history.add((input: input, data: data));

// Reset the input completer for the next input.
controller.state.input = Completer<Input>();
controller.state.builder = Completer<WidgetBuilder>();

return result;
// Scroll to the bottom after the widget is built
await Future<void>.delayed(const Duration(milliseconds: 200));
await scrollToBottom(controller.scrollController);
}
}
13 changes: 13 additions & 0 deletions pkgs/example/lib/sdk/agent/fake_output.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import 'package:flutter/material.dart';

import '../catalog/elements/carousel.dart';
import '../catalog/elements/filter.dart';
import '../catalog/elements/text_intro.dart';
import '../catalog/messages/elicitation.dart';
import '../catalog/messages/invitation.dart';
Expand Down Expand Up @@ -34,4 +37,14 @@ final fakeElicitationData = ElicitationData(
textIntroData: TextIntroData(
intro: 'OK I can help generate itinerary as follows or tap to edit',
),
filterData: FilterData([
FilterItemData(label: 'Zermatt', icon: Icons.location_pin),
FilterItemData(label: '3 days', icon: Icons.calendar_month),
FilterItemData(
label: '2 adults + 1 child',
icon: Icons.supervised_user_circle_outlined,
),
FilterItemData(label: 'Low cost', icon: Icons.money),
FilterItemData(label: 'Medium activity', icon: Icons.run_circle_sharp),
], submitLabel: 'Generate'),
);
12 changes: 9 additions & 3 deletions pkgs/example/lib/sdk/catalog/elements/chat_box.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,16 @@ import 'package:flutter/material.dart';
import '../../model/input.dart';

class ChatBox extends StatefulWidget {
ChatBox(this.onInput, {super.key, this.fakeInput = ''});
ChatBox(this.onInput, {super.key});

final UserInputCallback onInput;

/// Fake input to simulate pre-filled text in the chat box.
///
/// TODO(polina-c): Remove this in productized version.
final String fakeInput;
final String fakeInput =
'I have 3 days in Zermatt with my wife and 11 year old daughter, '
'and I am wondering how to make the most out of our time.';

@override
State<ChatBox> createState() => _ChatBoxState();
Expand All @@ -24,7 +26,11 @@ class _ChatBoxState extends State<ChatBox> {
void initState() {
super.initState();
_focusNode.addListener(() {
if (widget.fakeInput.isNotEmpty) {
// Reset the input on focus.
if (widget.fakeInput.isNotEmpty &&
!_isSubmitted &&
_focusNode.hasFocus &&
_controller.text.isEmpty) {
setState(() => _controller.text = widget.fakeInput);
}
});
Expand Down
73 changes: 73 additions & 0 deletions pkgs/example/lib/sdk/catalog/elements/filter.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import 'package:flutter/material.dart';

import '../../model/simple_items.dart';

class Filter extends StatefulWidget {
const Filter(this.data, {super.key});

final FilterData data;

@override
State<Filter> createState() => _FilterState();
}

class _FilterState extends State<Filter> {
@override
Widget build(BuildContext context) {
return Card(
color: Theme.of(context).colorScheme.primaryContainer,

child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Wrap(
runSpacing: 8.0,
spacing: 8.0,
children: widget.data.items.map(FilterItem.new).toList(),
),
const SizedBox(height: 16.0),
ElevatedButton(
onPressed: () {},
child: Text(widget.data.submitLabel),
style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(context).colorScheme.primary,
foregroundColor: Colors.white,
),
),
],
),
),
);
}
}

class FilterItem extends StatelessWidget {
const FilterItem(this.data, {super.key});

final FilterItemData data;

@override
Widget build(BuildContext context) {
return ElevatedButton.icon(
icon: Icon(data.icon), // The icon to display
label: Text(data.label), // The text label for the button
onPressed: () {},
);
}
}

class FilterData implements WidgetData {
final List<FilterItemData> items;
final String submitLabel;

FilterData(this.items, {required this.submitLabel});
}

class FilterItemData implements WidgetData {
final String label;
final IconData icon;

FilterItemData({required this.label, required this.icon});
}
23 changes: 9 additions & 14 deletions pkgs/example/lib/sdk/catalog/messages/elicitation.dart
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import 'package:flutter/material.dart';

import '../../model/agent.dart';
import '../../model/controller.dart';
import '../../model/input.dart';
import '../../model/simple_items.dart';
import '../elements/filter.dart';
import '../elements/text_intro.dart';
import '../shared/genui_widget.dart';
import '../shared/text_styles.dart';

class Elicitation extends StatefulWidget {
final ElicitationData data;
final GenUiAgent agent;
final GenUiController controller;

const Elicitation(this.data, this.agent, {super.key});
const Elicitation(this.data, this.controller, {super.key});

@override
State<Elicitation> createState() => _ElicitationState();
Expand All @@ -25,20 +25,14 @@ class _ElicitationState extends State<Elicitation> {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
widget.agent.icon(width: 40, height: 40),
widget.controller.icon(width: 40, height: 40),
const SizedBox(height: 8.0),
TextIntro(widget.data.textIntroData),
const SizedBox(height: 16.0),
Text('filter will be here', style: GenUiTextStyles.h2(context)),
Filter(widget.data.filterData),

const SizedBox(height: 16.0),
ValueListenableBuilder<UserInput?>(
valueListenable: _input,
builder: (context, input, child) {
if (input == null) return const SizedBox.shrink();
return GenUiWidget(input, widget.agent);
},
),
GenUiWidget(widget.controller),
],
);
}
Expand All @@ -50,6 +44,7 @@ class _ElicitationState extends State<Elicitation> {

class ElicitationData extends WidgetData {
final TextIntroData textIntroData;
final FilterData filterData;

ElicitationData({required this.textIntroData});
ElicitationData({required this.filterData, required this.textIntroData});
}
19 changes: 6 additions & 13 deletions pkgs/example/lib/sdk/catalog/messages/invitation.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,19 @@ import 'dart:async';

import 'package:flutter/material.dart';

import '../../model/agent.dart';
import '../../model/controller.dart';
import '../../model/input.dart';
import '../../model/simple_items.dart';
import '../elements/carousel.dart';
import '../elements/chat_box.dart';
import '../elements/text_intro.dart';
import '../shared/genui_widget.dart';
import '../shared/text_styles.dart';

class Invitation extends StatefulWidget {
final InvitationData data;
final GenUiAgent agent;
final GenUiController genUi;

const Invitation(this.data, this.agent, {super.key});
const Invitation(this.data, this.genUi, {super.key});

@override
State<Invitation> createState() => _InvitationState();
Expand All @@ -29,21 +28,15 @@ class _InvitationState extends State<Invitation> {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
widget.agent.icon(width: 40, height: 40),
widget.genUi.icon(width: 40, height: 40),
const SizedBox(height: 8.0),
TextIntro(widget.data.textIntroData),
const SizedBox(height: 16.0),
Text(widget.data.exploreTitle, style: GenUiTextStyles.h2(context)),
Carousel(CarouselData(items: widget.data.exploreItems), onInput),
const SizedBox(height: 16.0),
ChatBox(
onInput,
fakeInput:
'I have 3 days in Zermatt with my wife and 11 year old daughter, '
'and I am wondering how to make the most out of our time.',
),
const SizedBox(height: 28.0),
GenUiWidget.wait(_input, widget.agent),

GenUiWidget(widget.genUi),
],
);
}
Expand Down
Loading