art_adk 1.0.5 copy "art_adk: ^1.0.5" to clipboard
art_adk: ^1.0.5 copied to clipboard

Flutter SDK for ART realtime communication with WebSocket channels, AI Agents, AI Orchestrators, presence tracking, end-to-end encrypted messaging, and CRDT-backed shared objects.

example/lib/main.dart

import 'dart:async';
import 'dart:convert';

import 'package:art_adk/art_adk.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart' show rootBundle;
import 'package:http/http.dart' as http;

import 'dart:io';

import 'package:file_picker/file_picker.dart';
import 'package:flutter/foundation.dart' show kIsWeb, Uint8List;
import 'package:url_launcher/url_launcher.dart';

/// ─────────────────────────────────────────────────────────────────────────────
/// 0. Configure these three values for your environment
/// ─────────────────────────────────────────────────────────────────────────────
const String kServerUri = 'YOUR_WEBSOCKET_URI';
const String kPasscodeEndpoint = 'PASSCODE_ENDPOINT';
const String kUsername = 'USER_NAME';
const String kChannel = 'YOUR_CHANNEL_NAME';

void main() => runApp(const AdkExampleApp());

class AdkExampleApp extends StatelessWidget {
  const AdkExampleApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'ART ADK Example',
      theme: ThemeData(colorSchemeSeed: Colors.indigo, useMaterial3: true),
      home: const AdkHomePage(),
    );
  }
}

/// ─────────────────────────────────────────────────────────────────────────────
/// Home page — walks through every major ADK feature via buttons.
/// ─────────────────────────────────────────────────────────────────────────────
class AdkHomePage extends StatefulWidget {
  const AdkHomePage({super.key});

  @override
  State<AdkHomePage> createState() => _AdkHomePageState();
}

class _AdkHomePageState extends State<AdkHomePage> {
  Adk? _adk;
  BaseSubscription? _subscription;
  final List<String> _log = <String>[];
  String? _selectedUser;

  //Agent
  Agent? _agent;
  AgentThread? _thread;
  Run? _run;

  //Orchestrator
  Orchestrator? _orchestrator;
  OrchestratorThread? _orchestratorThread;

  LiveObjSubscription? get _liveObj {
    final sub = _subscription;
    return sub is LiveObjSubscription ? sub : null;
  }

  void _logEvent(String message) {
    debugPrint(message);
    if (mounted) {
      setState(() {
        _log.add(
            '[${DateTime.now().toIso8601String().substring(11, 19)}] $message');
      });
    }
  }

  /// ───────────────────────────────────────────────────────────────────────────
  /// 1. Connect
  /// ───────────────────────────────────────────────────────────────────────────
  Future<void> _connect() async {
    try {
      _logEvent('connecting...');
      final credentials = await _loadCredentials();
      final passcode = await _fetchPasscode(credentials);
      final updatedCredentials = credentials.copyWith(accessToken: passcode);

      final adk = Adk(
        adkConfig: AdkConfig(
          uri: kServerUri,
          authToken: passcode,
          getCredentials: () => updatedCredentials,
        ),
      );

      adk.on('connection', (dynamic data) {
        if (data is ConnectionDetail) {
          _logEvent('connected · ${data.connectionId}');
        } else {
          _logEvent('connected · $data');
        }
      });
      adk.on('close', (dynamic reason) => _logEvent('closed · $reason'));

      await adk.connect();
      setState(() => _adk = adk);
    } catch (e) {
      _logEvent('connect failed · $e');
    }
  }

  /// ───────────────────────────────────────────────────────────────────────────
  /// 2. Subscribe to a channel (default, secure, or shared object)
  /// ───────────────────────────────────────────────────────────────────────────
  Future<void> _subscribe() async {
    final adk = _adk;
    if (adk == null) {
      _logEvent('connect first');
      return;
    }

    try {
      final sub = await adk.subscribe(channel: kChannel);
      setState(() => _subscription = sub);
      _logEvent('subscribed to $kChannel (${sub.channelConfig.channelType})');

      // Bind a named event across any channel type.
      sub.emitter.on('message', (dynamic data) {
        _logEvent('message · $data');
      });

      // On default channels, also stream every event with listen().
      if (sub is Subscription) {
        sub.listen((Map<String, dynamic> data) {
          _logEvent("event=${data['event']}");
        });
      }

      // Track presence (must be enabled on the channel in the dashboard).
      unawaited(sub.fetchPresence(
        callback: (users) {
          final normalized =
              users.map((e) => e.split(':').first).toSet().toList();

          if (!mounted) return;

          setState(() {
            _selectedUser ??= normalized.firstWhere(
              (u) => u != kUsername,
              orElse: () => '',
            );
          });

          _logEvent('presence · $normalized');
        },
      )
          //     .catchError((e) {
          //   _logEvent('Presence ACK timeout: $e');
          // }),
          );
      // On CRDT channels, observe the document tree.
      if (sub is LiveObjSubscription) {
        await sub.query(path: 'document').listen((dynamic data) {
          _logEvent('doc · $data');
        });
      }
    } catch (e) {
      _logEvent('subscribe failed · $e');
    }
  }

