artemis_socket_plugin 1.0.2 copy "artemis_socket_plugin: ^1.0.2" to clipboard
artemis_socket_plugin: ^1.0.2 copied to clipboard

Artemis Socket Plugin - configuration-driven Flutter SDK for real-time AI agent chat over WebSockets on iOS and Android.

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:artemis_socket_plugin/artemis_socket_plugin.dart';

import 'widgets/markdown_message.dart';

enum ConnectionStatus { notConnected, connecting, connected }

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  // Initialize Artemis SDK with configuration from assets
  final sdk = await AgentSDK.initialize();

  runApp(MyApp(sdk: sdk));
}

class MyApp extends StatelessWidget {
  final AgentSDK sdk;

  const MyApp({super.key, required this.sdk});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Artemis SDK Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(
          seedColor: _hexToColor(sdk.config.theme.primaryColor),
        ),
        useMaterial3: true,
      ),
      home: ChatScreen(sdk: sdk),
    );
  }

  Color _hexToColor(String hexString) {
    final buffer = StringBuffer();
    if (hexString.length == 7) buffer.write('ff');
    buffer.write(hexString.replaceFirst('#', ''));
    return Color(int.parse(buffer.toString(), radix: 16));
  }
}

class ChatScreen extends StatefulWidget {
  final AgentSDK sdk;

  const ChatScreen({super.key, required this.sdk});

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

class _ChatScreenState extends State<ChatScreen> {
  final TextEditingController _messageController = TextEditingController();
  final ScrollController _scrollController = ScrollController();
  List<Message> _messages = [];
  ConnectionStatus _connectionStatus = ConnectionStatus.notConnected;

  bool get _isConnected => _connectionStatus == ConnectionStatus.connected;
  bool get _isConnecting => _connectionStatus == ConnectionStatus.connecting;

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

  Future<void> _initializeSDK() async {
    setState(() => _connectionStatus = ConnectionStatus.connecting);
    try {
      // Connect to platform
      await widget.sdk.connect();
      setState(() {
        _connectionStatus = ConnectionStatus.connected;
        _messages = widget.sdk.getMessages();
      });

      // Listen to chat events
      widget.sdk.chatEvents.listen((event) {
        if (event is MessageReceivedEvent ||
            event is MessageEndEvent ||
            event is MessageChunkEvent) {
          setState(() {
            _messages = widget.sdk.getMessages();
          });
          _scrollToBottom();
        } else if (event is ChatErrorEvent) {
          _showSnackBar('Chat error: ${event.error}');
        }
      });

      // Listen to SDK events and drive the connection status from them
      widget.sdk.events.listen((event) {
        if (event is SDKConnectedEvent) {
          setState(() {
            _connectionStatus = ConnectionStatus.connected;
          });
        } else if (event is SDKReconnectingEvent) {
          setState(() {
            _connectionStatus = ConnectionStatus.connecting;
          });
        } else if (event is SDKDisconnectedEvent) {
          setState(() {
            _connectionStatus = ConnectionStatus.notConnected;
          });
          _showSnackBar('Disconnected: ${event.reason ?? "Unknown"}');
        } else if (event is SDKErrorEvent) {
          _showSnackBar('Error: ${event.error}');
        }
      });
    } catch (e) {
      if (mounted) {
        setState(() => _connectionStatus = ConnectionStatus.notConnected);
      }
      _showSnackBar('Failed to initialize: $e');
    }
  }

  void _scrollToBottom() {
    WidgetsBinding.instance.addPostFrameCallback((_) {
      if (_scrollController.hasClients) {
        _scrollController.animateTo(
          _scrollController.position.maxScrollExtent,
          duration: const Duration(milliseconds: 300),
          curve: Curves.easeOut,
        );
      }
    });
  }

  Future<void> _reconnect() async {
    if (_isConnecting) return;
    setState(() => _connectionStatus = ConnectionStatus.connecting);
    try {
      await widget.sdk.connect();
      if (!mounted) return;
      setState(() {
        _connectionStatus = ConnectionStatus.connected;
        _messages = widget.sdk.getMessages();
      });
      _showSnackBar('Reconnected');
      _scrollToBottom();
    } catch (e) {
      if (!mounted) return;
      setState(() => _connectionStatus = ConnectionStatus.notConnected);
      _showSnackBar('Reconnect failed: $e');
    }
  }

