diff --git a/examples/generic_chat/lib/main.dart b/examples/generic_chat/lib/main.dart index e75b82ecf..084b65599 100644 --- a/examples/generic_chat/lib/main.dart +++ b/examples/generic_chat/lib/main.dart @@ -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(); @@ -62,7 +60,7 @@ class GenUIHomePage extends StatefulWidget { class _GenUIHomePageState extends State { final _promptController = TextEditingController(); - late final WidgetTreeLlmAdapter _widgetTreeLlmAdapter; + late final ConversationManager _conversationManager; @override void initState() { @@ -73,7 +71,7 @@ class _GenUIHomePageState extends State { debugPrint('[$severity] $message'); }, ); - _widgetTreeLlmAdapter = WidgetTreeLlmAdapter( + _conversationManager = ConversationManager( coreCatalog, systemPrompt, aiClient, @@ -83,14 +81,14 @@ class _GenUIHomePageState extends State { @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(); } } @@ -108,7 +106,7 @@ class _GenUIHomePageState extends State { child: Column( children: [ Expanded( - child: _widgetTreeLlmAdapter.widget(), + child: _conversationManager.widget(), ), Padding( padding: const EdgeInsets.all(8.0), @@ -128,7 +126,7 @@ class _GenUIHomePageState extends State { onPressed: _sendPrompt, ), StreamBuilder( - stream: _widgetTreeLlmAdapter.loadingStream, + stream: _conversationManager.loadingStream, initialData: false, builder: (context, snapshot) { if (snapshot.data ?? false) { diff --git a/examples/generic_chat/lib/src/core_catalog.dart b/examples/generic_chat/lib/src/core_catalog.dart deleted file mode 100644 index 19eacdfdc..000000000 --- a/examples/generic_chat/lib/src/core_catalog.dart +++ /dev/null @@ -1,16 +0,0 @@ -import 'catalog.dart'; -import 'widgets/checkbox_group.dart'; -import 'widgets/column.dart'; -import 'widgets/elevated_button.dart'; -import 'widgets/radio_group.dart'; -import 'widgets/text.dart'; -import 'widgets/text_field.dart'; - -final coreCatalog = Catalog([ - elevatedButtonCatalogItem, - columnCatalogItem, - text, - checkboxGroup, - radioGroup, - textField, -]); diff --git a/examples/generic_chat/macos/Podfile.lock b/examples/generic_chat/macos/Podfile.lock index edc6c5715..93b26a9a5 100644 --- a/examples/generic_chat/macos/Podfile.lock +++ b/examples/generic_chat/macos/Podfile.lock @@ -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 @@ -128,4 +128,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 54d867c82ac51cbd61b565781b9fada492027009 -COCOAPODS: 1.16.2 +COCOAPODS: 1.15.2 diff --git a/examples/generic_chat/lib/src/catalog.dart b/pkgs/flutter_genui/lib/catalog.dart similarity index 100% rename from examples/generic_chat/lib/src/catalog.dart rename to pkgs/flutter_genui/lib/catalog.dart diff --git a/examples/generic_chat/lib/src/catalog_item.dart b/pkgs/flutter_genui/lib/catalog_item.dart similarity index 100% rename from examples/generic_chat/lib/src/catalog_item.dart rename to pkgs/flutter_genui/lib/catalog_item.dart diff --git a/examples/generic_chat/lib/src/widget_tree_llm_adapter.dart b/pkgs/flutter_genui/lib/conversation_manager.dart similarity index 82% rename from examples/generic_chat/lib/src/widget_tree_llm_adapter.dart rename to pkgs/flutter_genui/lib/conversation_manager.dart index bdf749d13..ffd08f739 100644 --- a/examples/generic_chat/lib/src/widget_tree_llm_adapter.dart +++ b/pkgs/flutter_genui/lib/conversation_manager.dart @@ -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 { + ConversationManager( + this.catalog, + this.systemInstruction, + this.llmConnection, + ) { _eventDebouncer = EventDebouncer(callback: _handleEvents); } @@ -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); } } @@ -116,13 +127,12 @@ class WidgetTreeLlmAdapter { actionMap['definition'] as Map; final newConversation = List.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; @@ -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, + ); } } } @@ -224,19 +234,17 @@ class WidgetTreeLlmAdapter { Widget widget() { return StreamBuilder( - stream: uiDataStream, - initialData: const [], - 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 [], + builder: (context, snapshot) { + return ConversationWidget( + messages: snapshot.data!, + catalog: catalog, + onEvent: (event) { + _eventDebouncer.add(UiEvent.fromMap(event)); + }, + ); + }, + ); } -} \ No newline at end of file +} diff --git a/pkgs/flutter_genui/lib/core_catalog.dart b/pkgs/flutter_genui/lib/core_catalog.dart new file mode 100644 index 000000000..cad794031 --- /dev/null +++ b/pkgs/flutter_genui/lib/core_catalog.dart @@ -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, +]); diff --git a/examples/generic_chat/lib/src/widgets/checkbox_group.dart b/pkgs/flutter_genui/lib/core_widgets/checkbox_group.dart similarity index 100% rename from examples/generic_chat/lib/src/widgets/checkbox_group.dart rename to pkgs/flutter_genui/lib/core_widgets/checkbox_group.dart diff --git a/examples/generic_chat/lib/src/widgets/column.dart b/pkgs/flutter_genui/lib/core_widgets/column.dart similarity index 100% rename from examples/generic_chat/lib/src/widgets/column.dart rename to pkgs/flutter_genui/lib/core_widgets/column.dart diff --git a/examples/generic_chat/lib/src/widgets/elevated_button.dart b/pkgs/flutter_genui/lib/core_widgets/elevated_button.dart similarity index 100% rename from examples/generic_chat/lib/src/widgets/elevated_button.dart rename to pkgs/flutter_genui/lib/core_widgets/elevated_button.dart diff --git a/examples/generic_chat/lib/src/widgets/radio_group.dart b/pkgs/flutter_genui/lib/core_widgets/radio_group.dart similarity index 100% rename from examples/generic_chat/lib/src/widgets/radio_group.dart rename to pkgs/flutter_genui/lib/core_widgets/radio_group.dart diff --git a/examples/generic_chat/lib/src/widgets/text.dart b/pkgs/flutter_genui/lib/core_widgets/text.dart similarity index 100% rename from examples/generic_chat/lib/src/widgets/text.dart rename to pkgs/flutter_genui/lib/core_widgets/text.dart diff --git a/examples/generic_chat/lib/src/widgets/text_field.dart b/pkgs/flutter_genui/lib/core_widgets/text_field.dart similarity index 100% rename from examples/generic_chat/lib/src/widgets/text_field.dart rename to pkgs/flutter_genui/lib/core_widgets/text_field.dart diff --git a/examples/generic_chat/lib/src/dynamic_ui.dart b/pkgs/flutter_genui/lib/dynamic_ui.dart similarity index 98% rename from examples/generic_chat/lib/src/dynamic_ui.dart rename to pkgs/flutter_genui/lib/dynamic_ui.dart index 1d2f9d004..ee9a3c7c1 100644 --- a/examples/generic_chat/lib/src/dynamic_ui.dart +++ b/pkgs/flutter_genui/lib/dynamic_ui.dart @@ -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. /// diff --git a/pkgs/flutter_genui/lib/flutter_genui.dart b/pkgs/flutter_genui/lib/flutter_genui.dart index 02aba0dae..26d83d4b5 100644 --- a/pkgs/flutter_genui/lib/flutter_genui.dart +++ b/pkgs/flutter_genui/lib/flutter_genui.dart @@ -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'; diff --git a/examples/generic_chat/lib/src/chat_message.dart b/pkgs/flutter_genui/lib/src/chat_message.dart similarity index 100% rename from examples/generic_chat/lib/src/chat_message.dart rename to pkgs/flutter_genui/lib/src/chat_message.dart diff --git a/examples/generic_chat/lib/src/conversation_widget.dart b/pkgs/flutter_genui/lib/src/conversation_widget.dart similarity index 97% rename from examples/generic_chat/lib/src/conversation_widget.dart rename to pkgs/flutter_genui/lib/src/conversation_widget.dart index 59f7cf314..e9a6d01af 100644 --- a/examples/generic_chat/lib/src/conversation_widget.dart +++ b/pkgs/flutter_genui/lib/src/conversation_widget.dart @@ -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({ diff --git a/examples/generic_chat/lib/src/event_debouncer.dart b/pkgs/flutter_genui/lib/src/event_debouncer.dart similarity index 100% rename from examples/generic_chat/lib/src/event_debouncer.dart rename to pkgs/flutter_genui/lib/src/event_debouncer.dart diff --git a/examples/generic_chat/lib/src/ui_models.dart b/pkgs/flutter_genui/lib/src/ui_models.dart similarity index 67% rename from examples/generic_chat/lib/src/ui_models.dart rename to pkgs/flutter_genui/lib/src/ui_models.dart index 66c81a31c..e574f3b5b 100644 --- a/examples/generic_chat/lib/src/ui_models.dart +++ b/pkgs/flutter_genui/lib/src/ui_models.dart @@ -1,18 +1,5 @@ import 'dart:convert'; -/// Extension to provide JSON stringification for map-based objects. -extension JsonEncodeMap on Map { - /// 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 @@ -53,22 +40,6 @@ extension type UiEvent.fromMap(Map _json) { Map 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 _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 get props => _json['props'] as Map; -} - /// A data object that represents the entire UI definition. /// /// This is the root object that defines a complete UI to be rendered. diff --git a/examples/generic_chat/test/dynamic_ui_test.dart b/pkgs/flutter_genui/test/dynamic_ui_test.dart similarity index 88% rename from examples/generic_chat/test/dynamic_ui_test.dart rename to pkgs/flutter_genui/test/dynamic_ui_test.dart index 2ee3f9837..4263236e6 100644 --- a/examples/generic_chat/test/dynamic_ui_test.dart +++ b/pkgs/flutter_genui/test/dynamic_ui_test.dart @@ -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]);