-
Notifications
You must be signed in to change notification settings - Fork 135
add an examples app that has a gallery of pre-designed surfaces and allows editing/previewing A2UI JSON
#761
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
26e3305
b9be554
20c5bc3
41d2f9c
42a4c8d
16dbbf1
f727d87
f4cfa59
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| # Miscellaneous | ||
| *.class | ||
| *.log | ||
| *.pyc | ||
| *.swp | ||
| .DS_Store | ||
| .atom/ | ||
| .build/ | ||
| .buildlog/ | ||
| .history | ||
| .svn/ | ||
| .swiftpm/ | ||
| migrate_working_dir/ | ||
|
|
||
| # IntelliJ related | ||
| *.iml | ||
| *.ipr | ||
| *.iws | ||
| .idea/ | ||
|
|
||
| # The .vscode folder contains launch configuration and tasks you configure in | ||
| # VS Code which you may wish to be included in version control, so this line | ||
| # is commented out by default. | ||
| #.vscode/ | ||
|
|
||
| # Flutter/Dart/Pub related | ||
| **/doc/api/ | ||
| **/ios/Flutter/.last_build_id | ||
| .dart_tool/ | ||
| .flutter-plugins-dependencies | ||
| .pub-cache/ | ||
| .pub/ | ||
| /build/ | ||
| /coverage/ | ||
|
|
||
| # Symbolication related | ||
| app.*.symbols | ||
|
|
||
| # Obfuscation related | ||
| app.*.map.json | ||
|
|
||
| # Android Studio will place build artifacts here | ||
| /android/app/debug | ||
| /android/app/profile | ||
| /android/app/release |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| # This file tracks properties of this Flutter project. | ||
| # Used by Flutter tool to assess capabilities and perform upgrades etc. | ||
| # | ||
| # This file should be version controlled and should not be manually edited. | ||
|
|
||
| version: | ||
| revision: "44a626f4f0027bc38a46dc68aed5964b05a83c18" | ||
| channel: "stable" | ||
|
|
||
| project_type: app | ||
|
|
||
| # Tracks metadata for the flutter migrate command | ||
| migration: | ||
| platforms: | ||
| - platform: root | ||
| create_revision: 44a626f4f0027bc38a46dc68aed5964b05a83c18 | ||
| base_revision: 44a626f4f0027bc38a46dc68aed5964b05a83c18 | ||
| - platform: macos | ||
| create_revision: 44a626f4f0027bc38a46dc68aed5964b05a83c18 | ||
| base_revision: 44a626f4f0027bc38a46dc68aed5964b05a83c18 | ||
|
|
||
| # User provided section | ||
|
|
||
| # List of Local paths (relative to this file) that should be | ||
| # ignored by the migrate tool. | ||
| # | ||
| # Files that are not part of the templates will be ignored by default. | ||
| unmanaged_files: | ||
| - 'lib/main.dart' | ||
| - 'ios/Runner.xcodeproj/project.pbxproj' |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| # GenUI Composer | ||
|
|
||
| An application for experimenting with and building A2UI surfaces. | ||
|
|
||
| The composer allows you to: | ||
|
|
||
| - Generate a UI surface from a text prompt using AI. | ||
| - Browse a gallery of pre-designed surfaces. | ||
| - View and edit the underlying A2UI JSON for any surface and see a live preview. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| # Copyright 2025 The Flutter Authors. | ||
| # Use of this source code is governed by a BSD-style license that can be | ||
| # found in the LICENSE file. | ||
|
|
||
| include: package:flutter_lints/flutter.yaml |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| // Copyright 2025 The Flutter Authors. | ||
| // Use of this source code is governed by a BSD-style license that can be | ||
| // found in the LICENSE file. | ||
|
|
||
| import 'dart:async'; | ||
|
|
||
| import 'package:dartantic_ai/dartantic_ai.dart' as dartantic; | ||
|
|
||
| import 'get_api_key.dart'; | ||
|
|
||
| /// An abstract interface for AI clients. | ||
| abstract interface class AiClient { | ||
| /// Sends a message stream request to the AI service. | ||
| Stream<String> sendStream( | ||
| String prompt, { | ||
| required List<dartantic.ChatMessage> history, | ||
| }); | ||
|
|
||
| /// Dispose of resources. | ||
| void dispose(); | ||
| } | ||
|
|
||
| /// An implementation of [AiClient] using `package:dartantic_ai`. | ||
| class DartanticAiClient implements AiClient { | ||
| DartanticAiClient({String? modelName}) { | ||
| final String apiKey = getApiKey(); | ||
| _provider = dartantic.GoogleProvider(apiKey: apiKey); | ||
| _agent = dartantic.Agent.forProvider( | ||
| _provider, | ||
| chatModelName: modelName ?? 'gemini-3-flash-preview', | ||
| ); | ||
| } | ||
|
|
||
| late final dartantic.GoogleProvider _provider; | ||
| late final dartantic.Agent _agent; | ||
|
|
||
| @override | ||
| Stream<String> sendStream( | ||
| String prompt, { | ||
| required List<dartantic.ChatMessage> history, | ||
| }) async* { | ||
| final Stream<dartantic.ChatResult<String>> stream = _agent.sendStream( | ||
| prompt, | ||
| history: history, | ||
| ); | ||
|
|
||
| await for (final result in stream) { | ||
| if (result.output.isNotEmpty) { | ||
| yield result.output; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| @override | ||
| void dispose() {} | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| // Copyright 2025 The Flutter Authors. | ||
| // Use of this source code is governed by a BSD-style license that can be | ||
| // found in the LICENSE file. | ||
|
|
||
| import 'dart:async'; | ||
|
|
||
| import 'package:dartantic_ai/dartantic_ai.dart' as dartantic; | ||
| import 'package:genui/genui.dart'; | ||
| import 'package:logging/logging.dart'; | ||
|
|
||
| import 'ai_client.dart'; | ||
|
|
||
| /// A [Transport] that wraps an [AiClient] to communicate with an LLM. | ||
| class AiClientTransport implements Transport { | ||
| AiClientTransport({required this.aiClient}); | ||
|
|
||
| final AiClient aiClient; | ||
| final A2uiTransportAdapter _adapter = A2uiTransportAdapter(); | ||
| final List<dartantic.ChatMessage> _history = []; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The |
||
| final Logger _logger = Logger('AiClientTransport'); | ||
|
|
||
| @override | ||
| Stream<A2uiMessage> get incomingMessages => _adapter.incomingMessages; | ||
|
|
||
| @override | ||
| Stream<String> get incomingText => _adapter.incomingText; | ||
|
|
||
| @override | ||
| Future<void> sendRequest(ChatMessage message) async { | ||
| final buffer = StringBuffer(); | ||
| for (final dartantic.StandardPart part in message.parts) { | ||
| if (part.isUiInteractionPart) { | ||
| buffer.write(part.asUiInteractionPart!.interaction); | ||
| } else if (part is TextPart) { | ||
| buffer.write(part.text); | ||
| } | ||
| } | ||
| final text = buffer.toString(); | ||
| if (text.isEmpty) return; | ||
|
|
||
| _history.add(dartantic.ChatMessage.user(text)); | ||
|
|
||
| try { | ||
| final Stream<String> stream = aiClient.sendStream( | ||
| text, | ||
| history: List.of(_history), | ||
| ); | ||
| final fullResponseBuffer = StringBuffer(); | ||
|
|
||
| await for (final chunk in stream) { | ||
| if (chunk.isNotEmpty) { | ||
| fullResponseBuffer.write(chunk); | ||
| _adapter.addChunk(chunk); | ||
| } | ||
| } | ||
|
|
||
| _history.add(dartantic.ChatMessage.model(fullResponseBuffer.toString())); | ||
| } catch (e, stack) { | ||
| _logger.severe('Error sending request', e, stack); | ||
| rethrow; | ||
| } | ||
| } | ||
|
|
||
| @override | ||
| void dispose() { | ||
| _adapter.dispose(); | ||
| aiClient.dispose(); | ||
| } | ||
|
|
||
| /// Adds a system message to the history. | ||
| void addSystemMessage(String content) { | ||
| _history.add(dartantic.ChatMessage.system(content)); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| // Copyright 2025 The Flutter Authors. | ||
| // Use of this source code is governed by a BSD-style license that can be | ||
| // found in the LICENSE file. | ||
|
|
||
| import 'dart:convert'; | ||
|
|
||
| import 'package:flutter/material.dart'; | ||
| import 'package:genui/genui.dart'; | ||
|
|
||
| /// The Components tab shows every component in the standard catalog using | ||
| /// the built-in [DebugCatalogView]. | ||
| class ComponentsTab extends StatefulWidget { | ||
| const ComponentsTab({super.key}); | ||
|
|
||
| @override | ||
| State<ComponentsTab> createState() => _ComponentsTabState(); | ||
| } | ||
|
|
||
| class _ComponentsTabState extends State<ComponentsTab> { | ||
| late final Catalog _catalog; | ||
|
|
||
| @override | ||
| void initState() { | ||
| super.initState(); | ||
| _catalog = BasicCatalogItems.asCatalog(); | ||
| } | ||
|
|
||
| @override | ||
| Widget build(BuildContext context) { | ||
| final theme = Theme.of(context); | ||
|
|
||
| return Column( | ||
| crossAxisAlignment: CrossAxisAlignment.start, | ||
| children: [ | ||
| Padding( | ||
| padding: const EdgeInsets.fromLTRB(16, 16, 16, 0), | ||
| child: Text('Components', style: theme.textTheme.headlineSmall), | ||
| ), | ||
| const SizedBox(height: 8), | ||
| Expanded( | ||
| child: DebugCatalogView( | ||
| catalog: _catalog, | ||
| onSubmit: (message) { | ||
| ScaffoldMessenger.of(context).showSnackBar( | ||
| SnackBar( | ||
| content: Text( | ||
| 'User action: ' | ||
| '${jsonEncode(message.parts.last)}', | ||
| ), | ||
| ), | ||
| ); | ||
| }, | ||
| ), | ||
| ), | ||
| ], | ||
| ); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add instructions for how to run this, how to specify API key etc?