tg_claw_kit 0.0.2 copy "tg_claw_kit: ^0.0.2" to clipboard
tg_claw_kit: ^0.0.2 copied to clipboard

Flutter plugin for Claw auth, hidden webcam env bootstrap, chat session management, and device authorization.

example/lib/main.dart

import 'dart:convert' as convert;
import 'dart:io';
import 'dart:typed_data' as typed_data;

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:rapid_kit/rapid_kit.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:tg_claw_kit/tg_claw_kit.dart';

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  runApp(const TgClawKitExampleApp());
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'tg_claw_kit example',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(
          seedColor: const Color(0xFF182238),
          surface: const Color(0xFFF1F5FB),
        ),
        scaffoldBackgroundColor: const Color(0xFFF1F5FB),
        snackBarTheme: const SnackBarThemeData(
          behavior: SnackBarBehavior.floating,
        ),
        useMaterial3: true,
      ),
      home: const ExampleBootstrapPage(),
    );
  }
}

class ExampleBootstrapPage extends StatefulWidget {
  const ExampleBootstrapPage({super.key});

  @override
  State<ExampleBootstrapPage> createState() => _ExampleBootstrapPageState();
}

class _ExampleBootstrapPageState extends State<ExampleBootstrapPage> {
  bool _loading = true;
  String? _error;
  String? _accessToken;

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

  Future<void> _bootstrap() async {
    try {
      await ExampleStore.instance.init();
      final ExampleEnvironment environment = ExampleEnvironment.fromIndex(
        ExampleStore.instance.apiEnv,
      );
      await RapidKit.initialize(
        Configurations(
          id: ExampleStore.instance.appId,
          package: ExampleStore.instance.packageName,
          debugging: true,
          httpLogging: false,
          environment: environment.toRapidEnvironment(),
        ),
      );
      await TgClawKit.initialize(
        environment: environment.toPluginEnvironment(),
        authAppId: ExampleStore.instance.appId,
        authPackageName: ExampleStore.instance.packageName,
      );

      final String storedAccessToken = ExampleStore.instance.accessToken.trim();
      if (storedAccessToken.isNotEmpty) {
        final Resp<VerifiedToken> authResp = await Authenticate.configure(
          storedAccessToken,
        );
        if (!authResp.success) {
          await ExampleStore.instance.clearLogin();
        }
      }

      if (!mounted) {
        return;
      }
      setState(() {
        _loading = false;
        _accessToken = ExampleStore.instance.accessToken.trim();
      });
    } catch (error) {
      if (!mounted) {
        return;
      }
      setState(() {
        _loading = false;
        _error = error.toString();
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    if (_loading) {
      return const Scaffold(body: Center(child: CircularProgressIndicator()));
    }

    if (_error != null) {
      return Scaffold(
        body: Center(
          child: Padding(
            padding: const EdgeInsets.all(24),
            child: Text(_error!),
          ),
        ),
      );
    }

    return _accessToken?.isNotEmpty == true
        ? HomePage(initialToken: _accessToken!)
        : const LoginPage();
  }
}

class LoginPage extends StatefulWidget {
  const LoginPage({super.key});

  @override
  State<LoginPage> createState() => _LoginPageState();
}

class _LoginPageState extends State<LoginPage> {
  final TextEditingController _accountController = TextEditingController();
  final TextEditingController _passwordController = TextEditingController();
  bool _submitting = false;

  @override
  void initState() {
    super.initState();
    _accountController.text = ExampleStore.instance.savedAccount;
  }

  @override
  void dispose() {
    _accountController.dispose();
    _passwordController.dispose();
    super.dispose();
  }

  Future<void> _login() async {
    FocusScope.of(context).unfocus();
    final String account = _accountController.text.trim();
    final String password = _passwordController.text;
    if (account.isEmpty || password.isEmpty) {
      _showSnack('请输入账号和密码');
      return;
    }

    setState(() {
      _submitting = true;
    });
    try {
      final Resp<LoginRet> loginResp = await AccountService.login(
        account,
        password,
      );
      if (!mounted) {
        return;
      }
      if (!loginResp.success ||
          (loginResp.data?.accessToken?.isEmpty ?? true)) {
        _showSnack('登录失败: ${loginResp.message ?? loginResp.code}');
        return;
      }

      final String accessToken = loginResp.data!.accessToken!.trim();
      final Resp<VerifiedToken> authResp = await Authenticate.configure(
        accessToken,
      );
      if (!mounted) {
        return;
      }
      if (!authResp.success) {
        _showSnack('RapidKit 鉴权失败: ${authResp.message ?? authResp.code}');
        return;
      }

      await ExampleStore.instance.saveLogin(
        account: account,
        accessToken: accessToken,
      );
      if (!mounted) {
        return;
      }
      Navigator.of(context).pushReplacement(
        MaterialPageRoute(builder: (_) => HomePage(initialToken: accessToken)),
      );
    } catch (error) {
      _showSnack('登录异常: $error');
    } finally {
      if (mounted) {
        setState(() {
          _submitting = false;
        });
      }
    }
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: Center(
          child: SingleChildScrollView(
            padding: const EdgeInsets.all(24),
            child: ConstrainedBox(
              constraints: const BoxConstraints(maxWidth: 420),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.stretch,
                children: [
                  const SizedBox(height: 24),
                  const Text(
                    'tg_claw_kit Demo',
                    textAlign: TextAlign.center,
                    style: TextStyle(
                      fontSize: 28,
                      fontWeight: FontWeight.w800,
                      color: Color(0xFF182238),
                    ),
                  ),
                  const SizedBox(height: 12),
                  Text(
                    'RapidKit 登录 + tg_claw_kit Claw 会话与设备授权',
                    textAlign: TextAlign.center,
                    style: TextStyle(
                      color: Colors.blueGrey.shade600,
                      fontSize: 14,
                    ),
                  ),
                  const SizedBox(height: 32),
                  TextField(
                    controller: _accountController,
                    decoration: const InputDecoration(
                      labelText: '账号',
                      border: OutlineInputBorder(),
                    ),
                  ),
                  const SizedBox(height: 12),
                  TextField(
                    controller: _passwordController,
                    obscureText: true,
                    decoration: const InputDecoration(
                      labelText: '密码',
                      border: OutlineInputBorder(),
                    ),
                  ),
                  const SizedBox(height: 16),
                  FilledButton(
                    onPressed: _submitting ? null : _login,
                    child: _submitting
                        ? const SizedBox(
                            width: 18,
                            height: 18,
                            child: CircularProgressIndicator(strokeWidth: 2),
                          )
                        : const Text('登录'),
                  ),
                  const SizedBox(height: 12),
                  OutlinedButton(
                    onPressed: () {
                      Navigator.of(context).push(
                        MaterialPageRoute(builder: (_) => const SettingsPage()),
                      );
                    },
                    child: const Text('环境配置'),
                  ),
                ],
              ),
            ),
          ),
        ),
      ),
    );
  }
}

class HomePage extends StatefulWidget {
  const HomePage({super.key, required this.initialToken});

  final String initialToken;

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  int _currentIndex = 0;

