diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/animated_chat_list.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/animated_chat_list.dart index e9d7b940b3..29c6feb9d3 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/animated_chat_list.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/animated_chat_list.dart @@ -312,7 +312,7 @@ class ChatAnimatedListReversedState extends State // bottom of the list, set `_userHasScrolled` to false so that the scroll // animation is triggered. if (_userHasScrolled && - widget.scrollController.offset >= + widget.scrollController.offset > widget.scrollController.position.minScrollExtent) { _userHasScrolled = false; } @@ -325,7 +325,7 @@ class ChatAnimatedListReversedState extends State // Used later to trigger scroll to end only for the last inserted message. _lastInsertedMessageId = data.id; - if (position == _oldList.length) { + if (position == 0) { _scrollToEnd(data); } } diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_input/desktop_ai_prompt_input.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_input/desktop_ai_prompt_input.dart index 9e97b85aec..86342869c4 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_input/desktop_ai_prompt_input.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_input/desktop_ai_prompt_input.dart @@ -49,8 +49,11 @@ class _DesktopAIPromptInputState extends State { final focusNode = FocusNode(); final textController = TextEditingController(); - bool showPredefinedFormatSection = false; - PredefinedFormat predefinedFormat = const PredefinedFormat.auto(); + bool showPredefinedFormatSection = true; + PredefinedFormat predefinedFormat = const PredefinedFormat( + imageFormat: ImageFormat.text, + textFormat: TextFormat.bulletList, + ); late SendButtonState sendButtonState; @override @@ -185,10 +188,6 @@ class _DesktopAIPromptInputState extends State { setState(() { showPredefinedFormatSection = !showPredefinedFormatSection; - if (!showPredefinedFormatSection) { - predefinedFormat = - const PredefinedFormat.auto(); - } }); }, sendButtonState: sendButtonState, @@ -466,10 +465,7 @@ class _PromptTextFieldState extends State<_PromptTextField> { focusedBorder: InputBorder.none, contentPadding: calculateContentPadding(), hintText: widget.hintText, - hintStyle: Theme.of(context) - .textTheme - .bodyMedium - ?.copyWith(color: Theme.of(context).hintColor), + hintStyle: AIChatUILayout.inputHintTextStyle(context), isCollapsed: true, isDense: true, ), diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_input/mobile_ai_prompt_input.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_input/mobile_ai_prompt_input.dart index 225aa8e5f5..bba56b7b47 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_input/mobile_ai_prompt_input.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_input/mobile_ai_prompt_input.dart @@ -43,8 +43,11 @@ class _MobileAIPromptInputState extends State { final focusNode = FocusNode(); final textController = TextEditingController(); - bool showPredefinedFormatSection = false; - PredefinedFormat predefinedFormat = const PredefinedFormat.auto(); + bool showPredefinedFormatSection = true; + PredefinedFormat predefinedFormat = const PredefinedFormat( + imageFormat: ImageFormat.text, + textFormat: TextFormat.bulletList, + ); late SendButtonState sendButtonState; @override @@ -265,6 +268,7 @@ class _MobileAIPromptInputState extends State { AIType.appflowyAI => LocaleKeys.chat_inputMessageHint.tr(), AIType.localAI => LocaleKeys.chat_inputLocalAIMessageHint.tr() }, + hintStyle: AIChatUILayout.inputHintTextStyle(context), isCollapsed: true, isDense: true, ), @@ -304,9 +308,6 @@ class _MobileAIPromptInputState extends State { onTogglePredefinedFormatSection: () { setState(() { showPredefinedFormatSection = !showPredefinedFormatSection; - if (!showPredefinedFormatSection) { - predefinedFormat = const PredefinedFormat.auto(); - } }); }, onUpdateSelectedSources: widget.onUpdateSelectedSources, diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_input/predefined_format_buttons.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_input/predefined_format_buttons.dart index dfd563e039..08aa0e84ee 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_input/predefined_format_buttons.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_input/predefined_format_buttons.dart @@ -24,50 +24,25 @@ class PromptInputDesktopToggleFormatButton extends StatelessWidget { @override Widget build(BuildContext context) { - return GestureDetector( - onTap: onTap, - behavior: HitTestBehavior.opaque, - child: SizedBox( - height: DesktopAIPromptSizes.actionBarButtonSize, - child: FlowyHover( - style: const HoverStyle( - borderRadius: BorderRadius.all(Radius.circular(8.0)), - ), - child: Padding( - padding: const EdgeInsetsDirectional.all(6.0), - child: FlowyText( - _getDescription(), - fontSize: 12.0, - figmaLineHeight: 16.0, + return FlowyIconButton( + tooltipText: showFormatBar + ? LocaleKeys.chat_changeFormat_defaultDescription.tr() + : LocaleKeys.chat_changeFormat_blankDescription.tr(), + width: 28.0, + onPressed: onTap, + icon: showFormatBar + ? const FlowySvg( + FlowySvgs.m_aa_text_s, + size: Size.square(16.0), + color: Color(0xFF666D76), + ) + : const FlowySvg( + FlowySvgs.ai_text_image_s, + size: Size(21.0, 16.0), + color: Color(0xFF666D76), ), - ), - ), - ), ); } - - String _getDescription() { - if (!showFormatBar) { - return LocaleKeys.chat_changeFormat_blankDescription.tr(); - } - - return switch ((predefinedFormat, predefinedTextFormat)) { - (ImageFormat.image, _) => predefinedFormat.i18n, - (ImageFormat.text, TextFormat.auto) => - LocaleKeys.chat_changeFormat_defaultDescription.tr(), - (ImageFormat.text, _) when predefinedTextFormat != null => - predefinedTextFormat!.i18n, - (ImageFormat.textAndImage, TextFormat.auto) => - LocaleKeys.chat_changeFormat_textWithImageDescription.tr(), - (ImageFormat.textAndImage, TextFormat.bulletList) => - LocaleKeys.chat_changeFormat_bulletWithImageDescription.tr(), - (ImageFormat.textAndImage, TextFormat.numberedList) => - LocaleKeys.chat_changeFormat_numberWithImageDescription.tr(), - (ImageFormat.textAndImage, TextFormat.table) => - LocaleKeys.chat_changeFormat_tableWithImageDescription.tr(), - _ => throw UnimplementedError(), - }; - } } class ChangeFormatBar extends StatelessWidget { @@ -80,7 +55,7 @@ class ChangeFormatBar extends StatelessWidget { required this.onSelectPredefinedFormat, }); - final PredefinedFormat predefinedFormat; + final PredefinedFormat? predefinedFormat; final double buttonSize; final double iconSize; final double spacing; @@ -97,7 +72,7 @@ class ChangeFormatBar extends StatelessWidget { _buildFormatButton(context, ImageFormat.text), _buildFormatButton(context, ImageFormat.textAndImage), _buildFormatButton(context, ImageFormat.image), - if (predefinedFormat.imageFormat.hasText) ...[ + if (predefinedFormat?.imageFormat.hasText ?? true) ...[ _buildDivider(), _buildTextFormatButton(context, TextFormat.auto), _buildTextFormatButton(context, TextFormat.bulletList), @@ -113,11 +88,12 @@ class ChangeFormatBar extends StatelessWidget { return GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { - if (format == predefinedFormat.imageFormat) { + if (predefinedFormat != null && + format == predefinedFormat!.imageFormat) { return; } if (format.hasText) { - final textFormat = predefinedFormat.textFormat ?? TextFormat.auto; + final textFormat = predefinedFormat?.textFormat ?? TextFormat.auto; onSelectPredefinedFormat( PredefinedFormat(imageFormat: format, textFormat: textFormat), ); @@ -132,7 +108,7 @@ class ChangeFormatBar extends StatelessWidget { child: SizedBox.square( dimension: buttonSize, child: FlowyHover( - isSelected: () => format == predefinedFormat.imageFormat, + isSelected: () => format == predefinedFormat?.imageFormat, child: Center( child: FlowySvg( format.icon, @@ -162,12 +138,13 @@ class ChangeFormatBar extends StatelessWidget { return GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { - if (format == predefinedFormat.textFormat) { + if (predefinedFormat != null && + format == predefinedFormat!.textFormat) { return; } onSelectPredefinedFormat( PredefinedFormat( - imageFormat: predefinedFormat.imageFormat, + imageFormat: predefinedFormat?.imageFormat ?? ImageFormat.text, textFormat: format, ), ); @@ -177,7 +154,7 @@ class ChangeFormatBar extends StatelessWidget { child: SizedBox.square( dimension: buttonSize, child: FlowyHover( - isSelected: () => format == predefinedFormat.textFormat, + isSelected: () => format == predefinedFormat?.textFormat, child: Center( child: FlowySvg( format.icon, @@ -211,8 +188,8 @@ class PromptInputMobileToggleFormatButton extends StatelessWidget { expandText: false, text: showFormatBar ? const FlowySvg( - FlowySvgs.ai_text_auto_s, - size: Size.square(24.0), + FlowySvgs.m_aa_text_s, + size: Size.square(20.0), ) : const FlowySvg( FlowySvgs.ai_text_image_s, diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/layout_define.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/layout_define.dart index ad39830313..fdc2784ca1 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/layout_define.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/layout_define.dart @@ -1,3 +1,4 @@ +import 'package:appflowy/util/theme_extension.dart'; import 'package:flutter/material.dart'; import 'package:universal_platform/universal_platform.dart'; @@ -17,6 +18,14 @@ class AIChatUILayout { static EdgeInsets get messageMargin => UniversalPlatform.isMobile ? const EdgeInsets.symmetric(horizontal: 16) : EdgeInsets.zero; + + static TextStyle? inputHintTextStyle(BuildContext context) { + return Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).isLightMode + ? const Color(0xFFBDC2C8) + : const Color(0xFF3C3E51), + ); + } } class DesktopAIPromptSizes { diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_change_format_bottom_sheet.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_change_format_bottom_sheet.dart index 5faebed704..7edc5f7d5e 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_change_format_bottom_sheet.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_change_format_bottom_sheet.dart @@ -28,7 +28,7 @@ class _ChangeFormatBottomSheetContent extends StatefulWidget { class _ChangeFormatBottomSheetContentState extends State<_ChangeFormatBottomSheetContent> { - PredefinedFormat predefinedFormat = const PredefinedFormat.auto(); + PredefinedFormat? predefinedFormat; @override Widget build(BuildContext context) { @@ -106,7 +106,7 @@ class _Body extends StatelessWidget { required this.onSelectPredefinedFormat, }); - final PredefinedFormat predefinedFormat; + final PredefinedFormat? predefinedFormat; final void Function(PredefinedFormat) onSelectPredefinedFormat; @override @@ -119,7 +119,7 @@ class _Body extends StatelessWidget { _buildFormatButton(ImageFormat.image), const VSpace(32.0), Opacity( - opacity: predefinedFormat.imageFormat.hasText ? 1 : 0, + opacity: predefinedFormat?.imageFormat.hasText ?? true ? 1 : 0, child: Column( children: [ _buildTextFormatButton(TextFormat.auto, true), @@ -139,7 +139,7 @@ class _Body extends StatelessWidget { ]) { return FlowyOptionTile.checkbox( text: format.i18n, - isSelected: format == predefinedFormat.imageFormat, + isSelected: format == predefinedFormat?.imageFormat, showTopBorder: isFirst, leftIcon: FlowySvg( format.icon, @@ -148,11 +148,12 @@ class _Body extends StatelessWidget { : const Size.square(20), ), onTap: () { - if (format == predefinedFormat.imageFormat) { + if (predefinedFormat != null && + format == predefinedFormat!.imageFormat) { return; } if (format.hasText) { - final textFormat = predefinedFormat.textFormat ?? TextFormat.auto; + final textFormat = predefinedFormat?.textFormat ?? TextFormat.auto; onSelectPredefinedFormat( PredefinedFormat(imageFormat: format, textFormat: textFormat), ); @@ -171,19 +172,20 @@ class _Body extends StatelessWidget { ]) { return FlowyOptionTile.checkbox( text: format.i18n, - isSelected: format == predefinedFormat.textFormat, + isSelected: format == predefinedFormat?.textFormat, showTopBorder: isFirst, leftIcon: FlowySvg( format.icon, size: const Size.square(20), ), onTap: () { - if (format == predefinedFormat.textFormat) { + if (predefinedFormat != null && + format == predefinedFormat!.textFormat) { return; } onSelectPredefinedFormat( PredefinedFormat( - imageFormat: predefinedFormat.imageFormat, + imageFormat: predefinedFormat?.imageFormat ?? ImageFormat.text, textFormat: format, ), ); diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_message_action_bar.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_message_action_bar.dart index d72f28d8d4..e5229daf40 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_message_action_bar.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_message_action_bar.dart @@ -301,7 +301,7 @@ class _ChangeFormatPopoverContent extends StatefulWidget { class _ChangeFormatPopoverContentState extends State<_ChangeFormatPopoverContent> { - PredefinedFormat predefinedFormat = const PredefinedFormat.auto(); + PredefinedFormat? predefinedFormat; @override Widget build(BuildContext context) { @@ -362,7 +362,10 @@ class _ChangeFormatPopoverContentState cursor: SystemMouseCursors.click, child: GestureDetector( behavior: HitTestBehavior.opaque, - onTap: () => widget.onRegenerate?.call(predefinedFormat), + onTap: () { + widget.onRegenerate + ?.call(predefinedFormat ?? const PredefinedFormat.auto()); + }, child: SizedBox.square( dimension: DesktopAIPromptSizes.predefinedFormatButtonHeight, child: Center( @@ -467,7 +470,9 @@ class _SaveToPageButtonState extends State { final documentId = getOpenedDocumentId(); if (documentId != null) { await onAddToExistingPage(context, documentId); - await forceReloadAndUpdateSelection(documentId); + await forceReload(documentId); + await Future.delayed(const Duration(milliseconds: 500)); + await updateSelection(documentId); } else { widget.onOverrideVisibility?.call(true); if (spaceView != null) { @@ -497,6 +502,8 @@ class _SaveToPageButtonState extends State { if (context.mounted) { openPageFromMessage(context, view); } + await Future.delayed(const Duration(milliseconds: 500)); + await updateSelection(documentId); }, ), ); @@ -565,14 +572,20 @@ class _SaveToPageButtonState extends State { ); } - Future forceReloadAndUpdateSelection(String documentId) async { + Future forceReload(String documentId) async { final bloc = DocumentBloc.findOpen(documentId); if (bloc == null) { return; } await bloc.forceReloadDocumentState(); - await Future.delayed(const Duration(milliseconds: 500)); + } + Future updateSelection(String documentId) async { + final bloc = DocumentBloc.findOpen(documentId); + if (bloc == null) { + return; + } + await bloc.forceReloadDocumentState(); final editorState = bloc.state.editorState; final lastNodePath = editorState?.getLastSelectable()?.$1.path; if (editorState == null || lastNodePath == null) { diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/file/file_selection_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/file/file_selection_menu.dart index 8a5d7047a9..132a7b8e7e 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/file/file_selection_menu.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/file/file_selection_menu.dart @@ -9,26 +9,19 @@ extension InsertFile on EditorState { if (selection == null || !selection.isCollapsed) { return; } - final node = getNodeAtPath(selection.end.path); - if (node == null) { + final path = selection.end.path; + final node = getNodeAtPath(path); + final delta = node?.delta; + if (node == null || delta == null) { return; } final file = fileNode(url: '')..extraInfos = {'global_key': key}; - final transaction = this.transaction; - // if the current node is empty paragraph, replace it with the file node - if (node.type == ParagraphBlockKeys.type && - (node.delta?.isEmpty ?? false)) { - transaction - ..insertNode(node.path, file) - ..deleteNode(node); - } else { - transaction.insertNode(node.path.next, file); - } - - transaction.afterSelection = - Selection.collapsed(Position(path: node.path.next)); - transaction.selectionExtraInfo = {}; + final insertedPath = delta.isEmpty ? path : path.next; + final transaction = this.transaction + ..insertNode(insertedPath, file) + ..insertNode(insertedPath, paragraphNode()) + ..afterSelection = Selection.collapsed(Position(path: insertedPath.next)); return apply(transaction); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/image_selection_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/image_selection_menu.dart index f41127b78d..5a30a4dda5 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/image_selection_menu.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/image_selection_menu.dart @@ -58,26 +58,20 @@ extension InsertImage on EditorState { if (selection == null || !selection.isCollapsed) { return; } - final node = getNodeAtPath(selection.end.path); - if (node == null) { + final path = selection.end.path; + final node = getNodeAtPath(path); + final delta = node?.delta; + if (node == null || delta == null) { return; } final emptyImage = imageNode(url: '') ..extraInfos = {kImagePlaceholderKey: key}; - final transaction = this.transaction; - // if the current node is empty paragraph, replace it with image node - if (node.type == ParagraphBlockKeys.type && - (node.delta?.isEmpty ?? false)) { - transaction - ..insertNode(node.path, emptyImage) - ..deleteNode(node); - } else { - transaction.insertNode(node.path.next, emptyImage); - } - transaction.afterSelection = - Selection.collapsed(Position(path: node.path.next)); - transaction.selectionExtraInfo = {}; + final insertedPath = delta.isEmpty ? path : path.next; + final transaction = this.transaction + ..insertNode(insertedPath, emptyImage) + ..insertNode(insertedPath, paragraphNode()) + ..afterSelection = Selection.collapsed(Position(path: insertedPath.next)); return apply(transaction); } @@ -87,26 +81,20 @@ extension InsertImage on EditorState { if (selection == null || !selection.isCollapsed) { return; } - final node = getNodeAtPath(selection.end.path); - if (node == null) { + final path = selection.end.path; + final node = getNodeAtPath(path); + final delta = node?.delta; + if (node == null || delta == null) { return; } final emptyBlock = multiImageNode() ..extraInfos = {kMultiImagePlaceholderKey: key}; - final transaction = this.transaction; - // if the current node is empty paragraph, replace it with image node - if (node.type == ParagraphBlockKeys.type && - (node.delta?.isEmpty ?? false)) { - transaction - ..insertNode(node.path, emptyBlock) - ..deleteNode(node); - } else { - transaction.insertNode(node.path.next, emptyBlock); - } - transaction.afterSelection = - Selection.collapsed(Position(path: node.path.next)); - transaction.selectionExtraInfo = {}; + final insertedPath = delta.isEmpty ? path : path.next; + final transaction = this.transaction + ..insertNode(insertedPath, emptyBlock) + ..insertNode(insertedPath, paragraphNode()) + ..afterSelection = Selection.collapsed(Position(path: insertedPath.next)); return apply(transaction); } diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index da064fe623..7e6aec892f 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -61,8 +61,8 @@ packages: dependency: "direct main" description: path: "." - ref: "448174b" - resolved-ref: "448174bb11ae4cfb3bb093522ef02f10f856abdf" + ref: "5352bb4" + resolved-ref: "5352bb4a2483039b97073f40c32a93f8fc5e7242" url: "https://github.com/AppFlowy-IO/appflowy-editor.git" source: git version: "4.0.0" diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index eba1101f13..385d2f453c 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -174,7 +174,7 @@ dependency_overrides: appflowy_editor: git: url: https://github.com/AppFlowy-IO/appflowy-editor.git - ref: "448174b" + ref: "5352bb4" appflowy_editor_plugins: git: diff --git a/frontend/resources/flowy_icons/16x/ai_image.svg b/frontend/resources/flowy_icons/16x/ai_image.svg index 79f6081090..6f33715496 100644 --- a/frontend/resources/flowy_icons/16x/ai_image.svg +++ b/frontend/resources/flowy_icons/16x/ai_image.svg @@ -1,12 +1,12 @@ - - - - + + + + - - + + diff --git a/frontend/resources/flowy_icons/16x/ai_text_image.svg b/frontend/resources/flowy_icons/16x/ai_text_image.svg index 166048108f..320aad6bab 100644 --- a/frontend/resources/flowy_icons/16x/ai_text_image.svg +++ b/frontend/resources/flowy_icons/16x/ai_text_image.svg @@ -1,14 +1,14 @@ - - - - + + + + - - - + + + - + diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index ffe5504f63..7fd7046173 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -233,7 +233,7 @@ "number": "Numbered list", "table": "Table", "blankDescription": "Format response", - "defaultDescription": "Auto", + "defaultDescription": "Auto mode", "textWithImageDescription": "@:chat.changeFormat.text with image", "numberWithImageDescription": "@:chat.changeFormat.number with image", "bulletWithImageDescription": "@:chat.changeFormat.bullet with image", @@ -3069,7 +3069,7 @@ } } }, - "ai":{ + "ai": { "contentPolicyViolation": "Image generation failed due to sensitive content. Please rephrase your input and try again" } }