首次提交:初始化项目
This commit is contained in:
223
lib/screens/chat_screen.dart
Normal file
223
lib/screens/chat_screen.dart
Normal file
@@ -0,0 +1,223 @@
|
||||
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';
|
||||
|
||||
class ChatScreen extends StatefulWidget {
|
||||
const ChatScreen({super.key});
|
||||
|
||||
@override
|
||||
State<ChatScreen> createState() => _ChatScreenState();
|
||||
}
|
||||
|
||||
class _ChatScreenState extends State<ChatScreen> {
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
|
||||
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();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
content: 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: const 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();
|
||||
}
|
||||
}
|
||||
177
lib/screens/settings_screen.dart
Normal file
177
lib/screens/settings_screen.dart
Normal file
@@ -0,0 +1,177 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../theme/app_theme.dart';
|
||||
import '../widgets/page_mixin.dart';
|
||||
|
||||
const List<String> accentColorNames = [
|
||||
'System',
|
||||
'Yellow',
|
||||
'Orange',
|
||||
'Red',
|
||||
'Magenta',
|
||||
'Purple',
|
||||
'Blue',
|
||||
'Teal',
|
||||
'Green',
|
||||
];
|
||||
|
||||
class SettingsScreen extends StatefulWidget {
|
||||
const SettingsScreen({super.key});
|
||||
|
||||
@override
|
||||
State<SettingsScreen> createState() => _SettingsScreenState();
|
||||
}
|
||||
|
||||
class _SettingsScreenState extends State<SettingsScreen> with PageMixin {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final appTheme = context.watch<AppTheme>();
|
||||
const spacer = SizedBox(height: 10);
|
||||
const biggerSpacer = SizedBox(height: 40);
|
||||
|
||||
return ScaffoldPage.scrollable(
|
||||
header: const PageHeader(title: Text('Settings')),
|
||||
children: [
|
||||
Text('Theme mode', style: FluentTheme.of(context).typography.subtitle),
|
||||
spacer,
|
||||
...List.generate(ThemeMode.values.length, (index) {
|
||||
final mode = ThemeMode.values[index];
|
||||
return Padding(
|
||||
padding: const EdgeInsetsDirectional.only(bottom: 8),
|
||||
child: RadioButton(
|
||||
checked: appTheme.mode == mode,
|
||||
onChanged: (value) {
|
||||
if (value) {
|
||||
appTheme.mode = mode;
|
||||
}
|
||||
},
|
||||
content: Text('$mode'.replaceAll('ThemeMode.', '')),
|
||||
),
|
||||
);
|
||||
}),
|
||||
biggerSpacer,
|
||||
Text(
|
||||
'Navigation Pane Display Mode',
|
||||
style: FluentTheme.of(context).typography.subtitle,
|
||||
),
|
||||
spacer,
|
||||
...List.generate(PaneDisplayMode.values.length, (index) {
|
||||
final mode = PaneDisplayMode.values[index];
|
||||
return Padding(
|
||||
padding: const EdgeInsetsDirectional.only(bottom: 8),
|
||||
child: RadioButton(
|
||||
checked: appTheme.displayMode == mode,
|
||||
onChanged: (value) {
|
||||
if (value) appTheme.displayMode = mode;
|
||||
},
|
||||
content: Text(mode.toString().replaceAll('PaneDisplayMode.', '')),
|
||||
),
|
||||
);
|
||||
}),
|
||||
biggerSpacer,
|
||||
Text(
|
||||
'Navigation Indicator',
|
||||
style: FluentTheme.of(context).typography.subtitle,
|
||||
),
|
||||
spacer,
|
||||
...List.generate(NavigationIndicators.values.length, (index) {
|
||||
final mode = NavigationIndicators.values[index];
|
||||
return Padding(
|
||||
padding: const EdgeInsetsDirectional.only(bottom: 8),
|
||||
child: RadioButton(
|
||||
checked: appTheme.indicator == mode,
|
||||
onChanged: (value) {
|
||||
if (value) appTheme.indicator = mode;
|
||||
},
|
||||
content: Text(
|
||||
mode.toString().replaceAll('NavigationIndicators.', ''),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
biggerSpacer,
|
||||
Text(
|
||||
'Accent Color',
|
||||
style: FluentTheme.of(context).typography.subtitle,
|
||||
),
|
||||
spacer,
|
||||
Wrap(
|
||||
children: [
|
||||
Tooltip(
|
||||
message: accentColorNames[0],
|
||||
child: _buildColorBlock(appTheme, systemAccentColor),
|
||||
),
|
||||
...List.generate(Colors.accentColors.length, (index) {
|
||||
final color = Colors.accentColors[index];
|
||||
return Tooltip(
|
||||
message: accentColorNames[index + 1],
|
||||
child: _buildColorBlock(appTheme, color),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
biggerSpacer,
|
||||
Text(
|
||||
'Text Direction',
|
||||
style: FluentTheme.of(context).typography.subtitle,
|
||||
),
|
||||
spacer,
|
||||
...List.generate(TextDirection.values.length, (index) {
|
||||
final direction = TextDirection.values[index];
|
||||
return Padding(
|
||||
padding: const EdgeInsetsDirectional.only(bottom: 8),
|
||||
child: RadioButton(
|
||||
checked: appTheme.textDirection == direction,
|
||||
onChanged: (value) {
|
||||
if (value) {
|
||||
appTheme.textDirection = direction;
|
||||
}
|
||||
},
|
||||
content: Text(
|
||||
'$direction'
|
||||
.replaceAll('TextDirection.', '')
|
||||
.replaceAll('rtl', 'Right to left')
|
||||
.replaceAll('ltr', 'Left to right'),
|
||||
),
|
||||
),
|
||||
);
|
||||
}).reversed,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildColorBlock(AppTheme appTheme, AccentColor color) {
|
||||
return Padding(
|
||||
padding: const EdgeInsetsDirectional.all(2),
|
||||
child: Button(
|
||||
onPressed: () {
|
||||
appTheme.color = color;
|
||||
},
|
||||
style: ButtonStyle(
|
||||
padding: const WidgetStatePropertyAll(EdgeInsetsDirectional.zero),
|
||||
backgroundColor: WidgetStateProperty.resolveWith((states) {
|
||||
if (states.isPressed) {
|
||||
return color.light;
|
||||
} else if (states.isHovered) {
|
||||
return color.lighter;
|
||||
}
|
||||
return color;
|
||||
}),
|
||||
),
|
||||
child: Container(
|
||||
height: 40,
|
||||
width: 40,
|
||||
alignment: AlignmentDirectional.center,
|
||||
child: appTheme.color == color
|
||||
? Icon(
|
||||
FluentIcons.check_mark,
|
||||
color: color.basedOnLuminance(),
|
||||
size: 22,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user