import 'package:flutter/material.dart'; import 'avatar_bridge.dart'; /// 底部抽屉控制面板。 /// 视觉对齐 H5 调试面板:分区、按钮、滑条、模拟对话。 class ControlPanelSheet extends StatefulWidget { final AvatarBridge bridge; const ControlPanelSheet({super.key, required this.bridge}); @override State createState() => _ControlPanelSheetState(); } class _ControlPanelSheetState extends State { AvatarManifest? _manifest; double _mouth = 0; @override void initState() { super.initState(); _loadManifest(); } Future _loadManifest() async { final m = await widget.bridge.fetchManifest(); if (mounted) setState(() => _manifest = m); } @override Widget build(BuildContext context) { return DraggableScrollableSheet( initialChildSize: 0.55, minChildSize: 0.25, maxChildSize: 0.92, builder: (context, scrollController) { return Container( decoration: const BoxDecoration( color: Color(0xE60F0F14), borderRadius: BorderRadius.vertical(top: Radius.circular(16)), border: Border(top: BorderSide(color: Color(0xFF333333))), ), child: Column( children: [ // 顶部把手 + 标题 const SizedBox(height: 8), Container( width: 36, height: 4, decoration: BoxDecoration( color: Colors.white24, borderRadius: BorderRadius.circular(2), ), ), const Padding( padding: EdgeInsets.symmetric(vertical: 8), child: Text( '控制面板', style: TextStyle( color: Color(0xFF80C0FF), fontSize: 16, fontWeight: FontWeight.w600, letterSpacing: 0.5, ), ), ), const Divider(height: 1, color: Color(0xFF2A2A2F)), // 内容滚动区 Expanded( child: _manifest == null ? const Center(child: CircularProgressIndicator()) : ListView( controller: scrollController, padding: const EdgeInsets.fromLTRB(16, 12, 16, 24), children: [ _section( title: '角色状态', child: _ButtonGroup( buttons: const [ ('idle 待机', 'idle'), ('listening 倾听', 'listening'), ('thinking 思考', 'thinking'), ('speaking 说话', 'speaking'), ], onTap: (arg) => widget.bridge.setState(arg), ), ), _section( title: '表情切换', child: _ButtonGroup( buttons: _manifest!.expressions .map((e) => (e, e)) .toList(), onTap: (arg) => widget.bridge.setExpression(arg), ), ), _section( title: '动作播放', child: _ButtonGroup( buttons: _manifest!.motions.entries .expand((entry) => List.generate( entry.value, (i) => ( '${entry.key}#$i', '${entry.key}|$i', ), )) .toList(), onTap: (arg) { final parts = arg.split('|'); widget.bridge.playMotion( parts[0], int.parse(parts[1]), ); }, ), ), _section( title: '嘴型驱动 ${_mouth.toStringAsFixed(2)}', child: Column( children: [ Slider( value: _mouth, onChanged: (v) { setState(() => _mouth = v); widget.bridge.setMouthOpen(v); }, activeColor: const Color(0xFF4488DD), inactiveColor: Colors.white12, ), _ButtonGroup( buttons: const [ ('闭', '0'), ('半开', '0.5'), ('全开', '1'), ], onTap: (arg) { final v = double.parse(arg); setState(() => _mouth = v); widget.bridge.setMouthOpen(v); }, ), ], ), ), _section( title: '语义动作', child: _ButtonGroup( buttons: _manifest!.actions .map((a) => (a, a)) .toList(), onTap: (arg) => widget.bridge.playAction(arg), ), hint: '对应未来 LLM Function Call 触发', ), _section( title: '跳舞', child: Row( children: [ Expanded( child: FilledButton.icon( onPressed: () => widget.bridge.startDance(), icon: const Text('💃', style: TextStyle(fontSize: 14)), label: const Text('开始跳舞'), style: FilledButton.styleFrom( backgroundColor: const Color(0xFF2766B6), foregroundColor: Colors.white, padding: const EdgeInsets.symmetric( vertical: 10), ), ), ), const SizedBox(width: 8), OutlinedButton( onPressed: () => widget.bridge.stopDance(), style: OutlinedButton.styleFrom( foregroundColor: Colors.white70, side: const BorderSide( color: Color(0xFF444444)), padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 10), ), child: const Text('停止'), ), ], ), hint: '程序化驱动身体参数 + 循环切 Dance motion', ), _section( title: '事件模拟', child: SizedBox( width: double.infinity, child: FilledButton.icon( onPressed: () => widget.bridge.playMockConversation(), icon: const Icon(Icons.play_arrow, size: 18), label: const Text('播放模拟对话'), style: FilledButton.styleFrom( backgroundColor: const Color(0xFF2766B6), foregroundColor: Colors.white, padding: const EdgeInsets.symmetric( vertical: 10), ), ), ), hint: 'listening → thinking → 微笑+说话 → idle', ), ], ), ), ], ), ); }, ); } Widget _section({ required String title, required Widget child, String? hint, }) { return Padding( padding: const EdgeInsets.only(bottom: 18), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( title, style: const TextStyle( color: Color(0xFF80C0FF), fontSize: 13, fontWeight: FontWeight.w600, letterSpacing: 0.5, ), ), const SizedBox(height: 8), child, if (hint != null) Padding( padding: const EdgeInsets.only(top: 6), child: Text( hint, style: const TextStyle( color: Colors.white38, fontSize: 11, ), ), ), ], ), ); } } class _ButtonGroup extends StatelessWidget { /// 每项 (label, argument) final List<(String, String)> buttons; final void Function(String arg) onTap; const _ButtonGroup({required this.buttons, required this.onTap}); @override Widget build(BuildContext context) { return Wrap( spacing: 6, runSpacing: 6, children: buttons.map((b) { return OutlinedButton( onPressed: () => onTap(b.$2), style: OutlinedButton.styleFrom( foregroundColor: Colors.white70, side: const BorderSide(color: Color(0xFF444444)), backgroundColor: const Color(0xFF222228), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), minimumSize: const Size(0, 32), tapTargetSize: MaterialTapTargetSize.shrinkWrap, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(4), ), ), child: Text(b.$1, style: const TextStyle(fontSize: 12)), ); }).toList(), ); } }