genui_a2ui 0.6.0 copy "genui_a2ui: ^0.6.0" to clipboard
genui_a2ui: ^0.6.0 copied to clipboard

Integration package for genui and A2UI Streaming UI Protocol.

example/lib/main.dart

// 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:flutter/material.dart';
import 'package:genui/genui.dart';
import 'package:genui_a2ui/genui_a2ui.dart';
import 'package:logging/logging.dart';

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  configureGenUiLogging(level: Level.ALL);
  runApp(const GenUIExampleApp());
}

/// The main application widget.
class GenUIExampleApp extends StatelessWidget {
  /// Creates a [GenUIExampleApp].
  const GenUIExampleApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'A2UI Example',
      theme: ThemeData(primarySwatch: Colors.deepPurple),
      home: const ChatScreen(),
    );
  }
}

/// The main chat screen.
class ChatScreen extends StatefulWidget {
  /// Creates a [ChatScreen].
  const ChatScreen({super.key});

  @override
  State<ChatScreen> createState() => _ChatScreenState();
}

class _ChatScreenState extends State<ChatScreen> {
  final TextEditingController _textController = TextEditingController();
  final A2uiMessageProcessor _a2uiMessageProcessor = A2uiMessageProcessor(
    catalogs: [CoreCatalogItems.asCatalog()],
  );
  late final A2uiContentGenerator _contentGenerator;
  late final GenUiConversation _genUiConversation;
  final List<String> _surfaceIds = ['default'];
  int _currentSurfaceIndex = 0;
  StreamSubscription<GenUiUpdate>? _surfaceSubscription;