  /// ───────────────────────────────────────────────────────────────────────────
  /// 3. Push a message (optionally targeted)
  /// ───────────────────────────────────────────────────────────────────────────
  Future<void> _sendMessage() async {
    final sub = _subscription;
    if (sub == null) {
      _logEvent('Subscribe first');
      return;
    }

    if (_selectedUser == null || _selectedUser!.isEmpty) {
      _logEvent('No recipient available');
      return;
    }

    final messageId = '$kUsername-${DateTime.now().millisecondsSinceEpoch}';

    try {
      final refId = await sub.push(
        event: 'message',
        data: {
          'message': 'Hello from Flutter ADK',
          'from': kUsername,
          'id': messageId,
        },
        options: PushConfig(
          to: ["USER_NAME"],
        ),
      );

      _logEvent('Message sent');
      _logEvent('Reference Id: $refId');
    } catch (e) {
      _logEvent('Send failed: $e');
    }
  }

  /// ───────────────────────────────────────────────────────────────────────────
  /// 4. Encryption — generate a keypair once per session
  /// ───────────────────────────────────────────────────────────────────────────
  Future<void> _generateKeyPair() async {
    final adk = _adk;
    if (adk == null) {
      _logEvent('connect first');
      return;
    }
    try {
      final pair = await adk.generateKeyPair();
      _logEvent('keypair ready · pub=${pair.publicKey.substring(0, 10)}…');
    } catch (e) {
      _logEvent('keygen failed · $e');
    }
  }

  /// ───────────────────────────────────────────────────────────────────────────
  /// 5. CRDT — set a document title
  /// ───────────────────────────────────────────────────────────────────────────
  Future<void> _setDocTitle() async {
    final live = _liveObj;
    if (live == null) {
      _logEvent('subscribe to a shared-object channel first');
      return;
    }
    live.state()['document']['title'].set(
          'Title @ ${DateTime.now().millisecondsSinceEpoch}',
        );
    await live.flush();
    _logEvent('title written');
  }

  /// ───────────────────────────────────────────────────────────────────────────
  /// 6. CRDT — array push / pop
  /// ───────────────────────────────────────────────────────────────────────────
  Future<void> _pushToArray() async {
    final live = _liveObj;
    if (live == null) return;
    final length = live.state()['items'].push('item ${DateTime.now()}');
    await live.flush();
    _logEvent('items · push (len=$length)');
  }

  Future<void> _popFromArray() async {
    final live = _liveObj;
    if (live == null) return;
    final removed = live.state()['items'].pop();
    await live.flush();
    _logEvent('items · pop ($removed)');
  }

  /// ───────────────────────────────────────────────────────────────────────────
  /// 7. Interceptor — log every message that passes through
  /// ───────────────────────────────────────────────────────────────────────────
  Future<void> _addInterceptor() async {
    final adk = _adk;
    if (adk == null) return;
    try {
      await adk.intercept(
        interceptor: 'demo-logger',
        fn: (
          Map<String, dynamic> payload,
          void Function(dynamic data) resolve,
          void Function(String error) reject,
        ) {
          _logEvent('intercepted · ${payload['event']}');
          resolve(payload);
        },
      );
      _logEvent('interceptor installed');
    } catch (e) {
      _logEvent('interceptor failed · $e');
    }
  }

  /// ───────────────────────────────────────────────────────────────────────────
  /// 8. Teardown
  /// ───────────────────────────────────────────────────────────────────────────
  Future<void> _disconnect() async {
    try {
      await _subscription?.unsubscribe();
      await _adk?.disconnect();
    } finally {
      if (mounted) {
        setState(() {
          _subscription = null;
          _adk = null;
        });
      }
      _logEvent('disconnected');
    }
  }

  /// ───────────────────────────────────────────────────────────────────────────
  /// Helpers
  /// ───────────────────────────────────────────────────────────────────────────
  Future<CredentialStore> _loadCredentials() async {
    try {
      final raw = await rootBundle.loadString('assets/adk-services.json');
      final json = jsonDecode(raw) as Map<String, dynamic>;
      return CredentialStore(
        environment: json['Environment'] as String? ?? '',
        projectKey: json['ProjectKey'] as String? ?? '',
        orgTitle: json['Org-Title'] as String? ?? '',
        clientID: json['Client-ID'] as String? ?? '',
        clientSecret: json['Client-Secret'] as String? ?? '',
      );
    } catch (e) {
      throw Exception('Failed to load assets/adk-services.json: $e');
    }
  }

