首次提交:初始化项目

This commit is contained in:
lhr
2025-12-30 01:06:42 +08:00
parent 6dcba8d533
commit 9d45d4c726
141 changed files with 6186 additions and 133 deletions

View 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();
}
}

View 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,
),
),
);
}
}