  void _showSnackBar(String message) {
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text(message)),
    );
  }

  Future<void> _sendMessage() async {
    final text = _messageController.text.trim();
    if (text.isEmpty) return;

    try {
      await widget.sdk.sendMessage(text);
      setState(() {
        _messages = widget.sdk.getMessages();
      });
      _messageController.clear();
      _scrollToBottom();
    } catch (e) {
      _showSnackBar('Failed to send message: $e');
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Artemis SDK Demo'),
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        actions: [
          IconButton(
            tooltip: 'Reconnect',
            icon: _isConnecting
                ? const SizedBox(
                    width: 20,
                    height: 20,
                    child: CircularProgressIndicator(strokeWidth: 2),
                  )
                : const Icon(Icons.refresh),
            onPressed: _isConnecting ? null : _reconnect,
          ),
          Visibility(
            visible: false,
            maintainState: true,
            child: IconButton(
              icon: const Icon(Icons.info_outline),
              onPressed: _showConfigInfo,
            ),
          ),
        ],
      ),
      body: Column(
        children: [
          _buildStatusBar(),
          Expanded(
            child: _messages.isEmpty
                ? _buildEmptyState()
                : _buildMessageList(),
          ),
          _buildInputArea(),
        ],
      ),
    );
  }

  Widget _buildStatusBar() {
    final Color backgroundColor;
    final Color foregroundColor;
    final Color textColor;
    final Widget leading;
    final String label;

    switch (_connectionStatus) {
      case ConnectionStatus.connected:
        backgroundColor = Colors.green.shade100;
        foregroundColor = Colors.green.shade700;
        textColor = Colors.green.shade900;
        leading = Icon(Icons.check_circle, size: 16, color: foregroundColor);
        final sessionId = widget.sdk.getSessionId();
        label = sessionId != null ? 'Connected' : 'Connected';
        break;
      case ConnectionStatus.connecting:
        backgroundColor = Colors.blue.shade100;
        foregroundColor = Colors.blue.shade700;
        textColor = Colors.blue.shade900;
        leading = SizedBox(
          width: 16,
          height: 16,
          child: CircularProgressIndicator(
            strokeWidth: 2,
            valueColor: AlwaysStoppedAnimation<Color>(foregroundColor),
          ),
        );
        label = 'Connecting…';
        break;
      case ConnectionStatus.notConnected:
        backgroundColor = Colors.orange.shade100;
        foregroundColor = Colors.orange.shade700;
        textColor = Colors.orange.shade900;
        leading = Icon(Icons.warning, size: 16, color: foregroundColor);
        label = 'Not Connected';
        break;
    }

    return Container(
      padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
      color: backgroundColor,
      child: Row(
        children: [
          leading,
          const SizedBox(width: 8),
          Expanded(
            child: Text(
              label,
              style: TextStyle(fontSize: 12, color: textColor),
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildEmptyState() {
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Icon(
            Icons.chat_bubble_outline,
            size: 64,
            color: Colors.grey.shade400,
          ),
          const SizedBox(height: 16),
          Text(
            'No messages yet',
            style: TextStyle(
              fontSize: 18,
              color: Colors.grey.shade600,
              fontWeight: FontWeight.w500,
            ),
          ),
          const SizedBox(height: 8),
          Text(
            'Start a conversation with your AI assistant',
            style: TextStyle(
              fontSize: 14,
              color: Colors.grey.shade500,
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildMessageList() {
    return ListView.builder(
      controller: _scrollController,
      padding: const EdgeInsets.all(16),
      itemCount: _messages.length,
      itemBuilder: (context, index) {
        return _buildMessageBubble(_messages[index]);
      },
    );
  }

  Widget _buildMessageBubble(Message message) {
    final isUser = message.role == MessageRole.user;
    final useMarkdown =
        !isUser && widget.sdk.config.features.enableMarkdown;

    return Align(
      alignment: isUser ? Alignment.centerRight : Alignment.centerLeft,
      child: Container(
        margin: const EdgeInsets.only(bottom: 12),
        padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
        constraints: BoxConstraints(
          maxWidth: MediaQuery.of(context).size.width * 0.7,
        ),
        decoration: BoxDecoration(
          color: isUser
              ? Theme.of(context).colorScheme.primary
              : Colors.grey.shade200,
          borderRadius: BorderRadius.circular(
            widget.sdk.config.theme.borderRadius,
          ),
        ),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            if (useMarkdown)
              MarkdownMessage(
                content: message.content,
                textColor: Colors.black87,
                linkColor: Theme.of(context).colorScheme.primary,
                codeBackgroundColor: Colors.black.withValues(alpha: 0.06),
              )
            else
              Text(
                message.content,
                style: TextStyle(
                  color: isUser ? Colors.white : Colors.black87,
                  fontSize: 16,
                ),
              ),
            const SizedBox(height: 4),
            Text(
              _formatTime(message.timestamp),
              style: TextStyle(
                color: isUser ? Colors.white70 : Colors.black45,
                fontSize: 11,
              ),
            ),
          ],
        ),
      ),
    );
  }

  String _formatTime(DateTime time) {
    return '${time.hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')}';
  }

  Widget _buildInputArea() {
    return Container(
      padding: const EdgeInsets.all(16),
      decoration: BoxDecoration(
        color: Colors.white,
        boxShadow: [
          BoxShadow(
            color: Colors.black.withValues(alpha: 0.05),
            blurRadius: 10,
            offset: const Offset(0, -5),
          ),
        ],
      ),
      child: Row(
        children: [
          Expanded(
            child: TextField(
              controller: _messageController,
              decoration: InputDecoration(
                hintText: 'Type a message...',
                border: OutlineInputBorder(
                  borderRadius: BorderRadius.circular(24),
                ),
                contentPadding: const EdgeInsets.symmetric(
                  horizontal: 20,
                  vertical: 12,
                ),
              ),
              maxLines: null,
              textInputAction: TextInputAction.send,
              onSubmitted: (_) => _sendMessage(),
              enabled: _isConnected,
            ),
          ),
          const SizedBox(width: 8),
          FloatingActionButton(
            onPressed: _isConnected ? _sendMessage : null,
            child: const Icon(Icons.send),
          ),
        ],
      ),
    );
  }

  void _showConfigInfo() {
    final config = widget.sdk.config;
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: const Text('SDK Configuration'),
        content: SingleChildScrollView(
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            mainAxisSize: MainAxisSize.min,
            children: [
              _buildConfigItem('Environment', config.environment),
              _buildConfigItem('Project ID', config.connection.projectId),
              _buildConfigItem('Endpoint', config.connection.endpoint),
              _buildConfigItem(
                'Voice Enabled',
                config.features.enableVoice.toString(),
              ),
              _buildConfigItem(
                'File Upload',
                config.features.enableFileUpload.toString(),
              ),
              _buildConfigItem(
                'Debug Mode',
                config.debug.enabled.toString(),
              ),
              _buildConfigItem(
                'Log Level',
                config.debug.logLevel,
              ),
            ],
          ),
        ),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context),
            child: const Text('Close'),
          ),
        ],
      ),
    );
  }

  Widget _buildConfigItem(String label, String value) {
    return Padding(
      padding: const EdgeInsets.only(bottom: 8),
      child: Row(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          SizedBox(
            width: 100,
            child: Text(
              '$label:',
              style: const TextStyle(fontWeight: FontWeight.bold),
            ),
          ),
          Expanded(
            child: Text(
              value,
              style: const TextStyle(color: Colors.black87),
            ),
          ),
        ],
      ),
    );
  }

  @override
  void dispose() {
    _messageController.dispose();
    _scrollController.dispose();
    widget.sdk.dispose();
    super.dispose();
  }
}
0
likes
140
points
64
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Artemis Socket Plugin - configuration-driven Flutter SDK for real-time AI agent chat over WebSockets on iOS and Android.

Repository
View/report issues

License

unknown (license)

Dependencies

flutter, http, plugin_platform_interface, web_socket_channel, yaml

More

Packages that depend on artemis_socket_plugin

Packages that implement artemis_socket_plugin