  Future<String> _fetchPasscode(CredentialStore creds) async {
    final response = await http.post(
      Uri.parse(kPasscodeEndpoint),
      headers: <String, String>{
        'Client-Id': creds.clientID,
        'Client-Secret': creds.clientSecret,
        'X-Org': creds.orgTitle,
        'Environment': creds.environment,
        'ProjectKey': creds.projectKey,
        'Content-Type': 'application/json',
      },
      body: jsonEncode(<String, dynamic>{
        'username': kUsername,
        'first_name': 'Alice',
        'last_name': 'Example',
      }),
    );

    if (response.statusCode < 200 || response.statusCode >= 300) {
      throw Exception('passcode request failed (${response.statusCode})');
    }

    final decoded = jsonDecode(response.body) as Map<String, dynamic>;
    final data = decoded['data'];
    final passcode = data is Map<String, dynamic>
        ? data['passcode'] as String?
        : decoded['passcode'] as String?;
    if (passcode == null || passcode.isEmpty) {
      throw Exception('passcode missing in response');
    }
    return passcode;
  }

  /// ───────────────────────────────────────────────────────────────────────────
  /// Agent
  /// ───────────────────────────────────────────────────────────────────────────
  Future<void> _createAgent() async {
    final adk = _adk;

    if (adk == null) {
      _logEvent('connect first');
      return;
    }

    try {
      _agent = adk.agent('AGENT_ID');

      _logEvent('Agent created');
    } catch (e) {
      _logEvent('create agent failed · $e');
    }
  }

  Future<void> _startThread() async {
    final agent = _agent;

    if (agent == null) {
      _logEvent('create agent first');
      return;
    }

    try {
      final thread = agent.thread();

      _thread = thread;

      _logEvent('Thread started');
      _logEvent('Thread Id : ${thread.threadId}');
    } catch (e) {
      _logEvent('thread failed · $e');
    }
  }

  Future<void> _listenAgent() async {
    final thread = _thread;

    if (thread == null) {
      _logEvent('start thread first');
      return;
    }

    await thread.listen((AgentEventEnvelope envelope) {
      switch (envelope.event) {
        case 'agent_general_response':
          final output = envelope.content as AgentOutput;
          _logEvent('Agent : ${output.message}');
          break;

        case 'agent_error_response':
          final error = envelope.content as AgentError;
          _logEvent('Error : ${error.message}');
          break;

        case 'human_input_request':
          final request = envelope.content as HumanInputRequest;
          _logEvent('Human Input : ${request.prompt}');
          break;

        case 'agent_wait_response':
          final wait = envelope.content as AgentWait;
          _logEvent('Waiting : ${wait.waitingForAgentId}');
          break;

        default:
          _logEvent(envelope.event);
      }
    });

    await thread.listenTrace((frame) {
      _logEvent('TRACE : $frame');
    });

    thread.feedbackRequest(
      (HumanInputRequest request, Run run) async {
        _logEvent('Question : ${request.prompt}');

        // Demo response
        await run.sendFeedback(
          'Budget 50,000 and travelling in December',
        );
      },
    );

    _logEvent('Agent listeners registered');
  }

  Future<void> _sendPrompt() async {
    final thread = _thread;

    if (thread == null) {
      _logEvent('start thread first');
      return;
    }

    try {
      _run = await thread.run(
        'Plan a 3-day trip to Goa',
      );

      final output = await _run!.done();

      _logEvent('Final Response');
      _logEvent(output.message);
    } on AgentError catch (e) {
      _logEvent('${e.code} : ${e.message}');
    } catch (e) {
      _logEvent(e.toString());
    }
  }

  Future<void> _resetAgent() async {
    _run = null;
    _thread = null;
    _agent = null;

    _logEvent('Agent cleared');
  }

  /// ───────────────────────────────────────────────────────────────────────────
  /// Orchestrator
  /// ───────────────────────────────────────────────────────────────────────────
  Future<void> _createOrchestrator() async {
    final adk = _adk;

    if (adk == null) {
      _logEvent('connect first');
      return;
    }

    try {
      _orchestrator = adk.orchestrator("ORCHESTRATOR_ID");

      _logEvent('Orchestrator created');
    } catch (e) {
      _logEvent('create orchestrator failed · $e');
    }
  }

  Future<void> _startOrchestratorThread() async {
    final orchestrator = _orchestrator;

    if (orchestrator == null) {
      _logEvent('create orchestrator first');
      return;
    }

    try {
      final thread = await orchestrator.thread();

      _orchestratorThread = thread;

      _logEvent('Orchestrator thread started');
      _logEvent('Thread Id : ${thread.threadId}');
    } catch (e) {
      _logEvent('thread failed · $e');
    }
  }

