Files
llm_chat/lib/screens/chat_screen.dart

261 lines
7.3 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:provider/provider.dart';
import '../providers/chat_provider.dart';
import '../widgets/message_bubble.dart';
import '../widgets/typing_indicator.dart';
import '../widgets/chat_input.dart';
import 'map_screen.dart'; // 导入地图界面
// 定义一个 GlobalKey 来访问 _ChatScreenState
final GlobalKey<_ChatScreenState> chatScreenStateKey = GlobalKey<_ChatScreenState>();
class ChatScreen extends StatefulWidget {
const ChatScreen({super.key});
@override
State<ChatScreen> createState() => _ChatScreenState();
}
class _ChatScreenState extends State<ChatScreen> {
final ScrollController _scrollController = ScrollController();
bool _mapViewOpen = false; // 控制地图界面是否展开
void _toggleMapView() {
setState(() {
_mapViewOpen = !_mapViewOpen;
});
}
void _scrollToBottom() {
if (_scrollController.hasClients) {
Future.delayed(const Duration(milliseconds: 100), () {
_scrollController.animateTo(
_scrollController.position.maxScrollExtent,
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
);
});
}
}
@override
Widget build(BuildContext context) {
return ScaffoldPage(
header: PageHeader(
title: Row(
children: [
Container(
width: 32,
height: 32,
decoration: BoxDecoration(
color: FluentTheme.of(context).accentColor,
borderRadius: BorderRadius.circular(8),
),
child: const Icon(
FluentIcons.robot,
color: Colors.white,
size: 18,
),
),
const SizedBox(width: 12),
const Text('AI Chat'),
],
),
commandBar: CommandBar(
mainAxisAlignment: MainAxisAlignment.end,
primaryItems: [
CommandBarButton(
icon: const Icon(FluentIcons.add),
label: const Text('New Chat'),
onPressed: () {
context.read<ChatProvider>().clearMessages();
},
),
// 添加地图按钮
CommandBarButton(
icon: const Icon(FluentIcons.map_layers),
label: const Text('Map'),
onPressed: _toggleMapView, // 切换地图视图
),
],
),
),
content: Row( // 修改为Row布局实现左右分屏
children: [
// 聊天界面部分使用Expanded来正确分配空间
Expanded(
flex: 1,
child: _buildChatContent(),
),
// 地图界面部分,根据状态决定是否显示
if (_mapViewOpen)
Expanded(
child: const MapUiPage(), // 使用现有的地图页面
),
],
),
);
}
// 提取聊天内容部分到单独的方法
Widget _buildChatContent() {
return Column(
children: [
Expanded(child: _buildMessageList()),
_buildInputArea(),
],
);
}
Widget _buildMessageList() {
return Consumer<ChatProvider>(
builder: (context, chatProvider, child) {
final messages = chatProvider.messages;
final isTyping = chatProvider.isTyping;
final currentResponse = chatProvider.currentAiResponse;
WidgetsBinding.instance.addPostFrameCallback((_) {
_scrollToBottom();
});
if (messages.isEmpty && !isTyping) {
return _buildEmptyState();
}
return ListView.builder(
controller: _scrollController,
padding: const EdgeInsets.all(16),
itemCount: messages.length + (isTyping ? 1 : 0),
itemBuilder: (context, index) {
if (index == messages.length && isTyping) {
if (currentResponse.isNotEmpty) {
return _buildTypingResponse(currentResponse);
}
return const TypingIndicator();
}
return MessageBubble(message: messages[index], showAvatar: true);
},
);
},
);
}
Widget _buildTypingResponse(String content) {
final theme = FluentTheme.of(context);
return Padding(
padding: const EdgeInsets.only(bottom: 16),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: theme.accentColor,
borderRadius: BorderRadius.circular(8),
),
child: const Icon(FluentIcons.robot, color: Colors.white, size: 18),
),
const SizedBox(width: 12),
Flexible(
child: Card(
padding: const EdgeInsets.all(12),
child: MarkdownBody(
data: content,
styleSheet: MarkdownStyleSheet(
p: theme.typography.body,
h1: theme.typography.title,
h2: theme.typography.subtitle,
h3: theme.typography.bodyLarge,
code: TextStyle(
fontFamily: 'Consolas',
backgroundColor: theme.cardColor,
),
),
),
),
),
const SizedBox(width: 48),
],
),
);
}
Widget _buildEmptyState() {
final theme = FluentTheme.of(context);
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 80,
height: 80,
decoration: BoxDecoration(
color: theme.accentColor,
borderRadius: BorderRadius.circular(20),
),
child: Icon(FluentIcons.robot, color: Colors.white, size: 40),
),
const SizedBox(height: 24),
Text('Hello! I\'m your AI Assistant', style: theme.typography.title),
const SizedBox(height: 12),
Text(
'How can I help you today?',
style: theme.typography.body?.copyWith(
color: theme.typography.body?.color?.withAlpha(180),
),
),
const SizedBox(height: 32),
_buildSuggestionChips(),
],
),
);
}
Widget _buildSuggestionChips() {
final suggestions = [
'Write a poem',
'Explain quantum computing',
'Help me code',
'Translate to English',
];
return Wrap(
spacing: 8,
runSpacing: 8,
alignment: WrapAlignment.center,
children: suggestions.map((suggestion) {
return Button(
onPressed: () {
context.read<ChatProvider>().sendMessage(suggestion);
},
child: Text(suggestion),
);
}).toList(),
);
}
Widget _buildInputArea() {
return Consumer<ChatProvider>(
builder: (context, chatProvider, child) {
return ChatInput(
onSend: (message) {
chatProvider.sendMessage(message);
},
enabled: !chatProvider.isTyping,
);
},
);
}
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
}