首次提交:初始化项目

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,88 @@
import 'package:fluent_ui/fluent_ui.dart';
class ChatInput extends StatefulWidget {
final Function(String) onSend;
final bool enabled;
const ChatInput({super.key, required this.onSend, this.enabled = true});
@override
State<ChatInput> createState() => _ChatInputState();
}
class _ChatInputState extends State<ChatInput> {
final TextEditingController _controller = TextEditingController();
final FocusNode _focusNode = FocusNode();
bool _hasContent = false;
@override
void initState() {
super.initState();
_controller.addListener(_onTextChanged);
}
void _onTextChanged() {
final hasContent = _controller.text.trim().isNotEmpty;
if (hasContent != _hasContent) {
setState(() {
_hasContent = hasContent;
});
}
}
void _sendMessage() {
if (_controller.text.trim().isNotEmpty && widget.enabled) {
widget.onSend(_controller.text.trim());
_controller.clear();
_focusNode.requestFocus();
}
}
@override
void dispose() {
_controller.dispose();
_focusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final theme = FluentTheme.of(context);
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: theme.scaffoldBackgroundColor,
border: Border(
top: BorderSide(color: theme.resources.dividerStrokeColorDefault),
),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Expanded(
child: ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 150),
child: TextBox(
controller: _controller,
focusNode: _focusNode,
enabled: widget.enabled,
maxLines: null,
placeholder: 'Type a message...',
onSubmitted: (_) => _sendMessage(),
),
),
),
const SizedBox(width: 12),
FilledButton(
onPressed: _hasContent && widget.enabled ? _sendMessage : null,
child: const Padding(
padding: EdgeInsets.symmetric(vertical: 8),
child: Icon(FluentIcons.send, size: 16),
),
),
],
),
);
}
}

View File

@@ -0,0 +1,128 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import '../models/message.dart';
class MessageBubble extends StatelessWidget {
final Message message;
final bool showAvatar;
const MessageBubble({
super.key,
required this.message,
this.showAvatar = true,
});
@override
Widget build(BuildContext context) {
final theme = FluentTheme.of(context);
return Padding(
padding: const EdgeInsets.only(bottom: 16),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: message.isUser
? MainAxisAlignment.end
: MainAxisAlignment.start,
children: [
if (!message.isUser && showAvatar) ...[
_buildAiAvatar(theme),
const SizedBox(width: 12),
],
Flexible(child: _buildMessageContent(context, theme)),
if (message.isUser && showAvatar) ...[
const SizedBox(width: 12),
_buildUserAvatar(theme),
],
],
),
);
}
Widget _buildAiAvatar(FluentThemeData theme) {
return Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: theme.accentColor,
borderRadius: BorderRadius.circular(8),
),
child: const Icon(FluentIcons.robot, color: Colors.white, size: 18),
);
}
Widget _buildUserAvatar(FluentThemeData theme) {
return Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: theme.accentColor.light,
borderRadius: BorderRadius.circular(8),
),
child: const Icon(FluentIcons.contact, color: Colors.white, size: 18),
);
}
Widget _buildMessageContent(BuildContext context, FluentThemeData theme) {
if (message.isUser) {
return Container(
constraints: const BoxConstraints(maxWidth: 600),
child: Card(
backgroundColor: theme.accentColor,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Text(
message.content,
style: const TextStyle(color: Colors.white),
),
),
);
} else {
return Container(
constraints: const BoxConstraints(maxWidth: 600),
child: Card(
padding: const EdgeInsets.all(12),
child: MarkdownBody(
data: message.content,
selectable: true,
styleSheet: MarkdownStyleSheet(
p: theme.typography.body,
h1: theme.typography.title,
h2: theme.typography.subtitle,
h3: theme.typography.bodyLarge,
code: TextStyle(
fontFamily: 'Consolas',
fontSize: 13,
backgroundColor: theme.cardColor,
color: theme.accentColor,
),
codeblockDecoration: BoxDecoration(
color: theme.cardColor,
borderRadius: BorderRadius.circular(4),
),
blockquote: TextStyle(
color: theme.typography.body?.color?.withAlpha(180),
fontStyle: FontStyle.italic,
),
blockquoteDecoration: BoxDecoration(
border: Border(
left: BorderSide(color: theme.accentColor, width: 3),
),
),
tableHead: theme.typography.bodyStrong,
tableBody: theme.typography.body,
tableBorder: TableBorder.all(
color: theme.resources.dividerStrokeColorDefault,
),
tableCellsPadding: const EdgeInsets.all(8),
strong: theme.typography.bodyStrong,
em: TextStyle(
fontStyle: FontStyle.italic,
color: theme.typography.body?.color?.withAlpha(200),
),
),
),
),
);
}
}
}

View File

@@ -0,0 +1,31 @@
import 'package:fluent_ui/fluent_ui.dart';
mixin PageMixin {
Widget description({required Widget content}) {
return Builder(
builder: (context) {
return Padding(
padding: const EdgeInsetsDirectional.only(bottom: 4),
child: DefaultTextStyle(
style: FluentTheme.of(context).typography.body!,
child: content,
),
);
},
);
}
Widget subtitle({required Widget content}) {
return Builder(
builder: (context) {
return Padding(
padding: const EdgeInsetsDirectional.only(top: 14, bottom: 2),
child: DefaultTextStyle(
style: FluentTheme.of(context).typography.subtitle!,
child: content,
),
);
},
);
}
}

View File

@@ -0,0 +1,99 @@
import 'package:fluent_ui/fluent_ui.dart';
class TypingIndicator extends StatefulWidget {
const TypingIndicator({super.key});
@override
State<TypingIndicator> createState() => _TypingIndicatorState();
}
class _TypingIndicatorState extends State<TypingIndicator>
with TickerProviderStateMixin {
late List<AnimationController> _controllers;
late List<Animation<double>> _animations;
@override
void initState() {
super.initState();
_controllers = List.generate(
3,
(index) => AnimationController(
duration: const Duration(milliseconds: 600),
vsync: this,
),
);
_animations = _controllers.map((controller) {
return Tween<double>(
begin: 0,
end: -8,
).animate(CurvedAnimation(parent: controller, curve: Curves.easeInOut));
}).toList();
for (int i = 0; i < _controllers.length; i++) {
Future.delayed(Duration(milliseconds: i * 150), () {
if (mounted) {
_controllers[i].repeat(reverse: true);
}
});
}
}
@override
void dispose() {
for (var controller in _controllers) {
controller.dispose();
}
super.dispose();
}
@override
Widget build(BuildContext context) {
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),
Card(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
child: Row(
mainAxisSize: MainAxisSize.min,
children: List.generate(3, (index) {
return AnimatedBuilder(
animation: _animations[index],
builder: (context, child) {
return Transform.translate(
offset: Offset(0, _animations[index].value),
child: child,
);
},
child: Container(
margin: EdgeInsets.only(right: index < 2 ? 6 : 0),
width: 8,
height: 8,
decoration: BoxDecoration(
color: theme.accentColor,
shape: BoxShape.circle,
),
),
);
}),
),
),
],
),
);
}
}