  Future<void> _listenOrchestrator() async {
    final thread = _orchestratorThread;

    if (thread == null) {
      _logEvent('start orchestrator thread first');
      return;
    }

    thread.listen((Map<String, dynamic> data) {
      final event = data['event'] as String? ?? '';
      final content = data['content'];

      if (content is Map && content['reply'] is Function) {
        _logEvent('Question : ${content['prompt']}');

        final reply = content['reply'] as Function;

        reply(<String, dynamic>{
          'user_input': 'Budget 50,000 and travelling in December',
        });

        return;
      }

      final type = content is Map ? (content['type'] as String? ?? '') : '';

      switch (type.isNotEmpty ? type : event) {
        case 'agent_general_response':
          _logEvent('Answer : ${content['message']}');
          break;

        case 'agent_error_response':
          _logEvent(
            'Error : ${content['message']}',
          );
          break;

        case 'human_input_request':
          _logEvent(
            'Human Input : ${content['prompt']}',
          );
          break;

        case 'agent_wait_response':
          _logEvent(
            'Waiting : ${content['waiting_for_agent_id']}',
          );
          break;

        case 'planner_correction_request':
          _logEvent(
            'Planner : ${content['reason']}',
          );
          break;

        default:
          _logEvent('[$event] $content');
      }
    });

    thread.listenTrace((frame) {
      _logEvent('TRACE : $frame');
    });

    _logEvent('Orchestrator listeners registered');
  }

  Future<void> _runWorkflow() async {
    final thread = _orchestratorThread;

    if (thread == null) {
      _logEvent('start orchestrator thread first');
      return;
    }

    try {
      await thread.push(
        event: 'user_input',
        data: <String, dynamic>{
          'message': 'Plan a 3-day trip to Goa',
        },
      );

      _logEvent('Workflow started');
    } catch (e) {
      _logEvent('Workflow failed · $e');
    }
  }

  Future<void> _resetOrchestrator() async {
    _orchestratorThread?.dispose();

    _orchestratorThread = null;
    _orchestrator = null;

    _logEvent('Orchestrator cleared');
  }


  /// ───────────────────────────────────────────────────────────────────────────
  /// UI
  /// ───────────────────────────────────────────────────────────────────────────
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('ART ADK Example'),
        actions: <Widget>[
          Padding(
            padding: const EdgeInsets.only(right: 12),
            child: Center(
              child: Text(
                _adk?.getState() ?? 'stopped',
                style: Theme.of(context).textTheme.labelMedium,
              ),
            ),
          ),
          IconButton(
            icon: Row(
              children: [
                Text("Clear Logs"),
                const Icon(Icons.delete_outline),
              ],
            ),
            onPressed: () => setState(() => _log.clear()),
            tooltip: 'Clear Logs',
          ),
        ],
      ),
      body: SingleChildScrollView(
        child: Padding(
          padding: const EdgeInsets.all(12),
          child: Column(
            children: [
              header('Connection'),
              _btn('Connect', _connect, primary: true),
              _btn('Go to Upload Tester', () async {
                Navigator.push(
                  context,
                  MaterialPageRoute(
                    builder: (context) => UploadTesterScreen(adk: _adk),
                  ),
                );
              }, primary: true),
              _btn('Subscribe', _subscribe),
              _btn('Generate KeyPair', _generateKeyPair),
              _btn('Disconnect', _disconnect, destructive: true),
              const Divider(height: 1),
              header('Communication'),
              _btn('Send Message', _sendMessage),
              const Divider(height: 1),
              header('CRDT Operations'),
              _btn('CRDT · Set Title', _setDocTitle),
              _btn('CRDT · Array Push', _pushToArray),
              _btn('CRDT · Array Pop', _popFromArray),
              _btn('Add Interceptor', _addInterceptor),
              const Divider(height: 1),
              header('AI Agent'),
              _btn(
                'Create Agent',
                _createAgent,
              ),
              _btn(
                'Start Thread',
                _startThread,
              ),
              _btn(
                'Register Listeners',
                _listenAgent,
              ),
              _btn(
                'Run Prompt',
                _sendPrompt,
              ),
              _btn('Reset Agent', _resetAgent, primary: true),
              const Divider(height: 1),
              header('Orchestrator'),
              _btn(
                'Create Orchestrator',
                _createOrchestrator,
              ),
              _btn(
                'Start Orchestrator Thread',
                _startOrchestratorThread,
              ),
              _btn(
                'Register Orchestrator Listeners',
                _listenOrchestrator,
              ),
              _btn(
                'Run Workflow',
                _runWorkflow,
              ),
              _btn('Reset Orchestrator', _resetOrchestrator, primary: true),
            ],
          ),
        ),
      ),
    );
  }

  Widget header(String text) =>
      Align(alignment: Alignment.centerLeft, child: Text(text));

  Widget _btn(
    String label,
    Future<void> Function() onTap, {
    bool primary = false,
    bool destructive = false,
  }) {
    final style = primary
        ? FilledButton.styleFrom()
        : destructive
            ? FilledButton.styleFrom(
                backgroundColor: Colors.red.shade400,
                foregroundColor: Colors.white,
              )
            : null;

    return primary || destructive
        ? FilledButton(style: style, onPressed: onTap, child: Text(label))
        : OutlinedButton(onPressed: onTap, child: Text(label));
  }
}




