Skip to content
Open
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
45 changes: 45 additions & 0 deletions examples/composer/.gitignore
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
30 changes: 30 additions & 0 deletions examples/composer/.metadata
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'
9 changes: 9 additions & 0 deletions examples/composer/README.md
Copy link
Collaborator

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?

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.
5 changes: 5 additions & 0 deletions examples/composer/analysis_options.yaml
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
56 changes: 56 additions & 0 deletions examples/composer/lib/ai_client.dart
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() {}
}
74 changes: 74 additions & 0 deletions examples/composer/lib/ai_client_transport.dart
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 = [];
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

The _history list grows indefinitely with each request. For long-running sessions, this could lead to high memory usage and large context windows being sent to the AI, which can be expensive and may hit token limits. Consider implementing a strategy to prune the history, such as keeping only the last N messages or tokens.

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));
}
}
58 changes: 58 additions & 0 deletions examples/composer/lib/components_tab.dart
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)}',
),
),
);
},
),
),
],
);
}
}
Loading
Loading