  @override
  void initState() {
    super.initState();
    _contentGenerator = A2uiContentGenerator(
      // Replace this with the address of the A2A server (one that supports the
      // A2UI extension) that you wish to connect to.
      serverUrl: Uri.parse('http://localhost:10002'),
    );
    _genUiConversation = GenUiConversation(
      contentGenerator: _contentGenerator,
      a2uiMessageProcessor: _a2uiMessageProcessor,
    );
    // Initialize with existing surfaces
    _surfaceIds.addAll(
      _a2uiMessageProcessor.surfaces.keys.where(
        (id) => !_surfaceIds.contains(id),
      ),
    );

    _surfaceSubscription = _a2uiMessageProcessor.surfaceUpdates.listen((
      update,
    ) {
      if (update is SurfaceAdded) {
        genUiLogger.info('Surface added: ${update.surfaceId}');
        if (!_surfaceIds.contains(update.surfaceId)) {
          setState(() {
            _surfaceIds.add(update.surfaceId);
            // Switch to the new surface
            _currentSurfaceIndex = _surfaceIds.length - 1;
          });
        }
      } else if (update is SurfaceUpdated) {
        genUiLogger.info('Surface updated: ${update.surfaceId}');
        // The surface will redraw itself, but we call setState here to ensure
        // that any other dependent widgets are also updated.
        setState(() {});
      } else if (update is SurfaceRemoved) {
        genUiLogger.info('Surface removed: ${update.surfaceId}');
        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;
              }
            }
          });
        }
      }
    });
  }

  @override
  void dispose() {
    _textController.dispose();
    _genUiConversation.dispose();
    _surfaceSubscription?.cancel();
    _a2uiMessageProcessor.dispose();
    _contentGenerator.dispose();
    super.dispose();
  }

  void _handleSubmitted(String text) {
    _textController.clear();
    _genUiConversation.sendRequest(UserMessage.text(text));
  }

  void _previousSurface() {
    if (_currentSurfaceIndex > 0) {
      setState(() {
        _currentSurfaceIndex--;
      });
    }
  }

  void _nextSurface() {
    if (_currentSurfaceIndex < _surfaceIds.length - 1) {
      setState(() {
        _currentSurfaceIndex++;
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    if (_surfaceIds.isEmpty) {
      return Scaffold(
        appBar: AppBar(title: const Text('A2UI Example')),
        body: const Center(child: Text('No surfaces available.')),
      );
    }
    final String currentSurfaceId = _surfaceIds[_currentSurfaceIndex];
    return Scaffold(
      appBar: AppBar(
        leading: IconButton(
          icon: const Icon(Icons.arrow_back),
          onPressed: _previousSurface,
          tooltip: 'Previous Surface',
          color: _currentSurfaceIndex > 0
              ? null
              : Theme.of(context).disabledColor,
        ),
        title: Text('Surface: $currentSurfaceId'),
        actions: [
          IconButton(
            icon: const Icon(Icons.arrow_forward),
            onPressed: _nextSurface,
            tooltip: 'Next Surface',
            color: _currentSurfaceIndex < _surfaceIds.length - 1
                ? null
                : Theme.of(context).disabledColor,
          ),
        ],
      ),
      body: Row(
        children: <Widget>[
          ConstrainedBox(
            constraints: const BoxConstraints(minWidth: 200, maxWidth: 300),
            child: Column(
              children: <Widget>[
                Expanded(
                  child: ValueListenableBuilder<List<ChatMessage>>(
                    valueListenable: _genUiConversation.conversation,
                    builder: (context, messages, child) {
                      return ListView.builder(
                        padding: const EdgeInsets.all(8.0),
                        reverse: true,
                        itemBuilder: (_, int index) =>
                            _buildMessage(messages.reversed.toList()[index]),
                        itemCount: messages.length,
                      );
                    },
                  ),
                ),
                const Divider(height: 1.0),
                Container(
                  decoration: BoxDecoration(color: Theme.of(context).cardColor),
                  child: _buildTextComposer(),
                ),
              ],
            ),
          ),
          Expanded(
            child: SingleChildScrollView(
              child: GenUiSurface(
                key: ValueKey(currentSurfaceId),
                host: _a2uiMessageProcessor,
                surfaceId: currentSurfaceId,
              ),
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildMessage(ChatMessage message) {
    final isUserMessage = message is UserMessage;
    var text = '';
    if (message is UserMessage) {
      text = message.text;
    } else if (message is AiTextMessage) {
      text = message.text;
    }
    return Container(
      margin: const EdgeInsets.symmetric(vertical: 10.0),
      child: Row(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: <Widget>[
          Container(
            margin: const EdgeInsets.only(right: 16.0),
            child: CircleAvatar(child: Text(isUserMessage ? 'U' : 'A')),
          ),
          Expanded(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: <Widget>[
                Text(
                  isUserMessage ? 'User' : 'Agent',
                  style: const TextStyle(fontWeight: FontWeight.bold),
                ),
                Container(
                  margin: const EdgeInsets.only(top: 5.0),
                  child: Text(text),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildTextComposer() {
    return IconTheme(
      data: IconThemeData(color: Theme.of(context).colorScheme.secondary),
      child: Container(
        margin: const EdgeInsets.symmetric(horizontal: 8.0),
        child: Row(
          children: <Widget>[
            Flexible(
              child: TextField(
                controller: _textController,
                onSubmitted: _handleSubmitted,
                decoration: const InputDecoration.collapsed(
                  hintText: 'Send a message',
                ),
              ),
            ),
            Container(
              margin: const EdgeInsets.symmetric(horizontal: 4.0),
              child: IconButton(
                icon: const Icon(Icons.send),
                onPressed: () => _handleSubmitted(_textController.text),
              ),
            ),
          ],
        ),
      ),
    );
  }
}
4
likes
160
points
268
downloads

Publisher

verified publisherlabs.flutter.dev

Weekly Downloads

Integration package for genui and A2UI Streaming UI Protocol.

Repository (GitHub)
View/report issues
Contributing

Documentation

API reference

License

BSD-3-Clause (license)

Dependencies

a2a, flutter, genui, json_schema_builder, logging, uuid

More

Packages that depend on genui_a2ui