class UploadTesterScreen extends StatefulWidget {
  final Adk? adk;
  const UploadTesterScreen({super.key, this.adk});

  @override
  State<UploadTesterScreen> createState() => _UploadTesterScreenState();
}

class _UploadTesterScreenState extends State<UploadTesterScreen> {
  bool _isConnected = false;
  String _statusText = 'connecting…';
  Color _statusColor = Colors.red;

  final TextEditingController _configIdController =
  TextEditingController(text: '6a730207ec6df8e15a4189cc');

  PlatformFile? _selectedFile;
  bool _isUploading = false;
  double _uploadProgress = 0.0;
  String? _uploadResult;
  bool _isErrorResult = false;
  String? _previewUrl;

  List<StorageFile> _files = [];
  bool _isLoadingFiles = false;
  int _totalFiles = 0;

  @override
  void initState() {
    super.initState();
    _setupAdk();
  }

  @override
  void dispose() {
    _configIdController.dispose();
    super.dispose();
  }

  void _setupAdk() {
    final adk = widget.adk;
    if (adk == null) {
      setState(() {
        _statusText = 'No ADK instance provided';
        _statusColor = Colors.red;
      });
      return;
    }

    // Initial state
    _updateStatusFromAdk();

    adk.on('connection', (_) {
      if (mounted) {
        setState(() {
          _isConnected = true;
          _statusText = 'connected — ready to upload';
          _statusColor = Colors.green;
        });
      }
    });

    adk.on('close', (_) {
      if (mounted) {
        setState(() {
          _isConnected = false;
          _statusText = 'disconnected';
          _statusColor = Colors.red;
        });
      }
    });

    adk.on('error', (err) {
      if (mounted) {
        setState(() {
          _statusText = 'error: $err';
          _statusColor = Colors.red;
        });
      }
    });
  }

  void _updateStatusFromAdk() {
    final adk = widget.adk;
    if (adk == null) return;

    final state = adk.getState();
    setState(() {
      _isConnected = state == 'connected';
      if (_isConnected) {
        _statusText = 'connected — ready to upload';
        _statusColor = Colors.green;
      } else {
        _statusText = state;
        _statusColor = state == 'connecting' ? Colors.orange : Colors.red;
      }
    });
  }

  Future<void> _pickFile() async {
    debugPrint('[Upload] Opening file picker...');
    final result = await FilePicker.pickFiles(
      withData: true, // Required for Web to get bytes
    );
    if (result != null) {
      setState(() {
        _selectedFile = result.files.first;
        debugPrint('[Upload] Selected file: ${_selectedFile!.name} (size: ${_selectedFile!.size})');
      });
    } else {
      debugPrint('[Upload] File picking cancelled');
    }
  }