  @override
  Widget build(BuildContext context) {
    final List<Widget> pages = <Widget>[
      ClawChatPage(accessToken: widget.initialToken),
      ExampleProfilePage(accessToken: widget.initialToken),
    ];

    return Scaffold(
      body: pages[_currentIndex],
      bottomNavigationBar: NavigationBar(
        selectedIndex: _currentIndex,
        onDestinationSelected: (int index) {
          setState(() {
            _currentIndex = index;
          });
        },
        destinations: const [
          NavigationDestination(
            icon: Icon(Icons.chat_bubble_outline),
            label: 'Claw',
          ),
          NavigationDestination(icon: Icon(Icons.person_outline), label: '我的'),
        ],
      ),
    );
  }
}

class ClawChatPage extends StatefulWidget {
  const ClawChatPage({super.key, required this.accessToken});

  final String accessToken;

  @override
  State<ClawChatPage> createState() => _ClawChatPageState();
}

class _ClawChatPageState extends State<ClawChatPage> {
  static const Color _pageBackground = Color(0xFFF1F5FB);
  static const Color _panelColor = Colors.white;
  static const Color _primaryInk = Color(0xFF182238);
  static const Color _secondaryInk = Color(0xFF60708C);
  static const Color _accentSurface = Color(0xFFEAF0F8);
  static const Color _accentBorder = Color(0xFFDCE4F0);
  static const MethodChannel _rapidMethodChannel = MethodChannel(
    'com.tange.ai/rapid_kit/method.channel',
  );

  final TextEditingController _queryController = TextEditingController();
  final ScrollController _scrollController = ScrollController();
  final List<ExampleChatMessage> _messages = <ExampleChatMessage>[];
  final Map<String, int> _messageIndexById = <String, int>{};

  bool _loadingClaw = false;
  String _loadingClawText = '获取 token 中...';
  bool _streaming = false;
  bool _showToolBubbles = false;
  String? _error;

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

  @override
  void dispose() {
    _queryController.dispose();
    _scrollController.dispose();
    super.dispose();
  }

  Future<void> _bootstrap() async {
    setState(() {
      _loadingClaw = true;
      _loadingClawText = '获取 token 中...';
      _error = null;
    });

    try {
      await TgClawKit.auth(accessToken: widget.accessToken);
      if (!mounted) {
        return;
      }
      setState(() {
        _loadingClawText = '加载历史会话中...';
      });

      final TgClawChatHistory? history = await TgClawKit.loadChatHistory();
      if (!mounted) {
        return;
      }
      setState(() {
        _messages
          ..clear()
          ..addAll(
            (history?.messages ?? const <TgClawMessage>[]).map(
              ExampleChatMessage.fromDomain,
            ),
          );
        _rebuildMessageIndex();
      });
      if ((history?.messages.isNotEmpty ?? false)) {
        _scrollToBottom(animated: false);
      }
    } catch (error) {
      if (!mounted) {
        return;
      }
      setState(() {
        _error = error.toString();
      });
      _showSnack('Claw 初始化失败: $error');
    } finally {
      if (mounted) {
        setState(() {
          _loadingClaw = false;
          _loadingClawText = '获取 token 中...';
        });
      }
    }
  }

  void _rebuildMessageIndex() {
    _messageIndexById.clear();
    for (int i = 0; i < _messages.length; i++) {
      _messageIndexById[_messages[i].id] = i;
    }
  }

  void _submitFromComposer() {
    final String prompt = _queryController.text.trim();
    if (prompt.isEmpty || _streaming || _loadingClaw) {
      return;
    }

    if (TgClawKit.currentAuthSession == null) {
      _showSnack('当前未获取到 Claw Token,请确认登录态后重试');
      return;
    }

    _queryController.clear();
    _sendPrompt(prompt);
  }

  Future<void> _sendPrompt(String prompt) async {
    setState(() {
      _messages.add(ExampleChatMessage.user(prompt));
      _rebuildMessageIndex();
      _streaming = true;
      _error = null;
    });
    _scrollToBottom();

    try {
      await TgClawKit.chat(
        prompt,
        onEvent: _handleChatEvent,
        onError: (TgClawException error) {
          _addSystemMessage(error.toString());
          _showSnack('聊天请求失败');
        },
      );
    } catch (_) {
      // error already surfaced through onError
    } finally {
      if (mounted) {
        setState(() {
          _streaming = false;
          for (int i = 0; i < _messages.length; i++) {
            if (_messages[i].streaming) {
              _messages[i] = _messages[i].copyWith(streaming: false);
            }
          }
          _rebuildMessageIndex();
        });
        _scrollToBottom();
      }
    }
  }

  void _handleChatEvent(TgClawChatEvent event) {
    if (!mounted) {
      return;
    }
    if (event.type == TgClawChatEventType.messageFailed &&
        event.message != null) {
      _showSnack('聊天请求失败');
    }
    final TgClawMessage? message = event.message;
    if (message == null) {
      return;
    }

    final ExampleChatMessage next = ExampleChatMessage.fromDomain(message);
    final int? existingIndex = _messageIndexById[next.id];
    setState(() {
      if (existingIndex == null) {
        _messages.add(next);
      } else {
        _messages[existingIndex] = next;
      }
      _rebuildMessageIndex();
    });
    _scrollToBottom();
  }

  void _addSystemMessage(String text) {
    if (!mounted) {
      return;
    }
    setState(() {
      _messages.add(ExampleChatMessage.system(text));
      _rebuildMessageIndex();
    });
    _scrollToBottom();
  }

  void _scrollToBottom({bool animated = true}) {
    WidgetsBinding.instance.addPostFrameCallback((_) {
      if (!_scrollController.hasClients) {
        return;
      }
      final double target = _scrollController.position.minScrollExtent;
      if (animated) {
        _scrollController.animateTo(
          target,
          duration: const Duration(milliseconds: 220),
          curve: Curves.easeOut,
        );
        return;
      }
      _scrollController.jumpTo(target);
    });
  }

  void _showSnack(String message) {
    if (!mounted) {
      return;
    }
    ScaffoldMessenger.of(
      context,
    ).showSnackBar(SnackBar(content: Text(message)));
  }

