Skip to content
Draft
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
8 changes: 5 additions & 3 deletions examples/catalog_gallery/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@
// found in the LICENSE file.

import 'dart:convert';

import 'package:args/args.dart';
import 'package:file/file.dart';
import 'package:file/local.dart';
import 'package:flutter/material.dart';
import 'package:genui/genui.dart';
import 'package:logging/logging.dart';

import 'samples_view.dart';

Expand All @@ -22,14 +24,14 @@ void main(List<String> args) {
samplesDir = fs.directory(results['samples'] as String);
} else {
final Directory current = fs.currentDirectory;
final Directory defaultSamples = fs
.directory(current.path)
.childDirectory('samples');
final Directory defaultSamples = fs.directory('${current.path}/samples');
if (defaultSamples.existsSync()) {
samplesDir = defaultSamples;
}
}

configureGenUiLogging(level: Level.ALL);

runApp(CatalogGalleryApp(samplesDir: samplesDir, fs: fs));
}

Expand Down
16 changes: 12 additions & 4 deletions examples/catalog_gallery/lib/sample_parser.dart
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ class SampleParser {

static Sample parseString(String content) {
final List<String> lines = const LineSplitter().convert(content);
if (lines.firstOrNull == '---') {
lines.removeAt(0);
}
final int separatorIndex = lines.indexOf('---');

if (separatorIndex == -1) {
Expand All @@ -49,11 +52,16 @@ class SampleParser {
.convert(jsonlBody)
.where((line) => line.trim().isNotEmpty)
.map((line) {
final dynamic json = jsonDecode(line);
if (json is Map<String, dynamic>) {
return A2uiMessage.fromJson(json);
try {
final dynamic json = jsonDecode(line);
if (json is Map<String, dynamic>) {
return A2uiMessage.fromJson(json);
}
throw FormatException('Invalid JSON line: $line');
} on FormatException catch (e) {
print('Error parsing line: $line, error: $e');
rethrow;
}
throw FormatException('Invalid JSON line: $line');
}),
);

Expand Down
24 changes: 7 additions & 17 deletions examples/catalog_gallery/lib/samples_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -66,23 +66,12 @@ class _SamplesViewState extends State<SamplesView> {
});
}
} else if (update is SurfaceRemoved) {
if (_surfaceIds.contains(update.surfaceId)) {
setState(() {
final int removeIndex = _surfaceIds.indexOf(update.surfaceId);
_surfaceIds.removeAt(removeIndex);
if (_surfaceIds.isEmpty) {
_currentSurfaceIndex = 0;
} else {
if (_currentSurfaceIndex >= removeIndex &&
_currentSurfaceIndex > 0) {
_currentSurfaceIndex--;
}
if (_currentSurfaceIndex >= _surfaceIds.length) {
_currentSurfaceIndex = _surfaceIds.length - 1;
}
}
});
}
setState(() {
_surfaceIds.remove(update.surfaceId);
if (_currentSurfaceIndex >= _surfaceIds.length) {
_currentSurfaceIndex = 0;
}
});
}
});
}
Expand All @@ -97,6 +86,7 @@ class _SamplesViewState extends State<SamplesView> {
.toList();
setState(() {
_sampleFiles = files;
_sampleFiles.sort((a, b) => a.path.compareTo(b.path));
});
}

Expand Down
1 change: 1 addition & 0 deletions examples/catalog_gallery/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ dependencies:
sdk: flutter
genui: ^0.5.1
json_schema_builder: ^0.1.3
logging: ^1.3.0
yaml: ^3.1.3