  Future<void> _upload() async {
    debugPrint('[Upload] Starting _upload() function...');
    final adk = widget.adk;

    if (adk == null) {
      debugPrint('[Upload] ERROR: ADK instance is null');
    }
    if (_selectedFile == null) {
      debugPrint('[Upload] ERROR: No file selected');
    }
    if (!_isConnected) {
      debugPrint('[Upload] ERROR: Not connected to ADK');
    }

    if (_selectedFile == null || !_isConnected || adk == null) {
      debugPrint('[Upload] Aborting upload due to missing requirements');
      return;
    }

    debugPrint('[Upload] Setting UI state to uploading...');
    setState(() {
      _isUploading = true;
      _uploadProgress = 0.0;
      _uploadResult = null;
      _previewUrl = null;
    });

    try {
      final Uint8List bytes;
      if (kIsWeb) {
        debugPrint('[Upload] Platform: Web. Reading bytes from memory...');
        if (_selectedFile!.bytes == null) {
          throw Exception('File data (bytes) is null. Make sure the file was picked correctly.');
        }
        bytes = _selectedFile!.bytes!;
      } else {
        debugPrint('[Upload] Platform: Native. Reading file from path: ${_selectedFile!.path}');
        if (_selectedFile!.path == null) {
          throw Exception('File path is null. Cannot read file on this platform.');
        }
        bytes = await File(_selectedFile!.path!).readAsBytes();
      }

      debugPrint('[Upload] Successfully read ${bytes.length} bytes');

      final configId = _configIdController.text.trim();
      final fileName = _selectedFile!.name;
      String? contentType;

      final ext = fileName.split('.').last.toLowerCase();
      switch (ext) {
      // Images
        case 'jpg':
        case 'jpeg':
          contentType = 'image/jpeg';
          break;
        case 'png':
          contentType = 'image/png';
          break;
        case 'gif':
          contentType = 'image/gif';
          break;
        case 'webp':
          contentType = 'image/webp';
          break;

      // Audio
        case 'mp3':
          contentType = 'audio/mpeg';
          break;
        case 'm4a':
          contentType = 'audio/mp4';
          break;
        case 'wav':
          contentType = 'audio/wav';
          break;
        case 'aac':
          contentType = 'audio/aac';
          break;

      // Video
        case 'mp4':
          contentType = 'video/mp4';
          break;
        case 'mov':
          contentType = 'video/quicktime';
          break;
        case 'avi':
          contentType = 'video/x-msvideo';
          break;

      // Documents
        case 'pdf':
          contentType = 'application/pdf';
          break;
        case 'txt':
          contentType = 'text/plain';
          break;
        case 'csv':
          contentType = 'text/csv';
          break;
        case 'json':
          contentType = 'application/json';
          break;
        case 'doc':
        case 'docx':
          contentType = 'application/msword';
          break;
        case 'xls':
        case 'xlsx':
          contentType = 'application/vnd.ms-excel';
          break;
        case 'ppt':
        case 'pptx':
          contentType = 'application/vnd.ms-powerpoint';
          break;

      // Archives
        case 'zip':
          contentType = 'application/zip';
          break;
        case 'rar':
          contentType = 'application/x-rar-compressed';
          break;
        case '7z':
          contentType = 'application/x-7z-compressed';
          break;

        default:
          contentType = 'application/octet-stream';
      }

      debugPrint('[Upload] Using configId: "${configId.isEmpty ? 'default' : configId}"');
      debugPrint('[Upload] Filename: "$fileName"');
      debugPrint('[Upload] Inferred Content-Type: "$contentType"');

      debugPrint('[Upload] Initiating adk.upload()...$contentType');
      final ref = await adk.upload(
        bytes,
        opts: UploadOptions(
          configId: configId.isEmpty ? null : configId,
          filename: fileName,
          contentType: contentType,
          onProgress: (p) {
            debugPrint('[Upload] Progress: ${(p * 100).toStringAsFixed(1)}%');
            setState(() => _uploadProgress = p);
          },
        ),
      );

      debugPrint('[Upload] Upload SUCCESS. Received FileRef:');
      debugPrint('  - fileId: ${ref.fileId}');
      debugPrint('  - name: ${ref.name}');
      debugPrint('  - size: ${ref.size}');
      debugPrint('  - contentType: ${ref.contentType}');
      debugPrint('  - readUrl: ${ref.readUrl}');

      setState(() {
        _isErrorResult = false;
        _uploadResult = 'OK\n'
            'fileId:      ${ref.fileId}\n'
            'name:        ${ref.name}\n'
            'size:        ${ref.size}\n'
            'contentType: ${ref.contentType}\n'
            'readUrl:     ${ref.readUrl}';

        if (ref.contentType.startsWith('image/')) {
          debugPrint('[Upload] File is an image, setting preview URL');
          _previewUrl = ref.readUrl;
        }
      });

      debugPrint('[Upload] Refreshing file list...');
      _refreshList();
    } catch (e, stackTrace) {
      debugPrint('[Upload] FATAL ERROR during upload: $e');
      debugPrint('[Upload] Stacktrace: $stackTrace');
      setState(() {
        _isErrorResult = true;
        _uploadResult = 'FAILED\n$e';
      });
    } finally {
      debugPrint('[Upload] Function completed, setting _isUploading = false');
      setState(() {
        _isUploading = false;
      });
    }
  }

