diff --git a/lib/bloc/chat_event.dart b/lib/bloc/chat_event.dart index cbf3b9ee..648e5944 100644 --- a/lib/bloc/chat_event.dart +++ b/lib/bloc/chat_event.dart @@ -11,8 +11,10 @@ class ChatMessageReceivedEvent extends ChatMessageEvent { class ChatMessageSendEvent extends ChatMessageEvent { final Message message; + final int? index; + final bool isResent; - ChatMessageSendEvent(this.message); + ChatMessageSendEvent(this.message, {this.index, this.isResent = false}); } class ChatMessageGetRecentEvent extends ChatMessageEvent { diff --git a/lib/bloc/chat_message_bloc.dart b/lib/bloc/chat_message_bloc.dart index 44e042b6..cdbc94ec 100644 --- a/lib/bloc/chat_message_bloc.dart +++ b/lib/bloc/chat_message_bloc.dart @@ -265,7 +265,22 @@ class ChatMessageBloc extends BlocExt { await chatMsgRepo.fixMessageStatus(roomId); // 记录当前消息 - final sentMessageId = await chatMsgRepo.sendMessage(roomId, message); + var sentMessageId = 0; + if (event.isResent && + event.index == 0 && + last != null && + last.type == MessageType.text) { + // 如果当前是消息重发,同时重发的是最后一条消息,则不会重新生成该消息,直接生成答案即可 + sentMessageId = last.id!; + if (last.statusIsFailed()) { + // 如果最后一条消息发送失败,则重新发送 + await chatMsgRepo.updateMessagePart(roomId, last.id!, [ + MessagePart('status', 0), + ]); + } + } else { + sentMessageId = await chatMsgRepo.sendMessage(roomId, message); + } // 更新 Room 最后活跃时间 // 这里没有使用 await,因为不需要等待更新完成,让 room 的更新异步的去处理吧 @@ -312,6 +327,15 @@ class ChatMessageBloc extends BlocExt { if (systemCmds.isNotEmpty) { for (var element in systemCmds) { try { + // SYSTEM 命令 + // - type: 命令类型 + // + // type=summary (默认值) + // - question_id: 问题 ID + // - answer_id: 答案 ID + // - quota_consumed: 消耗的配额 + // - token: 消耗的 token + // - info: 提示信息 final cmd = jsonDecode(element.content); message.serverId = cmd['question_id']; @@ -320,6 +344,11 @@ class ChatMessageBloc extends BlocExt { final quotaConsumed = cmd['quota_consumed'] ?? 0; final tokenConsumed = cmd['token'] ?? 0; + final info = cmd['info'] ?? ''; + if (info != '') { + waitMessage.setExtra({'info': info}); + } + if (quotaConsumed == 0 && tokenConsumed == 0) { continue; } diff --git a/lib/bloc/group_chat_event.dart b/lib/bloc/group_chat_event.dart index 00cb4f8f..2c786e73 100644 --- a/lib/bloc/group_chat_event.dart +++ b/lib/bloc/group_chat_event.dart @@ -26,8 +26,11 @@ class GroupChatSendEvent extends GroupChatEvent { final int groupId; final String message; final List members; + final int? index; + final bool isResent; - GroupChatSendEvent(this.groupId, this.message, this.members); + GroupChatSendEvent(this.groupId, this.message, this.members, + {this.index, this.isResent = false}); } class GroupChatUpdateMessageStatusEvent extends GroupChatEvent { diff --git a/lib/helper/constant.dart b/lib/helper/constant.dart index c564ec3a..13af102d 100644 --- a/lib/helper/constant.dart +++ b/lib/helper/constant.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; // 客户端应用版本号 -const clientVersion = '1.0.7'; +const clientVersion = '1.0.8'; // 本地数据库版本号 const databaseVersion = 25; diff --git a/lib/helper/error.dart b/lib/helper/error.dart index 12b301dd..30d77ca2 100644 --- a/lib/helper/error.dart +++ b/lib/helper/error.dart @@ -5,7 +5,8 @@ import 'package:dart_openai/openai.dart'; Object resolveErrorMessage(dynamic e, {bool isChat = false}) { // TODO if (e is RequestFailedException) { - final msg = resolveHTTPStatusCode(e.statusCode, isChat: isChat); + final msg = + resolveHTTPStatusCode(e.statusCode, isChat: isChat, message: e.message); if (msg != null) { return msg; } @@ -16,7 +17,8 @@ Object resolveErrorMessage(dynamic e, {bool isChat = false}) { return e.toString(); } -Object? resolveHTTPStatusCode(int statusCode, {bool isChat = false}) { +Object? resolveHTTPStatusCode(int statusCode, + {bool isChat = false, String? message}) { switch (statusCode) { case 400: return const LanguageText('请求参数错误'); @@ -46,6 +48,10 @@ Object? resolveHTTPStatusCode(int statusCode, {bool isChat = false}) { case 402: return const LanguageText(AppLocale.quotaExceeded, action: 'payment'); case 500: + if (message != null && message.isNotEmpty) { + return message; + } + return const LanguageText(AppLocale.internalServerError); case 502: return const LanguageText(AppLocale.badGateway); diff --git a/lib/page/chat/component/group_avatar.dart b/lib/page/chat/component/group_avatar.dart index b985f107..1e356c4b 100644 --- a/lib/page/chat/component/group_avatar.dart +++ b/lib/page/chat/component/group_avatar.dart @@ -2,6 +2,7 @@ import 'package:askaide/helper/image.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; +// ignore: must_be_immutable class GroupAvatar extends StatelessWidget { final double size; final double padding; diff --git a/lib/page/chat/group/chat.dart b/lib/page/chat/group/chat.dart index c1677e63..7001ff15 100644 --- a/lib/page/chat/group/chat.dart +++ b/lib/page/chat/group/chat.dart @@ -411,16 +411,15 @@ class _GroupChatPageState extends State { onDeleteMessage: (id) { handleDeleteMessage(context, id); }, + onResetContext: () => handleResetContext(context), onSpeakEvent: (message) { _audioPlayerController.playAudio(message.text); }, - onResentEvent: (message) { + onResentEvent: (message, index) { _scrollController.animateTo(0, duration: const Duration(milliseconds: 500), curve: Curves.easeOut); - _handleSubmit( - message.text, - ); + _handleSubmit(message.text, index: index, isResent: true); }, helpWidgets: state.hasWaitTasks || loadedMessages.isEmpty ? null @@ -505,15 +504,23 @@ class _GroupChatPageState extends State { } /// 提交新消息 - void _handleSubmit(String text) { + void _handleSubmit( + String text, { + int? index, + bool isResent = false, + }) { setState(() { _inputEnabled.value = false; }); var replyMemberIds = (selectedMembers ?? []).map((e) => e.id!).toList(); - context - .read() - .add(GroupChatSendEvent(widget.groupId, text, replyMemberIds)); + context.read().add(GroupChatSendEvent( + widget.groupId, + text, + replyMemberIds, + index: index, + isResent: isResent, + )); } /// 处理消息删除事件 diff --git a/lib/page/chat/home_chat.dart b/lib/page/chat/home_chat.dart index b42d0e5a..3e1662db 100644 --- a/lib/page/chat/home_chat.dart +++ b/lib/page/chat/home_chat.dart @@ -411,11 +411,13 @@ class _HomeChatPageState extends State { onDeleteMessage: (id) { handleDeleteMessage(context, id, chatHistoryId: chatId); }, - onResentEvent: (message) { + onResetContext: () => handleResetContext(context), + onResentEvent: (message, index) { _scrollController.animateTo(0, duration: const Duration(milliseconds: 500), curve: Curves.easeOut); - _handleSubmit(message.text, messagetType: message.type); + _handleSubmit(message.text, + messagetType: message.type, index: index, isResent: true); }, onSpeakEvent: (message) { _audioPlayerController.playAudio(message.text); @@ -427,7 +429,12 @@ class _HomeChatPageState extends State { } /// 提交新消息 - void _handleSubmit(String text, {messagetType = MessageType.text}) { + void _handleSubmit( + String text, { + messagetType = MessageType.text, + int? index, + bool isResent = false, + }) { setState(() { _inputEnabled.value = false; }); @@ -443,6 +450,8 @@ class _HomeChatPageState extends State { type: messagetType, chatHistoryId: chatId, ), + index: index, + isResent: isResent, ), ); diff --git a/lib/page/chat/room_chat.dart b/lib/page/chat/room_chat.dart index 2df998ee..749de615 100644 --- a/lib/page/chat/room_chat.dart +++ b/lib/page/chat/room_chat.dart @@ -269,14 +269,16 @@ class _RoomChatPageState extends State { onDeleteMessage: (id) { handleDeleteMessage(context, id); }, + onResetContext: () => handleResetContext(context), onSpeakEvent: (message) { _audioPlayerController.playAudio(message.text); }, - onResentEvent: (message) { + onResentEvent: (message, index) { _scrollController.animateTo(0, duration: const Duration(milliseconds: 500), curve: Curves.easeOut); - _handleSubmit(message.text, messagetType: message.type); + _handleSubmit(message.text, + messagetType: message.type, index: index, isResent: true); }, helpWidgets: state.processing || loadedMessages.last.isInitMessage() ? null @@ -390,7 +392,12 @@ class _RoomChatPageState extends State { } /// 提交新消息 - void _handleSubmit(String text, {messagetType = MessageType.text}) { + void _handleSubmit( + String text, { + messagetType = MessageType.text, + int? index, + bool isResent = false, + }) { setState(() { _inputEnabled.value = false; }); @@ -404,6 +411,8 @@ class _RoomChatPageState extends State { ts: DateTime.now(), type: messagetType, ), + index: index, + isResent: isResent, ), ); diff --git a/lib/page/component/chat/chat_preview.dart b/lib/page/component/chat/chat_preview.dart index 6b4dca4d..a4c53bb5 100644 --- a/lib/page/component/chat/chat_preview.dart +++ b/lib/page/component/chat/chat_preview.dart @@ -21,12 +21,16 @@ import 'package:clipboard/clipboard.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_localization/flutter_localization.dart'; +import 'package:go_router/go_router.dart'; import 'package:loading_animation_widget/loading_animation_widget.dart'; +import 'package:quickalert/models/quickalert_type.dart'; +import 'package:url_launcher/url_launcher_string.dart'; class ChatPreview extends StatefulWidget { final List messages; final ScrollController? scrollController; final void Function(int id)? onDeleteMessage; + final void Function()? onResetContext; final ChatPreviewController controller; final MessageStateManager stateManager; final List? helpWidgets; @@ -35,13 +39,14 @@ class ChatPreview extends StatefulWidget { final Widget? Function(Message message)? senderNameBuilder; final bool supportBloc; final void Function(Message message)? onSpeakEvent; - final void Function(Message message)? onResentEvent; + final void Function(Message message, int index)? onResentEvent; const ChatPreview({ super.key, required this.messages, this.scrollController, this.onDeleteMessage, + this.onResetContext, required this.controller, required this.stateManager, this.robotAvatar, @@ -127,6 +132,7 @@ class _ChatPreviewState extends State { customColors, _resolveMessage(state, message), message.state, + index, ), ); }, @@ -141,6 +147,7 @@ class _ChatPreviewState extends State { customColors, message.message, message.state, + index, ), ), ), @@ -171,6 +178,7 @@ class _ChatPreviewState extends State { CustomColors customColors, Message message, MessageState state, + int index, ) { // 系统消息 if (message.isSystem()) { @@ -213,6 +221,10 @@ class _ChatPreviewState extends State { final showTranslate = state.showTranslate && state.translateText != null && state.translateText != ''; + + final extra = index == 0 ? message.decodeExtra() : null; + final extraInfo = extra != null ? extra['info'] ?? '' : ''; + // 普通消息 return Align( alignment: @@ -244,7 +256,7 @@ class _ChatPreviewState extends State { // 错误指示器 if (message.role == Role.sender && message.statusIsFailed()) - buildErrorIndicator(message, state, context), + buildErrorIndicator(message, state, context, index), // 消息主体 GestureDetector( // 选择模式下,单击切换选择与否 @@ -263,6 +275,7 @@ class _ChatPreviewState extends State { detail.globalPosition, message, state, + index, ); }, onDoubleTapDown: (details) { @@ -271,69 +284,118 @@ class _ChatPreviewState extends State { details.globalPosition, message, state, + index, ); }, - child: Container( - margin: message.role == Role.sender - ? const EdgeInsets.fromLTRB(0, 0, 10, 7) - : const EdgeInsets.fromLTRB(10, 0, 0, 7), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10), - color: message.role == Role.receiver - ? customColors.chatRoomReplyBackground - : customColors.chatRoomSenderBackground, - ), - padding: const EdgeInsets.symmetric( - horizontal: 13, - vertical: 13, - ), - child: Builder( - builder: (context) { - if ((message.statusPending() || - !message.isReady) && - message.text.isEmpty) { - return LoadingAnimationWidget.waveDots( - color: customColors.weakLinkColor!, - size: 25, - ); - } - - var text = message.text; - if (!message.isReady && text != '') { - text += ' ▌'; - } - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - state.showMarkdown - ? Markdown(data: text) - : SelectableText( - text, - style: TextStyle( - color: - customColors.chatRoomSenderText, - ), + child: Stack( + children: [ + Container( + margin: message.role == Role.sender + ? const EdgeInsets.fromLTRB(0, 0, 10, 7) + : const EdgeInsets.fromLTRB(10, 0, 0, 7), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + color: message.role == Role.receiver + ? customColors.chatRoomReplyBackground + : customColors.chatRoomSenderBackground, + ), + padding: const EdgeInsets.symmetric( + horizontal: 13, + vertical: 13, + ), + child: Builder( + builder: (context) { + if ((message.statusPending() || + !message.isReady) && + message.text.isEmpty) { + return LoadingAnimationWidget.waveDots( + color: customColors.weakLinkColor!, + size: 25, + ); + } + + var text = message.text; + if (!message.isReady && text != '') { + text += ' ▌'; + } + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + state.showMarkdown + ? Markdown( + data: text.trim(), + onUrlTap: (value) => + launchUrlString(value), + ) + : SelectableText( + text, + style: TextStyle( + color: customColors + .chatRoomSenderText, + ), + ), + if (message.quotaConsumed != null && + message.quotaConsumed! > 0) + Row( + children: [ + const Icon(Icons.check_circle, + size: 12, color: Colors.green), + const SizedBox(width: 5), + Expanded( + child: Text( + '共 ${message.tokenConsumed} 个 Token, 消耗 ${message.quotaConsumed} 个智慧果', + style: TextStyle( + fontSize: 14, + color: customColors + .weakTextColor, + ), + ), + ), + ], ), - if (message.quotaConsumed != null && - message.quotaConsumed! > 0) - Row( - children: [ - const Icon(Icons.check_circle, - size: 12, color: Colors.green), - const SizedBox(width: 5), - Text( - '共 ${message.tokenConsumed} 个 Token, 消耗 ${message.quotaConsumed} 个智慧果', - style: TextStyle( - fontSize: 14, - color: customColors.weakTextColor, - ), + ], + ); + }, + ), + ), + if (extraInfo.isNotEmpty) + Positioned( + top: 5, + right: 5, + child: InkWell( + onTap: () { + showCustomBeautyDialog( + context, + type: QuickAlertType.warning, + confirmBtnText: + AppLocale.gotIt.getString(context), + showCancelBtn: false, + title: '温馨提示', + child: Markdown( + data: extraInfo, + onUrlTap: (value) { + onMarkdownUrlTap(value); + context.pop(); + }, + textStyle: TextStyle( + fontSize: 14, + color: customColors + .dialogDefaultTextColor, ), - ], - ) - ], - ); - }, - ), + ), + ); + }, + child: Icon( + Icons.info_outline, + size: 16, + color: customColors.weakLinkColor + ?.withAlpha(50), + ), + ), + ), + ], ), ), ], @@ -400,6 +462,7 @@ class _ChatPreviewState extends State { Message message, MessageState state, BuildContext context, + int index, ) { return Container( margin: const EdgeInsets.only(right: 5, bottom: 10), @@ -418,7 +481,9 @@ class _ChatPreviewState extends State { if (extra['error'] != null && extra['error'] != '') { var e1 = extra['error']; try { - e1 = (e1 as String).getString(context); + if (e1 is LanguageText) { + e1 = (e1 as String).getString(context); + } // ignore: empty_catches } catch (ignored) {} confirmMessage = e1; @@ -431,7 +496,7 @@ class _ChatPreviewState extends State { context, confirmMessage, () { - widget.onResentEvent!(message); + widget.onResentEvent!(message, index); }, title: Text(AppLocale.robotHasSomeError.getString(context)), confirmText: '重新发送', @@ -442,6 +507,24 @@ class _ChatPreviewState extends State { ); } + void onMarkdownUrlTap(value) { + if (value.startsWith("aidea-app://")) { + var route = value.substring('aidea-app://'.length); + context.push(route); + } else if (value.startsWith("aidea-command://")) { + var command = value.substring('aidea-command://'.length); + switch (command) { + case "reset-context": + if (widget.onResetContext != null) { + widget.onResetContext!(); + } + break; + } + } else { + launchUrlString(value); + } + } + Widget buildAvatar(Message message) { if (widget.avatarBuilder != null) { final avatar = widget.avatarBuilder!(message); @@ -463,6 +546,7 @@ class _ChatPreviewState extends State { Offset? offset, Message message, MessageState state, + int index, ) { if (widget.controller.selectMode || message.isSystem()) { return; @@ -739,7 +823,7 @@ class _ChatPreviewState extends State { if (message.role == Role.sender && widget.onResentEvent != null) TextButton.icon( onPressed: () { - widget.onResentEvent!(message); + widget.onResentEvent!(message, index); cancel(); }, label: const Text(''), diff --git a/lib/page/component/chat/chat_share.dart b/lib/page/component/chat/chat_share.dart index cf8d3276..de6bdb16 100644 --- a/lib/page/component/chat/chat_share.dart +++ b/lib/page/component/chat/chat_share.dart @@ -409,8 +409,8 @@ class _ChatShareScreenState extends State { /// 获取聊天框的最大宽度 double _chatBoxMaxWidth(BuildContext context) { var screenWidth = MediaQuery.of(context).size.width; - if (screenWidth >= CustomSize.maxWindowSize) { - return CustomSize.maxWindowSize; + if (screenWidth >= CustomSize.smallWindowSize) { + return CustomSize.smallWindowSize; } return screenWidth; diff --git a/lib/page/component/chat/markdown.dart b/lib/page/component/chat/markdown.dart index 859e3848..edcf050f 100644 --- a/lib/page/component/chat/markdown.dart +++ b/lib/page/component/chat/markdown.dart @@ -1,9 +1,14 @@ +import 'package:askaide/helper/platform.dart'; +import 'package:askaide/page/component/chat/markdown/latex.dart'; import 'package:askaide/page/component/image_preview.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:flutter/material.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart'; +import 'package:flutter_highlight/themes/monokai.dart'; import 'package:flutter_markdown/flutter_markdown.dart' as md; import 'package:markdown/markdown.dart'; +import 'package:markdown_widget/config/all.dart'; +import 'package:markdown_widget/widget/all.dart'; class Markdown extends StatelessWidget { final String data; @@ -20,6 +25,15 @@ class Markdown extends StatelessWidget { @override Widget build(BuildContext context) { + if (!PlatformTool.isWeb()) { + return MarkdownPlus( + data: data, + onUrlTap: onUrlTap, + textStyle: textStyle, + compact: true, + ); + } + final customColors = Theme.of(context).extension()!; return md.MarkdownBody( shrinkWrap: true, @@ -73,3 +87,93 @@ class Markdown extends StatelessWidget { ); } } + +class MarkdownPlus extends StatelessWidget { + final String data; + final Function(String value)? onUrlTap; + final bool compact; + final TextStyle? textStyle; + final cacheManager = DefaultCacheManager(); + + MarkdownPlus({ + super.key, + required this.data, + this.onUrlTap, + this.compact = true, + this.textStyle, + }); + + MarkdownConfig _buildMarkdownConfig(CustomColors customColors) { + return MarkdownConfig( + configs: [ + PConfig(textStyle: textStyle ?? const TextStyle(fontSize: 16)), + // 链接配置 + LinkConfig( + style: TextStyle( + color: customColors.markdownLinkColor, + decoration: TextDecoration.none, + ), + onTap: (value) { + if (onUrlTap != null) onUrlTap!(value); + }, + ), + // 代码块配置 + PreConfig( + theme: monokaiTheme, + decoration: BoxDecoration( + color: customColors.markdownPreColor, + borderRadius: BorderRadius.circular(5), + ), + textStyle: const TextStyle(fontSize: 14), + ), + // 代码配置 + CodeConfig( + style: TextStyle( + fontSize: 14, + color: customColors.markdownCodeColor, + ), + ), + // 图片配置 + ImgConfig( + builder: (url, attributes) { + if (url.isEmpty) { + return const SizedBox(); + } + + return NetworkImagePreviewer( + url: url, + hidePreviewButton: true, + ); + }, + ) + ], + ); + } + + @override + Widget build(BuildContext context) { + final customColors = Theme.of(context).extension()!; + if (compact) { + final markdownGenerator = MarkdownGenerator( + generators: [latexGenerator], + inlineSyntaxList: [LatexSyntax()], + ); + return Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.start, + textDirection: TextDirection.ltr, + crossAxisAlignment: CrossAxisAlignment.start, + children: markdownGenerator.buildWidgets( + data, + config: _buildMarkdownConfig(customColors), + ), + ); + } + + return MarkdownWidget( + data: data, + shrinkWrap: true, + config: _buildMarkdownConfig(customColors), + ); + } +} diff --git a/lib/page/component/chat/markdown/latex.dart b/lib/page/component/chat/markdown/latex.dart new file mode 100644 index 00000000..515e1fbb --- /dev/null +++ b/lib/page/component/chat/markdown/latex.dart @@ -0,0 +1,76 @@ +import 'package:flutter/material.dart'; +import 'package:markdown_widget/markdown_widget.dart'; +import 'package:flutter_math_fork/flutter_math.dart'; +import 'package:markdown/markdown.dart' as m; + +SpanNodeGeneratorWithTag latexGenerator = SpanNodeGeneratorWithTag( + tag: _latexTag, + generator: (e, config, visitor) => + LatexNode(e.attributes, e.textContent, config)); + +const _latexTag = 'latex'; + +class LatexSyntax extends m.InlineSyntax { + LatexSyntax() : super(r'(\$\$[\s\S]+\$\$)|(\$.+?\$)'); + + @override + bool onMatch(m.InlineParser parser, Match match) { + final input = match.input; + final matchValue = input.substring(match.start, match.end); + String content = ''; + bool isInline = true; + const blockSyntax = '\$\$'; + const inlineSyntax = '\$'; + if (matchValue.startsWith(blockSyntax) && + matchValue.endsWith(blockSyntax) && + (matchValue != blockSyntax)) { + content = matchValue.substring(2, matchValue.length - 2); + isInline = false; + } else if (matchValue.startsWith(inlineSyntax) && + matchValue.endsWith(inlineSyntax) && + matchValue != inlineSyntax) { + content = matchValue.substring(1, matchValue.length - 1); + } + m.Element el = m.Element.text(_latexTag, matchValue); + el.attributes['content'] = content; + el.attributes['isInline'] = '$isInline'; + parser.addNode(el); + return true; + } +} + +class LatexNode extends SpanNode { + final Map attributes; + final String textContent; + final MarkdownConfig config; + + LatexNode(this.attributes, this.textContent, this.config); + + @override + InlineSpan build() { + final content = attributes['content'] ?? ''; + final isInline = attributes['isInline'] == 'true'; + final style = parentStyle ?? config.p.textStyle; + if (content.isEmpty) return TextSpan(style: style, text: textContent); + final latex = Math.tex( + content, + mathStyle: MathStyle.text, + textScaleFactor: 1, + onErrorFallback: (error) { + return Text( + textContent, + style: style.copyWith(color: Colors.red), + ); + }, + ); + return WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: !isInline + ? Container( + width: double.infinity, + margin: const EdgeInsets.symmetric(vertical: 16), + child: Center(child: latex), + ) + : latex); + } +} diff --git a/lib/page/component/dialog.dart b/lib/page/component/dialog.dart index 08ccb4cd..83184e07 100644 --- a/lib/page/component/dialog.dart +++ b/lib/page/component/dialog.dart @@ -63,6 +63,48 @@ showErrorMessageEnhanced( showErrorMessage(message.toString(), duration: duration); } +showCustomBeautyDialog( + BuildContext context, { + required QuickAlertType type, + required Widget child, + String confirmBtnText = '确定', + String? cancelBtnText, + Function()? onConfirmBtnTap, + Function()? onCancelBtnTap, + bool showCancelBtn = false, + String title = '', +}) { + final customColors = Theme.of(context).extension()!; + + QuickAlert.show( + context: context, + type: type, + widget: child, + width: MediaQuery.of(context).size.width > 600 ? 400 : null, + barrierDismissible: false, // 禁止点击外部关闭 + showCancelBtn: showCancelBtn, + confirmBtnText: confirmBtnText, + cancelBtnText: cancelBtnText ?? AppLocale.cancel.getString(context), + confirmBtnColor: customColors.linkColor!, + borderRadius: 10, + buttonBorderRadius: 10, + backgroundColor: customColors.dialogBackgroundColor!, + confirmBtnTextStyle: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.normal, + ), + title: title, + titleColor: customColors.dialogDefaultTextColor!, + textColor: customColors.dialogDefaultTextColor!, + cancelBtnTextStyle: TextStyle( + color: customColors.dialogDefaultTextColor, + fontWeight: FontWeight.normal, + ), + onConfirmBtnTap: onConfirmBtnTap, + onCancelBtnTap: onCancelBtnTap, + ); +} + showBeautyDialog( BuildContext context, { required QuickAlertType type, diff --git a/lib/page/setting/setting_screen.dart b/lib/page/setting/setting_screen.dart index 524695bd..d5d1b5db 100644 --- a/lib/page/setting/setting_screen.dart +++ b/lib/page/setting/setting_screen.dart @@ -392,7 +392,8 @@ class _SettingScreenState extends State { Future>> _defaultServerList() async { return [ - SelectorItem(const Text('默认服务器'), apiServerURL), + SelectorItem(const Text('官方正式服务器'), apiServerURL), + SelectorItem(const Text('本地开发机'), 'http://localhost:8080'), ]; } diff --git a/lib/repo/openai_repo.dart b/lib/repo/openai_repo.dart index 169a09cb..08e86f29 100644 --- a/lib/repo/openai_repo.dart +++ b/lib/repo/openai_repo.dart @@ -332,7 +332,8 @@ class OpenAIRepository { 'temperature': temperature, 'user': user, 'max_tokens': maxTokens, - 'n': Ability().enableLocalOpenAI() + 'n': Ability().enableLocalOpenAI() && + (model.startsWith('openai:') || model.startsWith('gpt-')) ? null : roomId, // n 参数暂时用不到,复用作为 roomId })); diff --git a/pubspec.lock b/pubspec.lock index bf76dd26..4994947c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -654,6 +654,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.6.18" + flutter_math_fork: + dependency: "direct main" + description: + name: flutter_math_fork + sha256: a143a3a89131b578043ecbdb5e759c1033a1b3e9174f5cd1b979d93f4a7fb41c + url: "https://pub.dev" + source: hosted + version: "0.7.1" flutter_native_splash: dependency: "direct main" description: @@ -961,6 +969,14 @@ packages: url: "https://pub.dev" source: hosted version: "7.1.1" + markdown_widget: + dependency: "direct main" + description: + name: markdown_widget + sha256: b69334a1dd633c32d688735d771ebaf5490f713cd00917cb52b53c14c2c09d81 + url: "https://pub.dev" + source: hosted + version: "2.3.1" matcher: dependency: transitive description: @@ -1266,6 +1282,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.27.7" + scroll_to_index: + dependency: transitive + description: + name: scroll_to_index + sha256: b707546e7500d9f070d63e5acf74fd437ec7eeeb68d3412ef7b0afada0b4f176 + url: "https://pub.dev" + source: hosted + version: "3.0.1" settings_ui: dependency: "direct main" description: @@ -1615,6 +1639,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.4.2" + tuple: + dependency: transitive + description: + name: tuple + sha256: a97ce2013f240b2f3807bcbaf218765b6f301c3eff91092bcfa23a039e7dd151 + url: "https://pub.dev" + source: hosted + version: "2.0.2" typed_data: dependency: transitive description: @@ -1743,6 +1775,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + visibility_detector: + dependency: transitive + description: + name: visibility_detector + sha256: dd5cc11e13494f432d15939c3aa8ae76844c42b723398643ce9addb88a5ed420 + url: "https://pub.dev" + source: hosted + version: "0.4.0+2" vm_service: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 66ebaa4d..4f36e24f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -17,7 +17,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. # 应用正式发布时,需要同步修改 lib/helper/constant.dart 中的 VERSION 值 -version: 1.0.7+1 +version: 1.0.8+1 environment: sdk: '>=3.0.0 <4.0.0' @@ -116,6 +116,8 @@ dependencies: fetch_api: 1.0.1 web_socket_channel: ^2.4.0 flutter_initicon: ^3.0.0+1 + markdown_widget: ^2.3.1 + flutter_math_fork: ^0.7.1 dev_dependencies: flutter_test: