diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_bloc.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_bloc.dart index 82840db5d9..330d86d54f 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_bloc.dart @@ -67,7 +67,19 @@ class ChatBloc extends Bloc { chatId: state.view.id, limit: Int64(10), ); - ChatEventLoadNextMessage(payload).send(); + ChatEventLoadNextMessage(payload).send().then( + (result) { + result.fold((list) { + if (!isClosed) { + final messages = + list.messages.map(_createTextMessage).toList(); + add(ChatEvent.didLoadLatestMessages(messages)); + } + }, (err) { + Log.error("Failed to load messages: $err"); + }); + }, + ); }, startLoadingPrevMessage: () async { Int64? beforeMessageId; diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_text_message.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_text_message.dart index 0eb4dde938..305eba9cd2 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_text_message.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_text_message.dart @@ -2,6 +2,7 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/ai_chat/application/chat_ai_message_bloc.dart'; import 'package:appflowy/plugins/ai_chat/application/chat_bloc.dart'; import 'package:appflowy/plugins/ai_chat/presentation/chat_loading.dart'; +import 'package:appflowy/util/theme_extension.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:fixnum/fixnum.dart'; import 'package:flowy_infra/theme_extension.dart'; @@ -13,6 +14,8 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_chat_types/flutter_chat_types.dart'; import 'package:markdown_widget/markdown_widget.dart'; +import 'selectable_highlight.dart'; + class ChatAITextMessageWidget extends StatelessWidget { const ChatAITextMessageWidget({ super.key, @@ -132,16 +135,28 @@ class ChatAITextMessageWidget extends StatelessWidget { ), ), PreConfig( - padding: const EdgeInsets.all(14), - decoration: BoxDecoration( - color: Theme.of(context) - .colorScheme - .surfaceContainerHighest - .withOpacity(0.6), - borderRadius: const BorderRadius.all( - Radius.circular(8.0), - ), - ), + builder: (code, language) { + return ConstrainedBox( + constraints: const BoxConstraints( + minWidth: 800, + ), + child: ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(6.0)), + child: SelectableHighlightView( + code, + language: language, + theme: getHightlineTheme(context), + padding: const EdgeInsets.all(14), + textStyle: TextStyle( + color: AFThemeExtension.of(context).textColor, + fontSize: 14, + fontWeight: FontWeight.bold, + height: 1.5, + ), + ), + ), + ); + }, ), PConfig( textStyle: TextStyle( @@ -168,6 +183,51 @@ class ChatAITextMessageWidget extends StatelessWidget { } } +Map getHightlineTheme(BuildContext context) { + return { + 'root': TextStyle( + color: const Color(0xffabb2bf), + backgroundColor: + Theme.of(context).isLightMode ? Colors.white : Colors.black38, + ), + 'comment': + const TextStyle(color: Color(0xff5c6370), fontStyle: FontStyle.italic), + 'quote': + const TextStyle(color: Color(0xff5c6370), fontStyle: FontStyle.italic), + 'doctag': const TextStyle(color: Color(0xffc678dd)), + 'keyword': const TextStyle(color: Color(0xffc678dd)), + 'formula': const TextStyle(color: Color(0xffc678dd)), + 'section': const TextStyle(color: Color(0xffe06c75)), + 'name': const TextStyle(color: Color(0xffe06c75)), + 'selector-tag': const TextStyle(color: Color(0xffe06c75)), + 'deletion': const TextStyle(color: Color(0xffe06c75)), + 'subst': const TextStyle(color: Color(0xffe06c75)), + 'literal': const TextStyle(color: Color(0xff56b6c2)), + 'string': const TextStyle(color: Color(0xff98c379)), + 'regexp': const TextStyle(color: Color(0xff98c379)), + 'addition': const TextStyle(color: Color(0xff98c379)), + 'attribute': const TextStyle(color: Color(0xff98c379)), + 'meta-string': const TextStyle(color: Color(0xff98c379)), + 'built_in': const TextStyle(color: Color(0xffe6c07b)), + 'attr': const TextStyle(color: Color(0xffd19a66)), + 'variable': const TextStyle(color: Color(0xffd19a66)), + 'template-variable': const TextStyle(color: Color(0xffd19a66)), + 'type': const TextStyle(color: Color(0xffd19a66)), + 'selector-class': const TextStyle(color: Color(0xffd19a66)), + 'selector-attr': const TextStyle(color: Color(0xffd19a66)), + 'selector-pseudo': const TextStyle(color: Color(0xffd19a66)), + 'number': const TextStyle(color: Color(0xffd19a66)), + 'symbol': const TextStyle(color: Color(0xff61aeee)), + 'bullet': const TextStyle(color: Color(0xff61aeee)), + 'link': const TextStyle(color: Color(0xff61aeee)), + 'meta': const TextStyle(color: Color(0xff61aeee)), + 'selector-id': const TextStyle(color: Color(0xff61aeee)), + 'title': const TextStyle(color: Color(0xff61aeee)), + 'emphasis': const TextStyle(fontStyle: FontStyle.italic), + 'strong': const TextStyle(fontWeight: FontWeight.bold), + }; +} + class ChatH1Config extends HeadingConfig { const ChatH1Config({ this.style = const TextStyle( diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/selectable_highlight.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/selectable_highlight.dart new file mode 100644 index 0000000000..1452f5af8c --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/selectable_highlight.dart @@ -0,0 +1,92 @@ +import 'package:flutter/material.dart'; +import 'package:highlight/highlight.dart'; + +/// Highlight Flutter Widget +class SelectableHighlightView extends StatelessWidget { + SelectableHighlightView( + String input, { + super.key, + this.language, + this.theme = const {}, + this.padding, + this.textStyle, + int tabSize = 8, + }) : source = input.replaceAll('\t', ' ' * tabSize); + + /// The original code to be highlighted + final String source; + + /// Highlight language + /// + /// It is recommended to give it a value for performance + /// + /// [All available languages](https://github.com/pd4d10/highlight/tree/master/highlight/lib/languages) + final String? language; + + /// Highlight theme + /// + /// [All available themes](https://github.com/pd4d10/highlight/blob/master/flutter_highlight/lib/themes) + final Map theme; + + /// Padding + final EdgeInsetsGeometry? padding; + + /// Text styles + /// + /// Specify text styles such as font family and font size + final TextStyle? textStyle; + + List _convert(List nodes) { + final List spans = []; + var currentSpans = spans; + final List> stack = []; + + // ignore: always_declare_return_types + traverse(Node node) { + if (node.value != null) { + currentSpans.add( + node.className == null + ? TextSpan(text: node.value) + : TextSpan(text: node.value, style: theme[node.className!]), + ); + } else if (node.children != null) { + final List tmp = []; + currentSpans + .add(TextSpan(children: tmp, style: theme[node.className!])); + stack.add(currentSpans); + currentSpans = tmp; + + for (final n in node.children!) { + traverse(n); + if (n == node.children!.last) { + currentSpans = stack.isEmpty ? spans : stack.removeLast(); + } + } + } + } + + for (final node in nodes) { + traverse(node); + } + + return spans; + } + + static const _rootKey = 'root'; + static const _defaultBackgroundColor = Color(0xffffffff); + + @override + Widget build(BuildContext context) { + return Container( + color: theme[_rootKey]?.backgroundColor ?? _defaultBackgroundColor, + padding: padding, + child: SelectableText.rich( + TextSpan( + style: textStyle, + children: + _convert(highlight.parse(source, language: language).nodes!), + ), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index ffdf26e42a..cf31629dec 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -681,7 +681,7 @@ packages: source: git version: "1.0.2" flutter_highlight: - dependency: transitive + dependency: "direct main" description: name: flutter_highlight sha256: "7b96333867aa07e122e245c033b8ad622e4e3a42a1a2372cbb098a2541d8782c" diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index 96a193e7f7..a332e75572 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -149,6 +149,7 @@ dependencies: # BitsDojo Window for Windows bitsdojo_window: ^0.1.6 + flutter_highlight: ^0.7.0 dev_dependencies: flutter_lints: ^3.0.1