  Future<void> _refreshList() async {
    final adk = widget.adk;
    if (!_isConnected || adk == null) return;
    setState(() {
      _isLoadingFiles = true;
    });

    try {
      final configId = _configIdController.text.trim();
      final result = await adk.listFiles(
        opts: ListOptions(
          configId: configId.isEmpty ? null : configId,
        ),
      );
      setState(() {
        _files = (result['files'] as List).cast<StorageFile>();
        _totalFiles = result['total'] as int;
      });
    } catch (e) {
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('List failed: $e')),
        );
      }
    } finally {
      if (mounted) {
        setState(() {
          _isLoadingFiles = false;
        });
      }
    }
  }

  Future<void> _viewFile(StorageFile file) async {
    final adk = widget.adk;
    if (adk == null) return;
    try {
      final full = await adk.getFile(file.fileId);
      if (full.readUrl != null) {
        final url = Uri.parse(full.readUrl!);
        if (!await launchUrl(url, mode: LaunchMode.externalApplication)) {
          if (mounted) {
            ScaffoldMessenger.of(context).showSnackBar(
              const SnackBar(content: Text('Could not launch URL')),
            );
          }
        }
      }
    } catch (e) {
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('View failed: $e')),
        );
      }
    }
  }

  Future<void> _deleteFile(StorageFile file) async {
    final adk = widget.adk;
    if (adk == null) return;
    final confirm = await showDialog<bool>(
      context: context,
      builder: (context) => AlertDialog(
        title: const Text('Delete File'),
        content: Text('Delete "${file.name}"?'),
        actions: [
          TextButton(onPressed: () => Navigator.pop(context, false), child: const Text('Cancel')),
          TextButton(onPressed: () => Navigator.pop(context, true), child: const Text('Delete')),
        ],
      ),
    );

    if (confirm == true) {
      try {
        await adk.deleteFile(file.fileId);
        _refreshList();
      } catch (e) {
        if (mounted) {
          ScaffoldMessenger.of(context).showSnackBar(
            SnackBar(content: Text('Delete failed: $e')),
          );
        }
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    const primaryColor = Color(0xFF075E54);

    return Scaffold(
      backgroundColor: Colors.white,
      appBar: AppBar(
        title: const Text('ADK Upload Tester', style: TextStyle(color: Colors.white)),
        backgroundColor: primaryColor,
        iconTheme: const IconThemeData(color: Colors.white),
      ),
      body: Column(
        children: [
          Container(
            padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
            width: double.infinity,
            color: const Color(0xFFF0F0F0),
            child: Row(
              children: [
                const Text('Status: ', style: TextStyle(fontSize: 12)),
                Text(
                  _statusText,
                  style: TextStyle(fontSize: 12, color: _statusColor, fontWeight: FontWeight.bold),
                ),
              ],
            ),
          ),
          Expanded(
            child: SingleChildScrollView(
              padding: const EdgeInsets.all(16),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  const Text(
                    'config_id',
                    style: TextStyle(fontSize: 13, color: Color(0xFF333333)),
                  ),
                  const SizedBox(height: 4),
                  TextField(
                    controller: _configIdController,
                    decoration: const InputDecoration(
                      isDense: true,
                      contentPadding: EdgeInsets.all(8),
                      border: OutlineInputBorder(),
                    ),
                  ),
                  const SizedBox(height: 14),
                  const Text('Pick a file', style: TextStyle(fontSize: 13, color: Color(0xFF333333))),
                  const SizedBox(height: 4),
                  InkWell(
                    onTap: _pickFile,
                    child: Container(
                      padding: const EdgeInsets.all(8),
                      width: double.infinity,
                      decoration: BoxDecoration(
                        border: Border.all(color: Colors.grey),
                        borderRadius: BorderRadius.circular(4),
                      ),
                      child: Text(
                        _selectedFile?.name ?? 'No file selected',
                        style: TextStyle(color: _selectedFile == null ? Colors.grey : Colors.black),
                      ),
                    ),
                  ),
                  const SizedBox(height: 14),
                  SizedBox(
                    width: double.infinity,
                    child: ElevatedButton(
                      onPressed: (_isConnected && _selectedFile != null && !_isUploading) ? _upload : null,
                      style: ElevatedButton.styleFrom(
                        backgroundColor: primaryColor,
                        foregroundColor: Colors.white,
                        disabledBackgroundColor: Colors.grey,
                        shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)),
                      ),
                      child: const Text('Upload'),
                    ),
                  ),
                  if (_isUploading) ...[
                    const SizedBox(height: 14),
                    LinearProgressIndicator(
                      value: _uploadProgress,
                      backgroundColor: Colors.grey[200],
                      valueColor: const AlwaysStoppedAnimation<Color>(primaryColor),
                      minHeight: 14,
                    ),
                  ],
                  if (_uploadResult != null) ...[
                    const SizedBox(height: 14),
                    Container(
                      width: double.infinity,
                      decoration: BoxDecoration(
                        color: _isErrorResult ? const Color(0xFFFDE2E2) : const Color(0xFFF7F7F7),
                        borderRadius: BorderRadius.circular(4),
                      ),
                      clipBehavior: Clip.antiAlias,
                      child: IntrinsicHeight(
                        child: Row(
                          crossAxisAlignment: CrossAxisAlignment.stretch,
                          children: [
                            Container(
                              width: 3,
                              color: _isErrorResult ? const Color(0xFFC0392B) : const Color(0xFFDDDDDD),
                            ),
                            Expanded(
                              child: Container(
                                padding: const EdgeInsets.all(10),
                                decoration: const BoxDecoration(
                                  border: Border(
                                    top: BorderSide(color: Color(0xFFDDDDDD)),
                                    right: BorderSide(color: Color(0xFFDDDDDD)),
                                    bottom: BorderSide(color: Color(0xFFDDDDDD)),
                                  ),
                                ),
                                child: Text(
                                  _uploadResult!,
                                  style: TextStyle(
                                    fontSize: 12,
                                    color: _isErrorResult ? const Color(0xFF7A1C14) : Colors.black,
                                    fontFamily: 'monospace',
                                  ),
                                ),
                              ),
                            ),
                          ],
                        ),
                      ),
                    ),
                  ],
                  if (_previewUrl != null) ...[
                    const SizedBox(height: 8),
                    ClipRRect(
                      borderRadius: BorderRadius.circular(4),
                      child: Image.network(_previewUrl!),
                    ),
                  ],
                  const SizedBox(height: 14),
                  const Divider(color: Color(0xFFEEEEEE)),
                  const SizedBox(height: 4),
                  SizedBox(
                    width: double.infinity,
                    child: ElevatedButton(
                      onPressed: _isConnected ? _refreshList : null,
                      style: ElevatedButton.styleFrom(
                        backgroundColor: primaryColor,
                        foregroundColor: Colors.white,
                        disabledBackgroundColor: Colors.grey,
                        shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)),
                      ),
                      child: const Text('List files in this config_id'),
                    ),
                  ),
                  const SizedBox(height: 10),
                  if (_isLoadingFiles)
                    const Center(child: Text('loading…', style: TextStyle(fontSize: 12, color: Colors.grey)))
                  else if (_files.isNotEmpty || _totalFiles > 0) ...[
                    Text('$_totalFiles file(s) in ${_configIdController.text.isEmpty ? "(project default)" : _configIdController.text}',
                        style: const TextStyle(fontSize: 12, color: Colors.grey)),
                    const SizedBox(height: 6),
                    ..._files.map((f) => _buildFileRow(f)),
                  ] else
                    const Text('No files here yet', style: TextStyle(fontSize: 12, color: Colors.grey)),
                  const SizedBox(height: 14),
                  const Text.rich(
                    TextSpan(
                      children: [
                        TextSpan(text: 'Needs the ADK rebuilt with '),
                        TextSpan(text: 'adk.upload()', style: TextStyle(fontFamily: 'monospace', backgroundColor: Color(0xFFEEEEEE))),
                        TextSpan(text: ' and the '),
                        TextSpan(text: 'AdkUser', style: TextStyle(fontFamily: 'monospace', backgroundColor: Color(0xFFEEEEEE))),
                        TextSpan(text: ' storage RBAC grant. A '),
                        TextSpan(text: '403', style: TextStyle(fontFamily: 'monospace', backgroundColor: Color(0xFFEEEEEE))),
                        TextSpan(text: ' on '),
                        TextSpan(text: 'signed-url', style: TextStyle(fontFamily: 'monospace', backgroundColor: Color(0xFFEEEEEE))),
                        TextSpan(text: " means the grant isn't live yet."),
                      ],
                    ),
                    style: TextStyle(fontSize: 12, color: Color(0xFF666666)),
                  ),
                ],
              ),
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildFileRow(StorageFile f) {
    return Container(
      margin: const EdgeInsets.only(bottom: 6),
      padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
      decoration: BoxDecoration(
        color: const Color(0xFFF7F7F7),
        border: Border.all(color: const Color(0xFFDDDDDD)),
        borderRadius: BorderRadius.circular(4),
      ),
      child: Row(
        children: [
          Expanded(
            child: Text(
              f.name,
              style: const TextStyle(fontSize: 12),
              overflow: TextOverflow.ellipsis,
            ),
          ),
          const SizedBox(width: 8),
          Text(
            '${(f.size / 1024).round()} KB · ${f.status}',
            style: const TextStyle(fontSize: 12, color: Color(0xFF888888)),
          ),
          const SizedBox(width: 8),
          SizedBox(
            height: 28,
            child: ElevatedButton(
              onPressed: () => _viewFile(f),
              style: ElevatedButton.styleFrom(
                backgroundColor: const Color(0xFF1A73E8),
                foregroundColor: Colors.white,
                padding: const EdgeInsets.symmetric(horizontal: 8),
                shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)),
                elevation: 0,
              ),
              child: const Text('View', style: TextStyle(fontSize: 12)),
            ),
          ),
          const SizedBox(width: 4),
          SizedBox(
            height: 28,
            child: ElevatedButton(
              onPressed: () => _deleteFile(f),
              style: ElevatedButton.styleFrom(
                backgroundColor: const Color(0xFFC0392B),
                foregroundColor: Colors.white,
                padding: const EdgeInsets.symmetric(horizontal: 8),
                shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)),
                elevation: 0,
              ),
              child: const Text('Delete', style: TextStyle(fontSize: 12)),
            ),
          ),
        ],
      ),
    );
  }
}
4
likes
160
points
129
downloads

Documentation

Documentation
API reference

Publisher

verified publisherarealtimetech.com

Weekly Downloads

Flutter SDK for ART realtime communication with WebSocket channels, AI Agents, AI Orchestrators, presence tracking, end-to-end encrypted messaging, and CRDT-backed shared objects.

Homepage

Topics

#websocket #realtime #messaging #pubsub #flutter

License

MIT (license)

Dependencies

flutter, http, pinenacl, web_socket_channel

More

Packages that depend on art_adk