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
22 changes: 10 additions & 12 deletions examples/generic_chat/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,16 @@ import 'package:flutter/material.dart';
import 'package:flutter_genui/flutter_genui.dart';

import 'firebase_options.dart';
import 'src/core_catalog.dart';
import 'src/widget_tree_llm_adapter.dart';

final systemPrompt =
'''You are a helpful assistant who figures out what the user wants to do and then helps suggest options so they can develop a plan and find relevant information.

The user will ask questions, and you will respond by generating appropriate UI elements. Typically, you will first elicit more information to understand the user's needs, then you will start displaying information and the user's plans.
The user will ask questions, and you will respond by generating appropriate UI elements. Typically, you will first elicit more information to understand the user's needs, then you will start displaying information and the user's plans.

For example, the user may say "I want to plan a trip to Mexico". You will first ask some questions by displaying a combination of UI elements, such as a slider to choose budget, options showing activity preferences etc. Then you will walk the user through choosing a hotel, flight and accomodation.
For example, the user may say "I want to plan a trip to Mexico". You will first ask some questions by displaying a combination of UI elements, such as a slider to choose budget, options showing activity preferences etc. Then you will walk the user through choosing a hotel, flight and accomodation.

Typically, you should not update existing surfaces and instead just continually "add" new ones.
''';
Typically, you should not update existing surfaces and instead just continually "add" new ones.
''';

void main() async {
WidgetsFlutterBinding.ensureInitialized();
Expand Down Expand Up @@ -62,7 +60,7 @@ class GenUIHomePage extends StatefulWidget {

class _GenUIHomePageState extends State<GenUIHomePage> {
final _promptController = TextEditingController();
late final WidgetTreeLlmAdapter _widgetTreeLlmAdapter;
late final ConversationManager _conversationManager;

@override
void initState() {
Expand All @@ -73,7 +71,7 @@ class _GenUIHomePageState extends State<GenUIHomePage> {
debugPrint('[$severity] $message');
},
);
_widgetTreeLlmAdapter = WidgetTreeLlmAdapter(
_conversationManager = ConversationManager(
coreCatalog,
systemPrompt,
aiClient,
Expand All @@ -83,14 +81,14 @@ class _GenUIHomePageState extends State<GenUIHomePage> {
@override
void dispose() {
_promptController.dispose();
_widgetTreeLlmAdapter.dispose();
_conversationManager.dispose();
super.dispose();
}

void _sendPrompt() {
final prompt = _promptController.text;
if (prompt.isNotEmpty) {
_widgetTreeLlmAdapter.sendUserPrompt(prompt);
_conversationManager.sendUserPrompt(prompt);
_promptController.clear();
}
}
Expand All @@ -108,7 +106,7 @@ class _GenUIHomePageState extends State<GenUIHomePage> {
child: Column(
children: [
Expanded(
child: _widgetTreeLlmAdapter.widget(),
child: _conversationManager.widget(),
),
Padding(
padding: const EdgeInsets.all(8.0),
Expand All @@ -128,7 +126,7 @@ class _GenUIHomePageState extends State<GenUIHomePage> {
onPressed: _sendPrompt,
),
StreamBuilder<bool>(
stream: _widgetTreeLlmAdapter.loadingStream,
stream: _conversationManager.loadingStream,
initialData: false,
builder: (context, snapshot) {
if (snapshot.data ?? false) {
Expand Down
16 changes: 0 additions & 16 deletions examples/generic_chat/lib/src/core_catalog.dart

This file was deleted.

8 changes: 4 additions & 4 deletions examples/generic_chat/macos/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -111,9 +111,9 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS:
AppCheckCore: cc8fd0a3a230ddd401f326489c99990b013f0c4f
Firebase: d99ac19b909cd2c548339c2241ecd0d1599ab02e
firebase_app_check: 61f24e87e25134752f63264d9bcb8198f96f241b
firebase_auth: 693f1e1ef2bb11a241d4478e63f1f47676af0538
firebase_core: 7667f880631ae8ad10e3d6567ab7582fe0682326
firebase_app_check: d3fa7155214c56f6c5f656c3a40ad5a025bf91b6
firebase_auth: a48c22017e8a04bdd95b0641f5c59cbdbf04440c
firebase_core: 2af692f4818474ed52eda1ba6aeb448a6a3352af
FirebaseAppCheck: 4574d7180be2a8b514f588099fc5262f032a92c7
FirebaseAppCheckInterop: 06fe5a3799278ae4667e6c432edd86b1030fa3df
FirebaseAuth: a6575e5fbf46b046c58dc211a28a5fbdd8d4c83b
Expand All @@ -128,4 +128,4 @@ SPEC CHECKSUMS:

PODFILE CHECKSUM: 54d867c82ac51cbd61b565781b9fada492027009

COCOAPODS: 1.16.2
COCOAPODS: 1.15.2
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,17 @@ import 'package:flutter/material.dart';
import 'package:flutter_genui/flutter_genui.dart';

import 'catalog.dart';
import 'chat_message.dart';
import 'conversation_widget.dart';
import 'event_debouncer.dart';
import 'ui_models.dart';

class WidgetTreeLlmAdapter {
WidgetTreeLlmAdapter(this.catalog, this.systemInstruction, this.llmConnection) {
import 'src/chat_message.dart';
import 'src/conversation_widget.dart';
import 'src/event_debouncer.dart';
import 'src/ui_models.dart';

class ConversationManager {
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

According to the repository's style guide, code should be tested.1 As ConversationManager is a core component being moved into the package, this would be a great time to add unit tests for it. Creating a conversation_manager_test.dart with tests for methods like sendUserPrompt and _handleEvents (perhaps by making it package-private for testing) using a mock LlmConnection would significantly improve the package's quality and maintainability.

Style Guide References

Footnotes

  1. The style guide states that code should be tested. (link)

ConversationManager(
this.catalog,
this.systemInstruction,
this.llmConnection,
) {
_eventDebouncer = EventDebouncer(callback: _handleEvents);
}

Expand Down Expand Up @@ -72,19 +76,26 @@ class WidgetTreeLlmAdapter {
continue;
}
for (final event in entry.value) {
final functionResponse =
FunctionResponse(event.widgetId, event.toMap());
surfaceConversation.add(Content.functionResponse(
functionResponse.name, functionResponse.response));
final functionResponse = FunctionResponse(
event.widgetId,
event.toMap(),
);
surfaceConversation.add(
Content.functionResponse(
functionResponse.name,
functionResponse.response,
),
);
}
surfaceConversation.add(Content.text(
surfaceConversation.add(
Content.text(
'The user has interacted with the UI surface named "$surfaceId". '
'Consolidate the UI events and update the UI accordingly. Respond '
'with an updated UI definition. You may update any of the '
'surfaces, or delete them if they are no longer needed.'));
_generateAndSendResponse(
conversation: surfaceConversation,
'surfaces, or delete them if they are no longer needed.',
),
);
_generateAndSendResponse(conversation: surfaceConversation);
}
}

Expand Down Expand Up @@ -116,13 +127,12 @@ class WidgetTreeLlmAdapter {
actionMap['definition'] as Map<String, Object?>;
final newConversation = List<Content>.from(conversation);
conversationsBySurfaceId[surfaceId] = newConversation;
_chatHistory.add(UiResponse(
definition: {
'surfaceId': surfaceId,
...definition,
},
surfaceId: surfaceId,
));
_chatHistory.add(
UiResponse(
definition: {'surfaceId': surfaceId, ...definition},
surfaceId: surfaceId,
),
);
case 'update':
final definition =
actionMap['definition'] as Map<String, Object?>;
Expand All @@ -133,16 +143,16 @@ class WidgetTreeLlmAdapter {
if (oldResponse != null) {
final index = _chatHistory.indexOf(oldResponse);
_chatHistory[index] = UiResponse(
definition: {
'surfaceId': surfaceId,
...definition,
},
surfaceId: surfaceId);
definition: {'surfaceId': surfaceId, ...definition},
surfaceId: surfaceId,
);
}
case 'delete':
conversationsBySurfaceId.remove(surfaceId);
_chatHistory.removeWhere((message) =>
message is UiResponse && message.surfaceId == surfaceId);
_chatHistory.removeWhere(
(message) =>
message is UiResponse && message.surfaceId == surfaceId,
);
}
}
}
Expand Down Expand Up @@ -224,19 +234,17 @@ class WidgetTreeLlmAdapter {

Widget widget() {
return StreamBuilder(
stream: uiDataStream,
initialData: const <ChatMessage>[],
builder: (context, snapshot) {
if (snapshot.hasData) {
return ConversationWidget(
messages: snapshot.data!,
catalog: catalog,
onEvent: (event) {
_eventDebouncer.add(UiEvent.fromMap(event));
});
} else {
return const Center(child: CircularProgressIndicator());
}
});
stream: uiDataStream,
initialData: const <ChatMessage>[],
builder: (context, snapshot) {
return ConversationWidget(
messages: snapshot.data!,
catalog: catalog,
onEvent: (event) {
_eventDebouncer.add(UiEvent.fromMap(event));
},
);
},
);
}
}
}
16 changes: 16 additions & 0 deletions pkgs/flutter_genui/lib/core_catalog.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import 'catalog.dart';
import 'core_widgets/checkbox_group.dart';
import 'core_widgets/column.dart';
import 'core_widgets/elevated_button.dart';
import 'core_widgets/radio_group.dart';
import 'core_widgets/text.dart';
import 'core_widgets/text_field.dart';

final coreCatalog = Catalog([
elevatedButtonCatalogItem,
columnCatalogItem,
text,
checkboxGroup,
radioGroup,
textField,
]);
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import 'package:flutter/material.dart';

import 'catalog.dart';
import 'ui_models.dart';
import 'src/ui_models.dart';

/// A widget that builds a UI dynamically from a JSON-like definition.
///
Expand Down
17 changes: 15 additions & 2 deletions pkgs/flutter_genui/lib/flutter_genui.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,19 @@
export 'ai_client/ai_client.dart';
export 'ai_client/llm_connection.dart';
export 'catalog.dart';
export 'catalog_item.dart';
export 'conversation_manager.dart';
export 'core_catalog.dart';
export 'core_widgets/checkbox_group.dart';
export 'core_widgets/column.dart';
export 'core_widgets/elevated_button.dart';
export 'core_widgets/radio_group.dart';
export 'core_widgets/text.dart';
export 'core_widgets/text_field.dart';
export 'dynamic_ui.dart';
export 'src/event_debouncer.dart';
export 'src/ui_models.dart';
export 'to_merge/agent/agent.dart';
export 'to_merge/model/controller.dart';
export 'to_merge/model/image_catalog.dart';
export 'to_merge/model/input.dart';
export 'ai_client/ai_client.dart';
export 'ai_client/llm_connection.dart';
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import 'package:flutter/material.dart';

import 'catalog.dart';
import 'chat_message.dart';
import 'dynamic_ui.dart';
import '../catalog.dart';
import '../dynamic_ui.dart';
import 'ui_models.dart';
import 'chat_message.dart';

class ConversationWidget extends StatelessWidget {
const ConversationWidget({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,5 @@
import 'dart:convert';

/// Extension to provide JSON stringification for map-based objects.
extension JsonEncodeMap on Map<String, Object?> {
/// Converts this map object to a JSON string.
///
/// If an [indent] is provided, the output will be formatted with that indent.
String toJsonString({String indent = ''}) {
if (indent.isNotEmpty) {
return JsonEncoder.withIndent(indent).convert(this);
}
return const JsonEncoder().convert(this);
}
}

/// A data object that represents a user interaction event in the UI.
///
/// This is used to send information from the client to the AI about user
Expand Down Expand Up @@ -53,22 +40,6 @@ extension type UiEvent.fromMap(Map<String, Object?> _json) {
Map<String, Object?> toMap() => _json;
}

/// A data object that represents a state update for a widget.
///
/// This is sent from the AI to the client to dynamically change the properties
/// of a widget that is already on screen.
extension type UiStateUpdate.fromMap(Map<String, Object?> _json) {
/// The ID of the surface to update.
String get surfaceId => _json['surfaceId'] as String;

/// The ID of the widget to update.
String get widgetId => _json['widgetId'] as String;

/// A map of the new properties to apply to the widget. These will be merged
/// with the existing properties of the widget.
Map<String, Object?> get props => _json['props'] as Map<String, Object?>;
}

/// A data object that represents the entire UI definition.
///
/// This is the root object that defines a complete UI to be rendered.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:genui_client/src/catalog.dart';
import 'package:genui_client/src/widgets/elevated_button.dart';
import 'package:genui_client/src/widgets/text.dart';
import 'package:genui_client/src/dynamic_ui.dart';
import 'package:genui_client/src/ui_models.dart';
import 'package:flutter_genui/flutter_genui.dart';

void main() {
final testCatalog = Catalog([elevatedButtonCatalogItem, text]);
Expand Down