dev_dependencies:
Expand Down
5 changes: 3 additions & 2 deletions examples/catalog_gallery/samples/hello_world.sample
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
---
name: Test Sample
description: This is a test sample to verify the parser.
---
{"surfaceUpdate": {"surfaceId": "default", "components": [{"id": "text1", "component": {"Text": {"text": {"literalString": "Hello World"}}}}]}}
{"beginRendering": {"surfaceId": "default", "root": "text1"}}
{"surfaceUpdate": {"surfaceId": "default", "components": [{"id": "root", "props": { "component": "Text", "text": {"literalString": "Hello World"}}}]}}
{"createSurface": {"surfaceId": "default"}}
9 changes: 4 additions & 5 deletions examples/catalog_gallery/test/sample_parser_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ void main() {
name: Test Sample
description: A test description
---
{"surfaceUpdate": {"surfaceId": "default", "components": [{"id": "text1", "component": {"Text": {"text": {"literalString": "Hello"}}}}]}}
{"beginRendering": {"surfaceId": "default", "root": "text1"}}
{"surfaceUpdate": {"surfaceId": "default", "components": [{"id": "text1", "props": {"component": "Text", "text": {"literalString": "Hello"}}}]}}
{"createSurface": {"surfaceId": "default"}}
''';

final Sample sample = SampleParser.parseString(sampleContent);
Expand All @@ -24,16 +24,15 @@ description: A test description
final List<A2uiMessage> messages = await sample.messages.toList();
expect(messages.length, 2);
expect(messages.first, isA<SurfaceUpdate>());
expect(messages.last, isA<BeginRendering>());
expect(messages.last, isA<CreateSurface>());

final update = messages.first as SurfaceUpdate;
expect(update.surfaceId, 'default');
expect(update.components.length, 1);
expect(update.components.first.type, 'Text');

final begin = messages.last as BeginRendering;
final begin = messages.last as CreateSurface;
expect(begin.surfaceId, 'default');
expect(begin.root, 'text1');
});

test('SampleParser throws on missing separator', () {
Expand Down
44 changes: 42 additions & 2 deletions examples/catalog_gallery/test/widget_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import 'package:file/memory.dart';
import 'package:file/src/interface/directory.dart';
import 'package:file/src/interface/file.dart';

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

void main() {
Expand All @@ -25,9 +26,10 @@ void main() {
final File sampleFile = samplesDir.childFile('test.sample');
sampleFile.writeAsStringSync('''
name: Test Sample
description: A test description
description: This is a test sample to verify the parser.
---
{"surfaceUpdate": {"surfaceId": "default", "components": [{"id": "text1", "component": {"Text": {"text": {"literalString": "Hello"}}}}]}}
{"surfaceUpdate": {"surfaceId": "default", "components": [{"id": "text1", "component": {"Text": {"text": {"literalString": "Hello World"}}}}]}}
{"beginRendering": {"surfaceId": "default", "root": "text1"}}
''');

await tester.pumpWidget(CatalogGalleryApp(samplesDir: samplesDir, fs: fs));
Expand All @@ -41,7 +43,45 @@ description: A test description
await tester.tap(find.text('Samples'));
await tester.pumpAndSettle();

// Verify that the sample file is listed.
// Verify that the sample file is listed.
expect(find.text('test'), findsOneWidget);
});

testWidgets('Loads sample with CreateSurface before SurfaceUpdate', (
WidgetTester tester,
) async {
final fs = MemoryFileSystem();
final Directory samplesDir = fs.directory('/samples')..createSync();
final File sampleFile = samplesDir.childFile('ordered.sample');
sampleFile.writeAsStringSync('''
name: Ordered Sample
description: Testing order.
---
{"createSurface": {"surfaceId": "s1"}}
{"surfaceUpdate": {"surfaceId": "s1", "components": [{"id": "root", "props": {"component": "Text", "text": {"literalString": "Ordered Success"}}}]}}
''');

await tester.pumpWidget(Container()); // Clear previous widget tree
await tester.pumpAndSettle();
await tester.pumpWidget(
CatalogGalleryApp(key: UniqueKey(), samplesDir: samplesDir, fs: fs),
);
await tester.pumpAndSettle();

// Tap on the Samples tab to load the view
await tester.tap(find.text('Samples'));
await tester.pumpAndSettle();

// Verify sample is listed
expect(find.text('ordered'), findsOneWidget);

// Tap on sample
await tester.tap(find.text('ordered'));
await tester.pumpAndSettle();

// Verify surface is created and content is shown
expect(find.text('s1'), findsOneWidget); // Surface tab
expect(find.text('Ordered Success'), findsOneWidget); // Content
});
}
2 changes: 1 addition & 1 deletion examples/custom_backend/test/backend_api_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ void main() {
expect(result, isNotNull);
expect(result!.messages.length, 2);
expect(result.messages[0], isA<SurfaceUpdate>());
expect(result.messages[1], isA<BeginRendering>());
expect(result.messages[1], isA<CreateSurface>());
},
retry: 3,
timeout: const Timeout(Duration(minutes: 2)),
Expand Down
12 changes: 6 additions & 6 deletions examples/travel_app/.metadata
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
# This file should be version controlled and should not be manually edited.

version:
revision: "adc901062556672b4138e18a4dc62a4be8f4b3c2"
revision: "b45fa18946ecc2d9b4009952c636ba7e2ffbb787"
channel: "stable"

project_type: app
Expand All @@ -13,11 +13,11 @@ project_type: app
migration:
platforms:
- platform: root
create_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2
base_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2
- platform: linux
create_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2
base_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2
create_revision: b45fa18946ecc2d9b4009952c636ba7e2ffbb787
base_revision: b45fa18946ecc2d9b4009952c636ba7e2ffbb787
- platform: macos
create_revision: b45fa18946ecc2d9b4009952c636ba7e2ffbb787
base_revision: b45fa18946ecc2d9b4009952c636ba7e2ffbb787

# User provided section

Expand Down
121 changes: 10 additions & 111 deletions examples/travel_app/lib/src/travel_planner_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -353,11 +353,9 @@ the user can return to the main booking flow once they have done some research.

## Controlling the UI

Use the provided tools to build and manage the user interface in response to the
user's requests. To display or update a UI, you must first call the
`surfaceUpdate` tool to define all the necessary components. After defining the
components, you must call the `beginRendering` tool to specify the root
component that should be displayed.
To display or update a UI, you must output a `surfaceUpdate` message in JSONL format.
The `surfaceUpdate` message must define all necessary components and specify the root
component in the `components` list. The root component must have the ID "root".

- Adding surfaces: Most of the time, you should only add new surfaces to the
conversation. This is less confusing for the user, because they can easily
Expand All @@ -368,9 +366,6 @@ component that should be displayed.
user because it avoids confusing the conversation with many versions of the
same itinerary etc.

Once you add or update a surface and are waiting for user input, the
conversation turn is complete, and you should call the provideFinalOutput tool.

If you are displaying more than one component, you should use a `Column` widget
as the root and add the other components as children.

Expand Down Expand Up @@ -433,110 +428,14 @@ ${_imagesJson ?? ''}

## Example

Here is an example of the arguments to the `surfaceUpdate` tool. Note that the
`root` widget ID must be present in the `widgets` list, and it should contain
the other widgets.

```json
{
"surfaceId": "mexico_trip_planner",
"definition": {
"root": "root_column",
"widgets": [
{
"id": "root_column",
"widget": {
"Column": {
"children": ["trip_title", "itinerary"]
}
}
},
{
"id": "trip_title",
"widget": {
"Text": {
"text": "Trip to Mexico City"
}
}
},
{
"id": "itinerary",
"widget": {
"ItineraryWithDetails": {
"title": "Mexico City Adventure",
"subheading": "3-day Itinerary",
"imageChildId": "mexico_city_image",
"child": "itinerary_details"
}
}
},
{
"id": "mexico_city_image",
"widget": {
"Image": {
"location": "assets/travel_images/mexico_city.jpg"
}
}
},
{
"id": "itinerary_details",
"widget": {
"Column": {
"children": ["day1"]
}
}
},
{
"id": "day1",
"widget": {
"ItineraryDay": {
"title": "Day 1",
"subtitle": "Arrival and Exploration",
"description": "Your first day in Mexico City will be focused on settling in and exploring the historic center.",
"imageChildId": "day1_image",
"children": ["day1_entry1", "day1_entry2"]
}
}
},
{
"id": "day1_image",
"widget": {
"Image": {
"location": "assets/travel_images/mexico_city.jpg"
}
}
},
{
"id": "day1_entry1",
"widget": {
"ItineraryEntry": {
"type": "transport",
"title": "Arrival at MEX Airport",
"time": "2:00 PM",
"bodyText": "Arrive at Mexico City International Airport (MEX), clear customs, and pick up your luggage.",
"status": "noBookingRequired"
}
}
},
{
"id": "day1_entry2",
"widget": {
"ItineraryEntry": {
"type": "activity",
"title": "Explore the Zocalo",
"subtitle": "Historic Center",
"time": "4:00 PM - 6:00 PM",
"address": "Plaza de la Constitución S/N, Centro Histórico, Ciudad de México",
"bodyText": "Head to the Zocalo, the main square of Mexico City. Visit the Metropolitan Cathedral and the National Palace.",
"status": "noBookingRequired"
}
}
}
]
}
}
Here is an example of a `surfaceUpdate` message. Note that the `root` component
ID must be present in the `components` list, and it should contain the other
components.

```jsonl
{"surfaceUpdate":{"surfaceId":"mexico_trip_planner","components":[{"id":"root","props":{"component":"Column","children":{"explicitList":["trip_title","itinerary"]}}},{"id":"trip_title","props":{"component":"Text","text":{"literalString":"Trip to Mexico City"}}},{"id":"itinerary","props":{"component":"Itinerary","title":{"literalString":"Mexico City Adventure"},"subheading":{"literalString":"3-day Itinerary"},"imageChildId":"mexico_city_image","days":[{"title":{"literalString":"Day 1"},"subtitle":{"literalString":"Arrival and Exploration"},"description":{"literalString":"Your first day in Mexico City will be focused on settling in and exploring the historic center."},"imageChildId":"day1_image","entries":[{"type":"transport","title":{"literalString":"Arrival at MEX Airport"},"time":{"literalString":"2:00 PM"},"bodyText":{"literalString":"Arrive at Mexico City International Airport (MEX), clear customs, and pick up your luggage."},"status":"noBookingRequired"},{"type":"activity","title":{"literalString":"Explore the Zocalo"},"subtitle":{"literalString":"Historic Center"},"time":{"literalString":"4:00 PM - 6:00 PM"},"address":{"literalString":"Plaza de la Constitución S/N, Centro Histórico, Ciudad de México"},"bodyText":{"literalString":"Head to the Zocalo, the main square of Mexico City. Visit the Metropolitan Cathedral and the National Palace."},"status":"noBookingRequired"}]}]}},{"id":"mexico_city_image","props":{"component":"Image","url":{"literalString":"assets/travel_images/mexico_city.jpg"}}},{"id":"day1_image","props":{"component":"Image","url":{"literalString":"assets/travel_images/mexico_city.jpg"}}}]}}
```

When updating or showing UIs, **ALWAYS** use the surfaceUpdate tool to supply
When updating or showing UIs, **ALWAYS** send a `surfaceUpdate` message to supply
them. Prefer to collect and show information by creating a UI for it.
''';
Loading
Loading