import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../models/chat_models.dart'; import '../providers/character_provider.dart'; import '../providers/chat_provider.dart'; import '../theme/app_colors.dart'; class ChatScreen extends StatefulWidget { const ChatScreen({super.key}); @override State createState() => _ChatScreenState(); } class _ChatScreenState extends State { final TextEditingController _controller = TextEditingController(); final ScrollController _scrollController = ScrollController(); @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { context.read().refresh(); }); } @override void dispose() { _controller.dispose(); _scrollController.dispose(); super.dispose(); } void _scrollToEnd() { WidgetsBinding.instance.addPostFrameCallback((_) { if (!_scrollController.hasClients) return; _scrollController.animateTo( _scrollController.position.maxScrollExtent + 80, duration: const Duration(milliseconds: 250), curve: Curves.easeOut, ); }); } Future _sendText() async { final text = _controller.text; _controller.clear(); await context.read().sendText(text); _scrollToEnd(); } @override Widget build(BuildContext context) { final chat = context.watch(); final character = context.watch(); final activeName = character.activePack?.name ?? '当前角色'; return Scaffold( appBar: AppBar( title: Text('对话 · $activeName'), actions: [ IconButton( tooltip: '清空会话', onPressed: chat.isBusy ? null : chat.clearHistory, icon: const Icon(Icons.delete_outline_rounded), ), ], ), body: Column( children: [ if (chat.status != null) Container( width: double.infinity, color: AppColors.card, padding: const EdgeInsets.symmetric( horizontal: AppSpacing.md, vertical: 8, ), child: Text( chat.status!, style: TextStyle( color: chat.error != null ? AppColors.error : AppColors.textSecondary, ), ), ), Expanded( child: chat.messages.isEmpty ? Center( child: Padding( padding: const EdgeInsets.all(AppSpacing.lg), child: Text( '文字发送,或按住下方麦克风说话。\n回复会在手机播放(设备本地不出声)。', textAlign: TextAlign.center, style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: AppColors.textSecondary, ), ), ), ) : ListView.builder( controller: _scrollController, padding: const EdgeInsets.all(AppSpacing.md), itemCount: chat.messages.length, itemBuilder: (context, index) { final msg = chat.messages[index]; final isUser = msg.role == ChatBubbleRole.user; return Align( alignment: isUser ? Alignment.centerRight : Alignment.centerLeft, child: Container( margin: const EdgeInsets.only(bottom: 10), padding: const EdgeInsets.symmetric( horizontal: 14, vertical: 10, ), constraints: BoxConstraints( maxWidth: MediaQuery.of(context).size.width * 0.82, ), decoration: BoxDecoration( color: isUser ? AppColors.primary.withValues(alpha: 0.25) : AppColors.card, borderRadius: BorderRadius.circular(16), border: Border.all(color: AppColors.border), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( msg.text, style: Theme.of(context).textTheme.bodyMedium, ), if (msg.audioPath != null && msg.audioPath!.isNotEmpty && !msg.isPending) ...[ const SizedBox(height: 8), TextButton.icon( onPressed: () => chat.playReply(msg.audioPath!), icon: const Icon(Icons.volume_up_rounded), label: const Text('重播语音'), ), ], if (msg.isPending) const Padding( padding: EdgeInsets.only(top: 6), child: SizedBox( width: 14, height: 14, child: CircularProgressIndicator( strokeWidth: 2, ), ), ), ], ), ), ); }, ), ), SafeArea( top: false, child: Padding( padding: const EdgeInsets.fromLTRB(12, 8, 12, 12), child: Row( children: [ Expanded( child: TextField( controller: _controller, enabled: !chat.isBusy && !chat.isRecording, minLines: 1, maxLines: 4, decoration: const InputDecoration( hintText: '输入文字和角色对话...', ), onSubmitted: (_) => _sendText(), ), ), const SizedBox(width: 8), IconButton.filled( onPressed: chat.isBusy || chat.isRecording ? null : _sendText, icon: const Icon(Icons.send_rounded), ), const SizedBox(width: 4), GestureDetector( onLongPressStart: (_) async { final ok = await context.read().startRecording(); if (!ok && context.mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( context.read().error ?? '无法录音', ), ), ); } }, onLongPressEnd: (_) async { await context.read().stopRecordingAndSend(); _scrollToEnd(); }, onLongPressCancel: () { context.read().cancelRecording(); }, child: CircleAvatar( radius: 26, backgroundColor: chat.isRecording ? AppColors.error : AppColors.primary.withValues(alpha: 0.2), child: Icon( chat.isRecording ? Icons.mic : Icons.mic_none_rounded, color: chat.isRecording ? Colors.white : AppColors.primary, ), ), ), ], ), ), ), ], ), ); } }