  Future<void> _openDeviceAuthorization() async {
    await Navigator.of(context).push(
      MaterialPageRoute(
        builder: (_) =>
            DeviceAuthorizationPage(accessToken: widget.accessToken),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: _pageBackground,
      body: SafeArea(
        child: Column(
          children: [
            _buildHeader(),
            Expanded(
              child: Container(
                margin: const EdgeInsets.fromLTRB(12, 0, 12, 12),
                padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
                decoration: BoxDecoration(
                  color: _pageBackground,
                  borderRadius: BorderRadius.circular(36),
                ),
                child: Stack(
                  children: [
                    AbsorbPointer(
                      absorbing: _loadingClaw,
                      child: Column(
                        children: [
                          if (_messages.isEmpty) ...[
                            _buildRangeHint(),
                            const SizedBox(height: 16),
                          ],
                          Expanded(child: _buildConversation()),
                          const SizedBox(height: 16),
                          _buildComposer(),
                        ],
                      ),
                    ),
                    if (_loadingClaw)
                      Positioned.fill(child: _buildLoadingOverlay()),
                  ],
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildHeader() {
    return Padding(
      padding: const EdgeInsets.fromLTRB(12, 16, 12, 8),
      child: Stack(
        alignment: Alignment.center,
        children: [
          const Align(
            alignment: Alignment.center,
            child: Text(
              'Claw',
              textAlign: TextAlign.center,
              style: TextStyle(
                fontSize: 26,
                fontWeight: FontWeight.w800,
                color: _primaryInk,
              ),
            ),
          ),
          Align(
            alignment: Alignment.centerRight,
            child: Row(
              mainAxisSize: MainAxisSize.min,
              children: [
                IconButton(
                  tooltip: _showToolBubbles ? '隐藏过程输出' : '显示过程输出',
                  onPressed: () {
                    setState(() {
                      _showToolBubbles = !_showToolBubbles;
                    });
                  },
                  icon: Icon(
                    _showToolBubbles
                        ? Icons.visibility_outlined
                        : Icons.visibility_off_outlined,
                    color: _secondaryInk,
                    size: 24,
                  ),
                ),
                IconButton(
                  tooltip: '设备授权',
                  onPressed: _openDeviceAuthorization,
                  icon: const Icon(
                    Icons.settings_outlined,
                    color: _secondaryInk,
                    size: 24,
                  ),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildLoadingOverlay() {
    return AnimatedContainer(
      duration: const Duration(milliseconds: 220),
      curve: Curves.easeOut,
      decoration: BoxDecoration(
        borderRadius: BorderRadius.circular(28),
        gradient: LinearGradient(
          begin: Alignment.topLeft,
          end: Alignment.bottomRight,
          colors: [
            Colors.white.withValues(alpha: 0.92),
            const Color(0xFFF0F5FC).withValues(alpha: 0.95),
          ],
        ),
      ),
      child: Center(
        child: Container(
          width: 244,
          padding: const EdgeInsets.fromLTRB(18, 18, 18, 16),
          decoration: BoxDecoration(
            color: Colors.white.withValues(alpha: 0.96),
            borderRadius: BorderRadius.circular(20),
            border: Border.all(color: _accentBorder),
            boxShadow: const [
              BoxShadow(
                color: Color(0x16000000),
                blurRadius: 18,
                offset: Offset(0, 10),
              ),
            ],
          ),
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              const SizedBox(
                width: 36,
                height: 36,
                child: CircularProgressIndicator(
                  strokeWidth: 3,
                  color: _secondaryInk,
                ),
              ),
              const SizedBox(height: 12),
              AnimatedSwitcher(
                duration: const Duration(milliseconds: 180),
                child: Text(
                  _loadingClawText,
                  key: ValueKey<String>(_loadingClawText),
                  textAlign: TextAlign.center,
                  style: const TextStyle(
                    color: _primaryInk,
                    fontSize: 14,
                    fontWeight: FontWeight.w800,
                    height: 1.4,
                  ),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }

  Widget _buildRangeHint() {
    const String hint =
        '你好!我是你的 Claw 助手。这里已经接入 tg_claw_kit,支持多轮上下文、流式响应、工具调用展示和设备授权。';

    return Container(
      width: double.infinity,
      padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
      decoration: BoxDecoration(
        color: Colors.white.withValues(alpha: 0.72),
        borderRadius: BorderRadius.circular(18),
        border: Border.all(color: _accentBorder),
      ),
      child: Text(
        hint,
        style: const TextStyle(color: _secondaryInk, fontSize: 12, height: 1.4),
      ),
    );
  }

  Widget _buildConversation() {
    final List<ExampleChatMessage> visibleMessages = _messages
        .where(
          (ExampleChatMessage message) => !_shouldHideMessageBubble(message),
        )
        .toList(growable: false)
        .reversed
        .toList(growable: false);

    if (_error != null && visibleMessages.isEmpty) {
      return Center(
        child: Padding(
          padding: const EdgeInsets.all(24),
          child: Text(
            _error!,
            textAlign: TextAlign.center,
            style: const TextStyle(
              color: Colors.redAccent,
              fontWeight: FontWeight.w700,
            ),
          ),
        ),
      );
    }

    if (visibleMessages.isEmpty) {
      return const SizedBox.shrink();
    }

    return ListView.separated(
      controller: _scrollController,
      reverse: true,
      itemCount: visibleMessages.length,
      separatorBuilder: (_, _) => const SizedBox(height: 12),
      itemBuilder: (BuildContext context, int index) {
        final ExampleChatMessage message = visibleMessages[index];
        return message.isUser
            ? _buildUserBubble(message)
            : _buildAssistantBubble(message);
      },
    );
  }

  bool _shouldHideMessageBubble(ExampleChatMessage message) {
    if (_showToolBubbles) {
      return false;
    }
    return message.role == TgClawMessageRole.reasoning ||
        message.role == TgClawMessageRole.toolCall ||
        message.role == TgClawMessageRole.toolOutput;
  }

  Widget _buildUserBubble(ExampleChatMessage message) {
    return Align(
      alignment: Alignment.centerRight,
      child: Container(
        constraints: const BoxConstraints(maxWidth: 300),
        padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
        decoration: const BoxDecoration(
          color: _primaryInk,
          borderRadius: BorderRadius.only(
            topLeft: Radius.circular(24),
            topRight: Radius.circular(24),
            bottomLeft: Radius.circular(24),
            bottomRight: Radius.circular(6),
          ),
        ),
        child: Text(
          message.text,
          style: const TextStyle(
            color: Colors.white,
            fontSize: 16,
            fontWeight: FontWeight.w700,
            height: 1.5,
          ),
        ),
      ),
    );
  }

  Widget _buildAssistantBubble(ExampleChatMessage message) {
    final bool isToolCard =
        message.role == TgClawMessageRole.toolCall ||
        message.role == TgClawMessageRole.toolOutput;
    final bool isReasoning = message.role == TgClawMessageRole.reasoning;

    return Align(
      alignment: Alignment.centerLeft,
      child: Container(
        width: double.infinity,
        padding: const EdgeInsets.all(16),
        decoration: BoxDecoration(
          color: _panelColor,
          borderRadius: BorderRadius.circular(26),
          border: Border.all(
            color: isToolCard ? const Color(0xFFCFE0F7) : _accentBorder,
          ),
          boxShadow: const [
            BoxShadow(
              color: Color(0x12000000),
              blurRadius: 18,
              offset: Offset(0, 8),
            ),
          ],
        ),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            if (message.title.isNotEmpty)
              Padding(
                padding: const EdgeInsets.only(bottom: 10),
                child: Row(
                  children: [
                    Icon(
                      _iconForRole(message.role),
                      size: 16,
                      color: _secondaryInk,
                    ),
                    const SizedBox(width: 6),
                    Text(
                      message.title,
                      style: const TextStyle(
                        color: _primaryInk,
                        fontSize: 13,
                        fontWeight: FontWeight.w800,
                      ),
                    ),
                  ],
                ),
              ),
            if ((message.toolName ?? '').trim().isNotEmpty)
              _buildToolMetaRow('name', message.toolName!.trim()),
            if ((message.toolArguments ?? '').trim().isNotEmpty)
              Padding(
                padding: const EdgeInsets.only(top: 8),
                child: _buildCodeBlock(message.toolArguments!.trim()),
              ),
            if ((message.toolOutput ?? '').trim().isNotEmpty)
              Padding(
                padding: const EdgeInsets.only(top: 8),
                child: _buildCodeBlock(message.toolOutput!.trim()),
              ),
            if (message.media.isNotEmpty)
              Padding(
                padding: const EdgeInsets.only(top: 10),
                child: _buildMediaBlocks(message.media),
              ),
            if (message.text.trim().isNotEmpty)
              Padding(
                padding: EdgeInsets.only(
                  top:
                      ((message.toolArguments ?? '').trim().isNotEmpty ||
                          (message.toolOutput ?? '').trim().isNotEmpty ||
                          message.media.isNotEmpty)
                      ? 10
                      : 0,
                ),
                child: MarkdownBody(
                  data: message.text,
                  selectable: true,
                  styleSheet: MarkdownStyleSheet(
                    p: TextStyle(
                      color: isReasoning ? _secondaryInk : _primaryInk,
                      fontSize: isReasoning ? 13 : 14,
                      fontStyle: isReasoning
                          ? FontStyle.italic
                          : FontStyle.normal,
                      fontWeight: isReasoning
                          ? FontWeight.w500
                          : FontWeight.w600,
                      height: 1.6,
                    ),
                    code: const TextStyle(
                      color: _primaryInk,
                      fontSize: 12,
                      fontWeight: FontWeight.w500,
                      fontFamily: 'monospace',
                      height: 1.45,
                    ),
                    codeblockDecoration: BoxDecoration(
                      color: const Color(0xFFF6F9FF),
                      borderRadius: BorderRadius.circular(12),
                      border: Border.all(color: _accentBorder),
                    ),
                    codeblockPadding: const EdgeInsets.all(10),
                    blockquoteDecoration: BoxDecoration(
                      color: const Color(0xFFF8FAFD),
                      borderRadius: BorderRadius.circular(12),
                      border: Border.all(color: _accentBorder),
                    ),
                    blockquotePadding: const EdgeInsets.all(10),
                    h1: const TextStyle(
                      color: _primaryInk,
                      fontSize: 20,
                      fontWeight: FontWeight.w800,
                    ),
                    h2: const TextStyle(
                      color: _primaryInk,
                      fontSize: 18,
                      fontWeight: FontWeight.w800,
                    ),
                    h3: const TextStyle(
                      color: _primaryInk,
                      fontSize: 16,
                      fontWeight: FontWeight.w800,
                    ),
                    listBullet: TextStyle(
                      color: isReasoning ? _secondaryInk : _primaryInk,
                      fontSize: isReasoning ? 13 : 14,
                      fontWeight: isReasoning
                          ? FontWeight.w500
                          : FontWeight.w600,
                    ),
                    strong: TextStyle(
                      color: isReasoning ? _secondaryInk : _primaryInk,
                      fontWeight: FontWeight.w800,
                    ),
                    em: TextStyle(
                      color: isReasoning ? _secondaryInk : _primaryInk,
                      fontStyle: FontStyle.italic,
                    ),
                  ),
                ),
              ),
            if (message.streaming)
              const Padding(
                padding: EdgeInsets.only(top: 8),
                child: Row(
                  mainAxisSize: MainAxisSize.min,
                  children: [
                    SizedBox(
                      width: 14,
                      height: 14,
                      child: CircularProgressIndicator(
                        strokeWidth: 2,
                        color: _secondaryInk,
                      ),
                    ),
                    SizedBox(width: 8),
                    Text(
                      '流式响应中...',
                      style: TextStyle(
                        color: _secondaryInk,
                        fontSize: 12,
                        fontWeight: FontWeight.w600,
                      ),
                    ),
                  ],
                ),
              ),
          ],
        ),
      ),
    );
  }

  IconData _iconForRole(TgClawMessageRole role) {
    switch (role) {
      case TgClawMessageRole.reasoning:
        return Icons.psychology_alt_outlined;
      case TgClawMessageRole.toolCall:
        return Icons.build_circle_outlined;
      case TgClawMessageRole.toolOutput:
        return Icons.data_object_outlined;
      case TgClawMessageRole.system:
        return Icons.info_outline;
      case TgClawMessageRole.assistant:
      case TgClawMessageRole.user:
        return Icons.chat_bubble_outline;
    }
  }

  Widget _buildToolMetaRow(String label, String value) {
    return Container(
      width: double.infinity,
      padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
      decoration: BoxDecoration(
        color: _accentSurface,
        borderRadius: BorderRadius.circular(12),
      ),
      child: Row(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text(
            '$label: ',
            style: const TextStyle(
              color: _secondaryInk,
              fontSize: 12,
              fontWeight: FontWeight.w800,
            ),
          ),
          Expanded(
            child: SelectableText(
              value,
              style: const TextStyle(
                color: _primaryInk,
                fontSize: 12,
                fontWeight: FontWeight.w700,
              ),
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildCodeBlock(String value) {
    return Container(
      width: double.infinity,
      padding: const EdgeInsets.all(10),
      decoration: BoxDecoration(
        color: const Color(0xFFF6F9FF),
        borderRadius: BorderRadius.circular(12),
        border: Border.all(color: _accentBorder),
      ),
      child: SelectableText(
        value,
        style: const TextStyle(
          color: _primaryInk,
          fontSize: 12,
          fontWeight: FontWeight.w500,
          height: 1.45,
          fontFamily: 'monospace',
        ),
      ),
    );
  }

  Widget _buildMediaBlocks(List<ExampleChatMedia> mediaList) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        for (int i = 0; i < mediaList.length; i++) ...[
          if (i > 0) const SizedBox(height: 10),
          _buildMediaItem(mediaList[i]),
        ],
      ],
    );
  }

  Widget _buildMediaItem(ExampleChatMedia media) {
    if (media.type == TgClawMediaType.image) {
      return _buildImageMediaItem(media);
    }

    return Container(
      width: double.infinity,
      padding: const EdgeInsets.all(10),
      decoration: BoxDecoration(
        color: const Color(0xFFF8FAFD),
        borderRadius: BorderRadius.circular(12),
        border: Border.all(color: _accentBorder),
      ),
      child: Row(
        children: [
          const Icon(
            Icons.insert_drive_file_outlined,
            size: 18,
            color: _secondaryInk,
          ),
          const SizedBox(width: 8),
          Expanded(
            child: SelectableText(
              (media.filename ?? '').trim().isNotEmpty
                  ? media.filename!.trim()
                  : '文件输出',
              style: const TextStyle(
                color: _primaryInk,
                fontSize: 12,
                fontWeight: FontWeight.w700,
              ),
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildImageMediaItem(ExampleChatMedia media) {
    final typed_data.Uint8List? bytes = _decodeImageBytes(media.uri);
    if (bytes == null || bytes.isEmpty) {
      return Container(
        width: double.infinity,
        padding: const EdgeInsets.all(12),
        decoration: BoxDecoration(
          color: const Color(0xFFF8FAFD),
          borderRadius: BorderRadius.circular(12),
          border: Border.all(color: _accentBorder),
        ),
        child: const Text(
          '图片数据解析失败',
          style: TextStyle(
            color: _secondaryInk,
            fontSize: 12,
            fontWeight: FontWeight.w700,
          ),
        ),
      );
    }

    final String filename = (media.filename ?? '').trim();

    return Container(
      width: double.infinity,
      clipBehavior: Clip.antiAlias,
      decoration: BoxDecoration(
        color: const Color(0xFFF8FAFD),
        borderRadius: BorderRadius.circular(14),
        border: Border.all(color: _accentBorder),
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          InkWell(
            onTap: () => _openImagePreview(media: media, bytes: bytes),
            child: Stack(
              children: [
                ConstrainedBox(
                  constraints: const BoxConstraints(maxHeight: 240),
                  child: Image.memory(
                    bytes,
                    fit: BoxFit.cover,
                    width: double.infinity,
                    gaplessPlayback: true,
                  ),
                ),
                const Positioned(
                  right: 10,
                  top: 10,
                  child: DecoratedBox(
                    decoration: BoxDecoration(
                      color: Color(0xB3000000),
                      borderRadius: BorderRadius.all(Radius.circular(999)),
                    ),
                    child: Padding(
                      padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
                      child: Row(
                        mainAxisSize: MainAxisSize.min,
                        children: [
                          Icon(
                            Icons.zoom_in_rounded,
                            color: Colors.white,
                            size: 14,
                          ),
                          SizedBox(width: 4),
                          Text(
                            '点击放大',
                            style: TextStyle(
                              color: Colors.white,
                              fontSize: 11,
                              fontWeight: FontWeight.w700,
                            ),
                          ),
                        ],
                      ),
                    ),
                  ),
                ),
              ],
            ),
          ),
          Padding(
            padding: const EdgeInsets.fromLTRB(10, 8, 6, 8),
            child: Row(
              children: [
                Expanded(
                  child: Text(
                    filename.isNotEmpty ? filename : '图片',
                    maxLines: 1,
                    overflow: TextOverflow.ellipsis,
                    style: const TextStyle(
                      color: _secondaryInk,
                      fontSize: 11,
                      fontWeight: FontWeight.w700,
                    ),
                  ),
                ),
                IconButton(
                  tooltip: '下载图片',
                  visualDensity: VisualDensity.compact,
                  onPressed: () =>
                      _downloadImageMedia(media: media, bytes: bytes),
                  icon: const Icon(
                    Icons.download_rounded,
                    size: 20,
                    color: _secondaryInk,
                  ),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }

  typed_data.Uint8List? _decodeImageBytes(String uri) {
    final String trimmed = uri.trim();
    if (trimmed.isEmpty) {
      return null;
    }

    if (trimmed.startsWith('data:')) {
      final int commaIndex = trimmed.indexOf(',');
      if (commaIndex < 0 || commaIndex >= trimmed.length - 1) {
        return null;
      }
      final String encoded = trimmed.substring(commaIndex + 1).trim();
      try {
        return convert.base64Decode(encoded);
      } catch (_) {
        return null;
      }
    }

    try {
      return convert.base64Decode(trimmed);
    } catch (_) {
      return null;
    }
  }

  Future<void> _openImagePreview({
    required ExampleChatMedia media,
    required typed_data.Uint8List bytes,
  }) async {
    final String title = (media.filename ?? '').trim().isNotEmpty
        ? media.filename!.trim()
        : '图片预览';

    await Navigator.of(context).push<void>(
      MaterialPageRoute(
        builder: (_) => _ImagePreviewPage(
          title: title,
          bytes: bytes,
          onDownload: () => _downloadImageMedia(media: media, bytes: bytes),
        ),
      ),
    );
  }

  Future<void> _downloadImageMedia({
    required ExampleChatMedia media,
    required typed_data.Uint8List bytes,
  }) async {
    final bool granted = await _ensureGalleryPermission();
    if (!granted) {
      return;
    }

    try {
      final String cacheBase =
          (await _rapidMethodChannel.invokeMethod<String>(
            'requireWriteableCachePath',
          ))?.trim() ??
          '';
      if (cacheBase.isEmpty) {
        _showSnack('图片下载失败: 无法申请缓存路径');
        return;
      }

      final String imagePath = '$cacheBase${_imageExtensionFor(media)}';
      final File file = File(imagePath);
      await file.writeAsBytes(bytes, flush: true);

      final bool exported =
          (await _rapidMethodChannel.invokeMethod<bool>(
            'exportCapturedPicture',
            <String, dynamic>{'cachePath': imagePath},
          )) ??
          false;

      if (!exported && await file.exists()) {
        await file.delete();
      }

      _showSnack(exported ? '图片下载成功' : '图片下载失败');
    } catch (error) {
      _showSnack('图片下载失败: $error');
    }
  }

  Future<bool> _ensureGalleryPermission() async {
    final bool granted = await RuntimePermissions.accessGranted(
      PermissionType.gallery,
    );
    if (granted) {
      return true;
    }

    final PermissionState state = await RuntimePermissions.requestAccess(
      PermissionType.gallery,
    );
    if (!mounted) {
      return false;
    }

    if (state != PermissionState.granted) {
      _showSnack(
        state == PermissionState.denied ? '请到系统设置中,手动授权访问相册。' : '请授权访问相册。',
      );
      return false;
    }

    return true;
  }

  String _imageExtensionFor(ExampleChatMedia media) {
    final String filename = (media.filename ?? '').trim().toLowerCase();
    if (filename.endsWith('.png')) {
      return '.png';
    }
    if (filename.endsWith('.webp')) {
      return '.webp';
    }
    if (filename.endsWith('.gif')) {
      return '.gif';
    }
    if (filename.endsWith('.jpg') || filename.endsWith('.jpeg')) {
      return '.jpg';
    }

    final String mime = media.mimeType.trim().toLowerCase();
    if (mime.contains('png')) {
      return '.png';
    }
    if (mime.contains('webp')) {
      return '.webp';
    }
    if (mime.contains('gif')) {
      return '.gif';
    }

    return '.jpg';
  }

  Widget _buildComposer() {
    return Container(
      padding: const EdgeInsets.fromLTRB(16, 12, 12, 12),
      decoration: BoxDecoration(
        color: _panelColor,
        borderRadius: BorderRadius.circular(999),
        boxShadow: const [
          BoxShadow(
            color: Color(0x12000000),
            blurRadius: 18,
            offset: Offset(0, 8),
          ),
        ],
      ),
      child: Row(
        children: [
          Expanded(
            child: TextField(
              controller: _queryController,
              enabled: !_streaming && !_loadingClaw,
              onSubmitted: (_) => _submitFromComposer(),
              decoration: InputDecoration(
                border: InputBorder.none,
                isCollapsed: true,
                hintText: _loadingClaw
                    ? _loadingClawText
                    : (_streaming ? '响应中,请稍候...' : '输入指令...'),
                hintStyle: const TextStyle(
                  color: Color(0xFF98A4BA),
                  fontSize: 16,
                  fontWeight: FontWeight.w600,
                ),
              ),
              style: const TextStyle(
                color: _primaryInk,
                fontSize: 16,
                fontWeight: FontWeight.w600,
              ),
            ),
          ),
          const SizedBox(width: 12),
          InkWell(
            borderRadius: BorderRadius.circular(999),
            onTap: (_streaming || _loadingClaw) ? null : _submitFromComposer,
            child: Container(
              width: 56,
              height: 56,
              decoration: BoxDecoration(
                color: (_streaming || _loadingClaw)
                    ? const Color(0xFF9AA8BF)
                    : _primaryInk,
                shape: BoxShape.circle,
              ),
              child: const Icon(
                Icons.send_rounded,
                color: Colors.white,
                size: 28,
              ),
            ),
          ),
        ],
      ),
    );
  }
}

class DeviceAuthorizationPage extends StatefulWidget {
  const DeviceAuthorizationPage({super.key, required this.accessToken});

  final String accessToken;

  @override
  State<DeviceAuthorizationPage> createState() =>
      _DeviceAuthorizationPageState();
}

class _DeviceAuthorizationPageState extends State<DeviceAuthorizationPage> {
  static const Color _pageBackground = Color(0xFFF1F5FB);
  static const Color _panelColor = Colors.white;
  static const Color _primaryInk = Color(0xFF182238);
  static const Color _secondaryInk = Color(0xFF60708C);
  static const Color _accentBorder = Color(0xFFDCE4F0);

  bool _loading = true;
  String? _error;
  List<Primary> _devices = const <Primary>[];
  final Set<String> _authorizedDeviceIds = <String>{};
  final Set<String> _authorizingDeviceIds = <String>{};
  final Set<String> _revokingDeviceIds = <String>{};

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

  Future<void> _load() async {
    setState(() {
      _loading = true;
      _error = null;
    });
    try {
      final Resp<PrimaryByPage> deviceResp = await SpecService.primaryByPage(
        0,
        limit: 100,
      );
      if (!deviceResp.success) {
        throw StateError(
          '${deviceResp.code} ${deviceResp.message ?? '设备列表加载失败'}'.trim(),
        );
      }

      final List<TgClawAuthorizedDevice> authorizedDevices =
          await TgClawKit.getAuthorizationDevices();
      if (!mounted) {
        return;
      }
      setState(() {
        _devices = deviceResp.data?.list ?? const <Primary>[];
        _authorizedDeviceIds
          ..clear()
          ..addAll(
            authorizedDevices.map(
              (TgClawAuthorizedDevice device) =>
                  _normalizeDeviceId(device.deviceId),
            ),
          );
      });
    } catch (error) {
      if (!mounted) {
        return;
      }
      setState(() {
        _error = error.toString();
      });
      _showSnack('更新设备授权状态失败: $error');
    } finally {
      if (mounted) {
        setState(() {
          _loading = false;
        });
      }
    }
  }

  Future<void> _authorizeDevice(String deviceId) async {
    final String normalizedId = _normalizeDeviceId(deviceId);
    if (normalizedId.isEmpty) {
      _showSnack('设备 ID 为空,无法授权');
      return;
    }

    setState(() {
      _authorizingDeviceIds.add(normalizedId);
    });
    try {
      await TgClawKit.authorize(deviceId: deviceId);
      if (!mounted) {
        return;
      }
      setState(() {
        _authorizedDeviceIds.add(normalizedId);
      });
      _showSnack('设备授权成功');
    } catch (error) {
      _showSnack('设备授权失败: $error');
    } finally {
      if (mounted) {
        setState(() {
          _authorizingDeviceIds.remove(normalizedId);
        });
      }
    }
  }

  Future<void> _revokeDevice(String deviceId, String displayName) async {
    final bool confirmed = await _confirmRevokeAuthorization(
      deviceName: displayName,
      deviceId: deviceId,
    );
    if (!confirmed) {
      return;
    }

    final String normalizedId = _normalizeDeviceId(deviceId);
    setState(() {
      _revokingDeviceIds.add(normalizedId);
    });
    try {
      await TgClawKit.revoke(deviceId: deviceId);
      if (!mounted) {
        return;
      }
      setState(() {
        _authorizedDeviceIds.remove(normalizedId);
      });
      _showSnack('已解除授权');
    } catch (error) {
      _showSnack('解除设备授权失败: $error');
    } finally {
      if (mounted) {
        setState(() {
          _revokingDeviceIds.remove(normalizedId);
        });
      }
    }
  }

  String _normalizeDeviceId(String value) => value.trim().toLowerCase();

  void _showSnack(String message) {
    if (!mounted) {
      return;
    }
    ScaffoldMessenger.of(
      context,
    ).showSnackBar(SnackBar(content: Text(message)));
  }

  Future<bool> _confirmRevokeAuthorization({
    required String deviceName,
    required String deviceId,
  }) async {
    final String target = deviceName.trim().isNotEmpty
        ? deviceName.trim()
        : (deviceId.trim().isNotEmpty ? deviceId.trim() : '当前设备');
    final bool? confirmed = await showDialog<bool>(
      context: context,
      builder: (BuildContext context) {
        return AlertDialog(
          title: const Text('确认解除授权?'),
          content: Text('解除后,AI 将无法继续访问设备“$target”。'),
          actions: [
            TextButton(
              onPressed: () => Navigator.of(context).pop(false),
              child: const Text('取消'),
            ),
            TextButton(
              onPressed: () => Navigator.of(context).pop(true),
              style: TextButton.styleFrom(foregroundColor: _secondaryInk),
              child: const Text('确认解除'),
            ),
          ],
        );
      },
    );
    return confirmed == true;
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: _pageBackground,
      body: SafeArea(
        child: Column(
          children: [
            Padding(
              padding: const EdgeInsets.fromLTRB(18, 18, 18, 8),
              child: Row(
                children: [
                  IconButton(
                    tooltip: '返回 Claw',
                    onPressed: () => Navigator.of(context).pop(),
                    icon: const Icon(
                      Icons.arrow_back_rounded,
                      color: _secondaryInk,
                    ),
                  ),
                  const Expanded(
                    child: Text(
                      '设备授权',
                      textAlign: TextAlign.center,
                      style: TextStyle(
                        fontSize: 26,
                        fontWeight: FontWeight.w800,
                        color: _primaryInk,
                      ),
                    ),
                  ),
                  IconButton(
                    onPressed: _loading ? null : _load,
                    icon: const Icon(
                      Icons.refresh_rounded,
                      color: _secondaryInk,
                    ),
                  ),
                ],
              ),
            ),
            Expanded(
              child: Container(
                margin: const EdgeInsets.fromLTRB(12, 0, 12, 12),
                padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
                decoration: BoxDecoration(
                  color: _pageBackground,
                  borderRadius: BorderRadius.circular(36),
                ),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    const Padding(
                      padding: EdgeInsets.only(left: 2),
                      child: Text(
                        '选择设备进行授权或解除授权。授权后,AI 可访问该设备;解除后将取消访问权限。',
                        style: TextStyle(
                          color: _secondaryInk,
                          fontSize: 14,
                          height: 1.4,
                          fontWeight: FontWeight.w500,
                        ),
                      ),
                    ),
                    const SizedBox(height: 10),
                    const Padding(
                      padding: EdgeInsets.only(left: 2),
                      child: Text(
                        '我的设备',
                        style: TextStyle(
                          color: _secondaryInk,
                          fontSize: 16,
                          fontWeight: FontWeight.w800,
                        ),
                      ),
                    ),
                    const SizedBox(height: 10),
                    Expanded(child: _buildDeviceList()),
                  ],
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildDeviceList() {
    if (_loading) {
      return const Center(
        child: CircularProgressIndicator(color: _secondaryInk),
      );
    }

    if (_error != null && _devices.isEmpty) {
      return Container(
        width: double.infinity,
        padding: const EdgeInsets.all(18),
        decoration: BoxDecoration(
          color: _panelColor,
          borderRadius: BorderRadius.circular(20),
          border: Border.all(color: _accentBorder),
        ),
        child: Text(
          _error!,
          style: const TextStyle(
            color: _secondaryInk,
            fontSize: 14,
            fontWeight: FontWeight.w600,
          ),
        ),
      );
    }

    if (_devices.isEmpty) {
      return Container(
        width: double.infinity,
        padding: const EdgeInsets.all(18),
        decoration: BoxDecoration(
          color: _panelColor,
          borderRadius: BorderRadius.circular(20),
          border: Border.all(color: _accentBorder),
        ),
        child: const Text(
          '暂无设备,请先返回首页添加设备',
          style: TextStyle(
            color: _secondaryInk,
            fontSize: 14,
            fontWeight: FontWeight.w600,
          ),
        ),
      );
    }

    return ListView.separated(
      itemCount: _devices.length,
      separatorBuilder: (_, _) => const SizedBox(height: 10),
      itemBuilder: (BuildContext context, int index) {
        final Primary device = _devices[index];
        final String name = (device.deviceName?.trim().isNotEmpty ?? false)
            ? device.deviceName!.trim()
            : (device.deviceId?.trim().isNotEmpty ?? false)
            ? device.deviceId!.trim()
            : '未命名设备';
        final String id = device.deviceId?.trim() ?? '';
        final String normalizedId = _normalizeDeviceId(id);
        final bool isAuthorized =
            normalizedId.isNotEmpty &&
            _authorizedDeviceIds.contains(normalizedId);
        final bool isAuthorizing = _authorizingDeviceIds.contains(normalizedId);
        final bool isRevoking = _revokingDeviceIds.contains(normalizedId);

        return Container(
          width: double.infinity,
          padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
          decoration: BoxDecoration(
            color: _panelColor,
            borderRadius: BorderRadius.circular(18),
            border: Border.all(color: _accentBorder),
          ),
          child: Row(
            children: [
              Expanded(
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      name,
                      style: const TextStyle(
                        color: _primaryInk,
                        fontSize: 14,
                        fontWeight: FontWeight.w800,
                      ),
                    ),
                    const SizedBox(height: 4),
                    Text(
                      id.isNotEmpty ? id : '设备 ID 为空',
                      style: const TextStyle(
                        color: _secondaryInk,
                        fontSize: 12,
                        fontWeight: FontWeight.w600,
                      ),
                    ),
                  ],
                ),
              ),
              const SizedBox(width: 10),
              Row(
                mainAxisSize: MainAxisSize.min,
                children: [
                  if (isAuthorized && !isRevoking) ...[
                    const Text(
                      '已授权',
                      style: TextStyle(
                        color: Color(0xFF2F7D4A),
                        fontSize: 12,
                        fontWeight: FontWeight.w700,
                      ),
                    ),
                    const SizedBox(width: 8),
                  ],
                  OutlinedButton(
                    onPressed:
                        (normalizedId.isEmpty || isAuthorizing || isRevoking)
                        ? null
                        : () {
                            if (isAuthorized) {
                              _revokeDevice(id, name);
                              return;
                            }
                            _authorizeDevice(id);
                          },
                    style: OutlinedButton.styleFrom(
                      minimumSize: const Size(72, 34),
                      padding: const EdgeInsets.symmetric(
                        horizontal: 8,
                        vertical: 6,
                      ),
                      side: const BorderSide(color: _secondaryInk, width: 1),
                      shape: RoundedRectangleBorder(
                        borderRadius: BorderRadius.circular(999),
                      ),
                      foregroundColor: _secondaryInk,
                      backgroundColor: Colors.white,
                      textStyle: const TextStyle(
                        fontSize: 12,
                        fontWeight: FontWeight.w700,
                      ),
                    ),
                    child: Text(
                      isAuthorizing
                          ? '授权中...'
                          : (isRevoking
                                ? '解除中...'
                                : (isAuthorized ? '去解除' : '去授权')),
                    ),
                  ),
                ],
              ),
            ],
          ),
        );
      },
    );
  }
}

class ExampleProfilePage extends StatelessWidget {
  const ExampleProfilePage({super.key, required this.accessToken});

  final String accessToken;

  Future<void> _logout(BuildContext context) async {
    await ExampleStore.instance.clearLogin();
    await TgClawKit.initialize(
      environment: ExampleEnvironment.fromIndex(
        ExampleStore.instance.apiEnv,
      ).toPluginEnvironment(),
      authAppId: ExampleStore.instance.appId,
      authPackageName: ExampleStore.instance.packageName,
    );
    if (!context.mounted) {
      return;
    }
    Navigator.of(context).pushAndRemoveUntil(
      MaterialPageRoute(builder: (_) => const LoginPage()),
      (Route<dynamic> route) => false,
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('我的')),
      body: Padding(
        padding: const EdgeInsets.all(24),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              '当前环境: ${ExampleEnvironment.fromIndex(ExampleStore.instance.apiEnv).label}',
            ),
            const SizedBox(height: 8),
            Text('App ID: ${ExampleStore.instance.appId}'),
            const SizedBox(height: 8),
            Text('Package: ${ExampleStore.instance.packageName}'),
            const SizedBox(height: 8),
            Text('AccessToken: ${accessToken.isNotEmpty ? '已登录' : '未登录'}'),
            const SizedBox(height: 24),
            FilledButton(
              onPressed: () => _logout(context),
              child: const Text('退出登录'),
            ),
            const SizedBox(height: 12),
            OutlinedButton(
              onPressed: () {
                Navigator.of(
                  context,
                ).push(MaterialPageRoute(builder: (_) => const SettingsPage()));
              },
              child: const Text('环境配置'),
            ),
          ],
        ),
      ),
    );
  }
}

class SettingsPage extends StatefulWidget {
  const SettingsPage({super.key});

  @override
  State<SettingsPage> createState() => _SettingsPageState();
}

class _SettingsPageState extends State<SettingsPage> {
  late final TextEditingController _appIdController;
  late final TextEditingController _packageNameController;
  late int _apiEnv;

  @override
  void initState() {
    super.initState();
    _appIdController = TextEditingController(text: ExampleStore.instance.appId);
    _packageNameController = TextEditingController(
      text: ExampleStore.instance.packageName,
    );
    _apiEnv = ExampleStore.instance.apiEnv;
  }

  @override
  void dispose() {
    _appIdController.dispose();
    _packageNameController.dispose();
    super.dispose();
  }

  Future<void> _save() async {
    await ExampleStore.instance.saveConfig(
      appId: _appIdController.text.trim(),
      packageName: _packageNameController.text.trim(),
      apiEnv: _apiEnv,
    );
    if (!mounted) {
      return;
    }
    ScaffoldMessenger.of(
      context,
    ).showSnackBar(const SnackBar(content: Text('配置已保存,重启示例页后生效')));
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('环境配置')),
      body: ListView(
        padding: const EdgeInsets.all(16),
        children: [
          TextField(
            controller: _appIdController,
            decoration: const InputDecoration(
              labelText: 'App ID',
              border: OutlineInputBorder(),
            ),
          ),
          const SizedBox(height: 12),
          TextField(
            controller: _packageNameController,
            decoration: const InputDecoration(
              labelText: 'Package Name',
              border: OutlineInputBorder(),
            ),
          ),
          const SizedBox(height: 12),
          DropdownButtonFormField<int>(
            initialValue: _apiEnv,
            decoration: const InputDecoration(
              labelText: '环境',
              border: OutlineInputBorder(),
            ),
            items: ExampleEnvironment.values
                .map(
                  (ExampleEnvironment environment) => DropdownMenuItem<int>(
                    value: environment.index,
                    child: Text(environment.label),
                  ),
                )
                .toList(growable: false),
            onChanged: (int? value) {
              if (value == null) {
                return;
              }
              setState(() {
                _apiEnv = value;
              });
            },
          ),
          const SizedBox(height: 16),
          FilledButton(onPressed: _save, child: const Text('保存配置')),
        ],
      ),
    );
  }
}

class ExampleStore {
  ExampleStore._();

  static final ExampleStore instance = ExampleStore._();

  static const String _keyAppId = 'tg_claw_example_app_id';
  static const String _keyPackageName = 'tg_claw_example_package_name';
  static const String _keyApiEnv = 'tg_claw_example_api_env';
  static const String _keyLoginAccount = 'tg_claw_example_login_account';
  static const String _keyLoginToken = 'tg_claw_example_login_token';

  late SharedPreferences _preferences;
  String appId = '';
  String packageName = '';
  int apiEnv = 0;
  String savedAccount = '';
  String accessToken = '';

  Future<void> init() async {
    _preferences = await SharedPreferences.getInstance();
    appId = _preferences.getString(_keyAppId) ?? appId;
    packageName = _preferences.getString(_keyPackageName) ?? packageName;
    apiEnv = _preferences.getInt(_keyApiEnv) ?? apiEnv;
    savedAccount = _preferences.getString(_keyLoginAccount) ?? '';
    accessToken = _preferences.getString(_keyLoginToken) ?? '';
  }

  Future<void> saveConfig({
    required String appId,
    required String packageName,
    required int apiEnv,
  }) async {
    this.appId = appId;
    this.packageName = packageName;
    this.apiEnv = apiEnv;
    await _preferences.setString(_keyAppId, appId);
    await _preferences.setString(_keyPackageName, packageName);
    await _preferences.setInt(_keyApiEnv, apiEnv);
  }

  Future<void> saveLogin({
    required String account,
    required String accessToken,
  }) async {
    savedAccount = account;
    this.accessToken = accessToken;
    await _preferences.setString(_keyLoginAccount, account);
    await _preferences.setString(_keyLoginToken, accessToken);
  }

  Future<void> clearLogin() async {
    savedAccount = '';
    accessToken = '';
    await _preferences.remove(_keyLoginAccount);
    await _preferences.remove(_keyLoginToken);
  }
}

enum ExampleEnvironment {
  production('正式'),
  preRelease('预发'),
  test('测试');

  const ExampleEnvironment(this.label);

  final String label;

  static ExampleEnvironment fromIndex(int index) {
    if (index < 0 || index >= values.length) {
      return ExampleEnvironment.production;
    }
    return values[index];
  }

  TgClawEnvironment toPluginEnvironment() {
    switch (this) {
      case ExampleEnvironment.production:
        return TgClawEnvironment.production;
      case ExampleEnvironment.preRelease:
        return TgClawEnvironment.preRelease;
      case ExampleEnvironment.test:
        return TgClawEnvironment.test;
    }
  }

  Environment toRapidEnvironment() {
    switch (this) {
      case ExampleEnvironment.production:
        return Environment.release;
      case ExampleEnvironment.preRelease:
        return Environment.pre;
      case ExampleEnvironment.test:
        return Environment.test;
    }
  }
}

class ExampleChatMessage {
  const ExampleChatMessage({
    required this.id,
    required this.role,
    required this.text,
    required this.streaming,
    required this.media,
    this.toolName,
    this.toolArguments,
    this.toolOutput,
  });

  final String id;
  final TgClawMessageRole role;
  final String text;
  final bool streaming;
  final String? toolName;
  final String? toolArguments;
  final String? toolOutput;
  final List<ExampleChatMedia> media;

  bool get isUser => role == TgClawMessageRole.user;

  String get title {
    switch (role) {
      case TgClawMessageRole.reasoning:
        return '思考过程';
      case TgClawMessageRole.toolCall:
        return '工具调用';
      case TgClawMessageRole.toolOutput:
        return '工具结果';
      case TgClawMessageRole.system:
        return '系统消息';
      case TgClawMessageRole.assistant:
      case TgClawMessageRole.user:
        return '';
    }
  }

  factory ExampleChatMessage.user(String text) {
    return ExampleChatMessage(
      id: 'user_${DateTime.now().millisecondsSinceEpoch}',
      role: TgClawMessageRole.user,
      text: text,
      streaming: false,
      media: const <ExampleChatMedia>[],
    );
  }

  factory ExampleChatMessage.system(String text) {
    return ExampleChatMessage(
      id: 'system_${DateTime.now().millisecondsSinceEpoch}',
      role: TgClawMessageRole.system,
      text: text,
      streaming: false,
      media: const <ExampleChatMedia>[],
    );
  }

  factory ExampleChatMessage.fromDomain(TgClawMessage message) {
    return ExampleChatMessage(
      id: message.id,
      role: message.role,
      text: message.text,
      streaming: message.streaming,
      toolName: message.toolName,
      toolArguments: message.toolArguments,
      toolOutput: message.toolOutput,
      media: message.media
          .map((TgClawMedia media) => ExampleChatMedia.fromDomain(media))
          .toList(growable: false),
    );
  }

  ExampleChatMessage copyWith({
    bool? streaming,
    String? text,
    String? toolName,
    String? toolArguments,
    String? toolOutput,
    List<ExampleChatMedia>? media,
  }) {
    return ExampleChatMessage(
      id: id,
      role: role,
      text: text ?? this.text,
      streaming: streaming ?? this.streaming,
      toolName: toolName ?? this.toolName,
      toolArguments: toolArguments ?? this.toolArguments,
      toolOutput: toolOutput ?? this.toolOutput,
      media: media ?? this.media,
    );
  }
}

class ExampleChatMedia {
  const ExampleChatMedia({
    required this.type,
    required this.uri,
    required this.mimeType,
    this.filename,
  });

  final TgClawMediaType type;
  final String uri;
  final String mimeType;
  final String? filename;

  factory ExampleChatMedia.fromDomain(TgClawMedia media) {
    return ExampleChatMedia(
      type: media.type,
      uri: media.uri,
      mimeType: media.mimeType,
      filename: media.filename,
    );
  }
}

class _ImagePreviewPage extends StatelessWidget {
  const _ImagePreviewPage({
    required this.title,
    required this.bytes,
    required this.onDownload,
  });

  final String title;
  final typed_data.Uint8List bytes;
  final VoidCallback onDownload;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.black,
      appBar: AppBar(
        backgroundColor: Colors.black,
        foregroundColor: Colors.white,
        title: Text(title),
        actions: [
          IconButton(
            onPressed: onDownload,
            icon: const Icon(Icons.download_rounded),
          ),
        ],
      ),
      body: Center(
        child: InteractiveViewer(
          minScale: 0.8,
          maxScale: 4,
          child: Image.memory(bytes, fit: BoxFit.contain),
        ),
      ),
    );
  }
}
1
likes
0
points
365
downloads

Publisher

unverified uploader

Weekly Downloads

Flutter plugin for Claw auth, hidden webcam env bootstrap, chat session management, and device authorization.

Homepage

License

unknown (license)

Dependencies

crypto, flutter, http

More

Packages that depend on tg_claw_kit

Packages that implement tg_claw_kit