2024-07-15 15:23:23 +08:00
|
|
|
import 'package:appflowy/plugins/ai_chat/application/chat_file_bloc.dart';
|
2024-07-18 20:54:35 +08:00
|
|
|
import 'package:appflowy/plugins/ai_chat/application/chat_input_bloc.dart';
|
2024-07-26 07:58:54 +08:00
|
|
|
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart';
|
|
|
|
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
2024-07-15 15:23:23 +08:00
|
|
|
import 'package:desktop_drop/desktop_drop.dart';
|
2024-06-28 16:54:54 +02:00
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
import 'package:flutter/services.dart';
|
|
|
|
|
2024-06-03 14:27:28 +08:00
|
|
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
|
|
|
import 'package:appflowy/plugins/ai_chat/application/chat_bloc.dart';
|
2024-06-09 14:02:32 +08:00
|
|
|
import 'package:appflowy/plugins/ai_chat/presentation/ai_message_bubble.dart';
|
2024-06-03 14:27:28 +08:00
|
|
|
import 'package:appflowy/plugins/ai_chat/presentation/chat_related_question.dart';
|
2024-06-09 14:02:32 +08:00
|
|
|
import 'package:appflowy/plugins/ai_chat/presentation/user_message_bubble.dart';
|
2024-06-03 14:27:28 +08:00
|
|
|
import 'package:appflowy/workspace/presentation/home/toast.dart';
|
|
|
|
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
|
|
|
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
|
|
|
import 'package:easy_localization/easy_localization.dart';
|
|
|
|
import 'package:flowy_infra/theme_extension.dart';
|
|
|
|
import 'package:flowy_infra_ui/style_widget/text.dart';
|
|
|
|
import 'package:flowy_infra_ui/widget/spacing.dart';
|
|
|
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
|
|
import 'package:flutter_chat_types/flutter_chat_types.dart';
|
|
|
|
import 'package:flutter_chat_types/flutter_chat_types.dart' as types;
|
2024-06-28 16:54:54 +02:00
|
|
|
import 'package:flutter_chat_ui/flutter_chat_ui.dart' show Chat;
|
2024-06-03 14:27:28 +08:00
|
|
|
|
2024-07-27 23:47:08 +08:00
|
|
|
import 'presentation/chat_input/chat_input.dart';
|
2024-06-03 14:27:28 +08:00
|
|
|
import 'presentation/chat_popmenu.dart';
|
|
|
|
import 'presentation/chat_theme.dart';
|
|
|
|
import 'presentation/chat_user_invalid_message.dart';
|
|
|
|
import 'presentation/chat_welcome_page.dart';
|
2024-06-09 14:02:32 +08:00
|
|
|
import 'presentation/message/ai_text_message.dart';
|
|
|
|
import 'presentation/message/user_text_message.dart';
|
2024-06-03 14:27:28 +08:00
|
|
|
|
2024-06-03 23:20:33 +08:00
|
|
|
class AIChatUILayout {
|
|
|
|
static EdgeInsets get chatPadding =>
|
2024-06-28 16:54:54 +02:00
|
|
|
isMobile ? EdgeInsets.zero : const EdgeInsets.symmetric(horizontal: 20);
|
2024-06-03 23:20:33 +08:00
|
|
|
|
|
|
|
static EdgeInsets get welcomePagePadding => isMobile
|
|
|
|
? const EdgeInsets.symmetric(horizontal: 20)
|
2024-06-28 16:54:54 +02:00
|
|
|
: const EdgeInsets.symmetric(horizontal: 50);
|
2024-06-03 23:20:33 +08:00
|
|
|
|
|
|
|
static double get messageWidthRatio => 0.85;
|
|
|
|
|
|
|
|
static EdgeInsets safeAreaInsets(BuildContext context) {
|
|
|
|
final query = MediaQuery.of(context);
|
|
|
|
return isMobile
|
|
|
|
? EdgeInsets.fromLTRB(
|
|
|
|
query.padding.left,
|
|
|
|
0,
|
|
|
|
query.padding.right,
|
|
|
|
query.viewInsets.bottom + query.padding.bottom,
|
|
|
|
)
|
2024-06-28 16:54:54 +02:00
|
|
|
: const EdgeInsets.symmetric(horizontal: 50) +
|
|
|
|
const EdgeInsets.only(bottom: 20);
|
2024-06-03 23:20:33 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-07-15 15:23:23 +08:00
|
|
|
class AIChatPage extends StatelessWidget {
|
2024-06-03 14:27:28 +08:00
|
|
|
const AIChatPage({
|
|
|
|
super.key,
|
|
|
|
required this.view,
|
|
|
|
required this.onDeleted,
|
|
|
|
required this.userProfile,
|
|
|
|
});
|
|
|
|
|
|
|
|
final ViewPB view;
|
|
|
|
final VoidCallback onDeleted;
|
|
|
|
final UserProfilePB userProfile;
|
|
|
|
|
|
|
|
@override
|
2024-07-15 15:23:23 +08:00
|
|
|
Widget build(BuildContext context) {
|
|
|
|
if (userProfile.authenticator == AuthenticatorPB.AppFlowyCloud) {
|
2024-07-18 20:54:35 +08:00
|
|
|
return MultiBlocProvider(
|
|
|
|
providers: [
|
|
|
|
BlocProvider(
|
2024-07-26 07:58:54 +08:00
|
|
|
create: (_) => ChatFileBloc(chatId: view.id.toString())
|
|
|
|
..add(const ChatFileEvent.initial()),
|
2024-07-18 20:54:35 +08:00
|
|
|
),
|
|
|
|
BlocProvider(
|
|
|
|
create: (_) => ChatBloc(
|
|
|
|
view: view,
|
|
|
|
userProfile: userProfile,
|
|
|
|
)..add(const ChatEvent.initialLoad()),
|
|
|
|
),
|
2024-07-27 23:47:08 +08:00
|
|
|
BlocProvider(
|
|
|
|
create: (_) => ChatInputBloc()..add(const ChatInputEvent.started()),
|
|
|
|
),
|
2024-07-18 20:54:35 +08:00
|
|
|
],
|
2024-07-26 07:58:54 +08:00
|
|
|
child: BlocListener<ChatFileBloc, ChatFileState>(
|
|
|
|
listenWhen: (previous, current) =>
|
|
|
|
previous.indexFileIndicator != current.indexFileIndicator,
|
|
|
|
listener: (context, state) {
|
|
|
|
_handleIndexIndicator(state.indexFileIndicator, context);
|
|
|
|
},
|
|
|
|
child: BlocBuilder<ChatFileBloc, ChatFileState>(
|
|
|
|
builder: (context, state) {
|
|
|
|
return DropTarget(
|
2024-07-18 20:54:35 +08:00
|
|
|
onDragDone: (DropDoneDetails detail) async {
|
2024-07-26 07:58:54 +08:00
|
|
|
if (state.supportChatWithFile) {
|
|
|
|
await showConfirmDialog(
|
|
|
|
context: context,
|
|
|
|
style: ConfirmPopupStyle.cancelAndOk,
|
|
|
|
title: LocaleKeys.chat_chatWithFilePrompt.tr(),
|
|
|
|
confirmLabel: LocaleKeys.button_confirm.tr(),
|
|
|
|
onConfirm: () {
|
|
|
|
for (final file in detail.files) {
|
|
|
|
context
|
|
|
|
.read<ChatFileBloc>()
|
|
|
|
.add(ChatFileEvent.newFile(file.path, file.name));
|
|
|
|
}
|
|
|
|
},
|
|
|
|
description: '',
|
|
|
|
);
|
2024-07-18 20:54:35 +08:00
|
|
|
}
|
|
|
|
},
|
2024-07-26 07:58:54 +08:00
|
|
|
child: _ChatContentPage(
|
|
|
|
view: view,
|
|
|
|
userProfile: userProfile,
|
|
|
|
),
|
2024-07-18 20:54:35 +08:00
|
|
|
);
|
2024-07-26 07:58:54 +08:00
|
|
|
},
|
|
|
|
),
|
2024-07-15 15:23:23 +08:00
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
return Center(
|
|
|
|
child: FlowyText(
|
|
|
|
LocaleKeys.chat_unsupportedCloudPrompt.tr(),
|
|
|
|
fontSize: 20,
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
2024-07-26 07:58:54 +08:00
|
|
|
|
|
|
|
void _handleIndexIndicator(
|
|
|
|
IndexFileIndicator? indicator,
|
|
|
|
BuildContext context,
|
|
|
|
) {
|
|
|
|
if (indicator != null) {
|
|
|
|
indicator.when(
|
|
|
|
finish: (fileName) {
|
|
|
|
showSnackBarMessage(
|
|
|
|
context,
|
|
|
|
LocaleKeys.chat_indexFileSuccess.tr(args: [fileName]),
|
|
|
|
);
|
|
|
|
},
|
|
|
|
indexing: (fileName) {
|
|
|
|
showSnackBarMessage(
|
|
|
|
context,
|
|
|
|
LocaleKeys.chat_indexingFile.tr(args: [fileName]),
|
|
|
|
duration: const Duration(seconds: 2),
|
|
|
|
);
|
|
|
|
},
|
|
|
|
error: (err) {
|
|
|
|
showSnackBarMessage(
|
|
|
|
context,
|
|
|
|
err,
|
|
|
|
);
|
|
|
|
},
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
2024-07-15 15:23:23 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
class _ChatContentPage extends StatefulWidget {
|
|
|
|
const _ChatContentPage({
|
|
|
|
required this.view,
|
|
|
|
required this.userProfile,
|
|
|
|
});
|
|
|
|
|
|
|
|
final UserProfilePB userProfile;
|
|
|
|
final ViewPB view;
|
|
|
|
|
|
|
|
@override
|
|
|
|
State<_ChatContentPage> createState() => _ChatContentPageState();
|
2024-06-03 14:27:28 +08:00
|
|
|
}
|
|
|
|
|
2024-07-15 15:23:23 +08:00
|
|
|
class _ChatContentPageState extends State<_ChatContentPage> {
|
2024-06-03 14:27:28 +08:00
|
|
|
late types.User _user;
|
|
|
|
|
|
|
|
@override
|
|
|
|
void initState() {
|
|
|
|
super.initState();
|
|
|
|
_user = types.User(id: widget.userProfile.id.toString());
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
if (widget.userProfile.authenticator == AuthenticatorPB.AppFlowyCloud) {
|
|
|
|
return buildChatWidget();
|
|
|
|
}
|
2024-06-28 16:54:54 +02:00
|
|
|
|
|
|
|
return Center(
|
|
|
|
child: FlowyText(
|
|
|
|
LocaleKeys.chat_unsupportedCloudPrompt.tr(),
|
|
|
|
fontSize: 20,
|
|
|
|
),
|
|
|
|
);
|
2024-06-03 14:27:28 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
Widget buildChatWidget() {
|
2024-06-28 16:54:54 +02:00
|
|
|
return Row(
|
|
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
|
|
children: [
|
|
|
|
Flexible(
|
|
|
|
child: ConstrainedBox(
|
|
|
|
constraints: const BoxConstraints(maxWidth: 784),
|
2024-07-18 20:54:35 +08:00
|
|
|
child: BlocBuilder<ChatBloc, ChatState>(
|
|
|
|
builder: (blocContext, state) => Chat(
|
|
|
|
messages: state.messages,
|
|
|
|
onSendPressed: (_) {
|
|
|
|
// We use custom bottom widget for chat input, so
|
|
|
|
// do not need to handle this event.
|
|
|
|
},
|
|
|
|
customBottomWidget: buildChatInput(blocContext),
|
|
|
|
user: _user,
|
|
|
|
theme: buildTheme(context),
|
|
|
|
onEndReached: () async {
|
|
|
|
if (state.hasMorePrevMessage &&
|
|
|
|
state.loadingPreviousStatus !=
|
|
|
|
const LoadingState.loading()) {
|
|
|
|
blocContext
|
|
|
|
.read<ChatBloc>()
|
|
|
|
.add(const ChatEvent.startLoadingPrevMessage());
|
|
|
|
}
|
|
|
|
},
|
|
|
|
emptyState: BlocBuilder<ChatBloc, ChatState>(
|
|
|
|
builder: (_, state) =>
|
|
|
|
state.initialLoadingStatus == const LoadingState.finish()
|
|
|
|
? Padding(
|
|
|
|
padding: AIChatUILayout.welcomePagePadding,
|
|
|
|
child: ChatWelcomePage(
|
|
|
|
onSelectedQuestion: (question) => blocContext
|
|
|
|
.read<ChatBloc>()
|
|
|
|
.add(ChatEvent.sendMessage(question)),
|
|
|
|
),
|
|
|
|
)
|
|
|
|
: const Center(
|
|
|
|
child: CircularProgressIndicator.adaptive(),
|
2024-06-03 23:20:33 +08:00
|
|
|
),
|
2024-06-03 14:27:28 +08:00
|
|
|
),
|
2024-07-18 20:54:35 +08:00
|
|
|
messageWidthRatio: AIChatUILayout.messageWidthRatio,
|
|
|
|
textMessageBuilder: (
|
|
|
|
textMessage, {
|
|
|
|
required messageWidth,
|
|
|
|
required showName,
|
|
|
|
}) =>
|
|
|
|
_buildAITextMessage(blocContext, textMessage),
|
|
|
|
bubbleBuilder: (
|
|
|
|
child, {
|
|
|
|
required message,
|
|
|
|
required nextMessageInGroup,
|
|
|
|
}) {
|
|
|
|
if (message.author.id == _user.id) {
|
|
|
|
return ChatUserMessageBubble(
|
|
|
|
message: message,
|
|
|
|
child: child,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
return _buildAIBubble(message, blocContext, state, child);
|
|
|
|
},
|
2024-06-28 16:54:54 +02:00
|
|
|
),
|
|
|
|
),
|
2024-06-03 14:27:28 +08:00
|
|
|
),
|
|
|
|
),
|
2024-06-28 16:54:54 +02:00
|
|
|
],
|
2024-06-03 14:27:28 +08:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2024-06-09 14:02:32 +08:00
|
|
|
Widget _buildAITextMessage(BuildContext context, TextMessage message) {
|
|
|
|
final isAuthor = message.author.id == _user.id;
|
|
|
|
if (isAuthor) {
|
|
|
|
return ChatTextMessageWidget(
|
|
|
|
user: message.author,
|
|
|
|
messageUserId: message.id,
|
|
|
|
text: message.text,
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
final stream = message.metadata?["$AnswerStream"];
|
|
|
|
final questionId = message.metadata?["question"];
|
|
|
|
return ChatAITextMessageWidget(
|
|
|
|
user: message.author,
|
|
|
|
messageUserId: message.id,
|
|
|
|
text: stream is AnswerStream ? stream : message.text,
|
|
|
|
key: ValueKey(message.id),
|
|
|
|
questionId: questionId,
|
|
|
|
chatId: widget.view.id,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Widget _buildAIBubble(
|
|
|
|
Message message,
|
|
|
|
BuildContext blocContext,
|
|
|
|
ChatState state,
|
|
|
|
Widget child,
|
|
|
|
) {
|
|
|
|
final messageType = onetimeMessageTypeFromMeta(
|
|
|
|
message.metadata,
|
|
|
|
);
|
|
|
|
|
|
|
|
if (messageType == OnetimeShotType.invalidSendMesssage) {
|
|
|
|
return ChatInvalidUserMessage(
|
|
|
|
message: message,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (messageType == OnetimeShotType.relatedQuestion) {
|
|
|
|
return RelatedQuestionList(
|
|
|
|
onQuestionSelected: (question) {
|
|
|
|
blocContext.read<ChatBloc>().add(ChatEvent.sendMessage(question));
|
|
|
|
blocContext
|
|
|
|
.read<ChatBloc>()
|
|
|
|
.add(const ChatEvent.clearReleatedQuestion());
|
|
|
|
},
|
|
|
|
chatId: widget.view.id,
|
|
|
|
relatedQuestions: state.relatedQuestions,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
return ChatAIMessageBubble(
|
|
|
|
message: message,
|
|
|
|
customMessageType: messageType,
|
|
|
|
child: child,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2024-06-03 14:27:28 +08:00
|
|
|
Widget buildBubble(Message message, Widget child) {
|
|
|
|
final isAuthor = message.author.id == _user.id;
|
|
|
|
const borderRadius = BorderRadius.all(Radius.circular(6));
|
|
|
|
final childWithPadding = isAuthor
|
|
|
|
? Padding(
|
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
|
|
|
|
child: child,
|
|
|
|
)
|
|
|
|
: Padding(
|
|
|
|
padding: const EdgeInsets.all(8),
|
|
|
|
child: child,
|
|
|
|
);
|
|
|
|
|
|
|
|
// If the message is from the author, we will decorate it with a different color
|
|
|
|
final decoratedChild = isAuthor
|
|
|
|
? DecoratedBox(
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
borderRadius: borderRadius,
|
|
|
|
color: !isAuthor || message.type == types.MessageType.image
|
|
|
|
? AFThemeExtension.of(context).tint1
|
|
|
|
: Theme.of(context).colorScheme.secondary,
|
|
|
|
),
|
|
|
|
child: childWithPadding,
|
|
|
|
)
|
|
|
|
: childWithPadding;
|
|
|
|
|
|
|
|
// If the message is from the author, no further actions are needed
|
|
|
|
if (isAuthor) {
|
|
|
|
return ClipRRect(
|
|
|
|
borderRadius: borderRadius,
|
|
|
|
child: decoratedChild,
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
if (isMobile) {
|
|
|
|
return ChatPopupMenu(
|
|
|
|
onAction: (action) {
|
|
|
|
switch (action) {
|
|
|
|
case ChatMessageAction.copy:
|
|
|
|
if (message is TextMessage) {
|
|
|
|
Clipboard.setData(ClipboardData(text: message.text));
|
|
|
|
showMessageToast(LocaleKeys.grid_row_copyProperty.tr());
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
builder: (context) =>
|
|
|
|
ClipRRect(borderRadius: borderRadius, child: decoratedChild),
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
// Show hover effect only on desktop
|
|
|
|
return ClipRRect(
|
|
|
|
borderRadius: borderRadius,
|
|
|
|
child: ChatAIMessageHover(
|
|
|
|
message: message,
|
|
|
|
child: decoratedChild,
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Widget buildChatInput(BuildContext context) {
|
2024-06-03 23:20:33 +08:00
|
|
|
return ClipRect(
|
|
|
|
child: Padding(
|
|
|
|
padding: AIChatUILayout.safeAreaInsets(context),
|
2024-07-18 20:54:35 +08:00
|
|
|
child: BlocBuilder<ChatInputBloc, ChatInputState>(
|
|
|
|
builder: (context, state) {
|
2024-07-27 23:47:08 +08:00
|
|
|
final hintText = state.aiType.when(
|
|
|
|
appflowyAI: () => LocaleKeys.chat_inputMessageHint.tr(),
|
|
|
|
localAI: () => LocaleKeys.chat_inputLocalAIMessageHint.tr(),
|
|
|
|
);
|
|
|
|
|
|
|
|
return Column(
|
|
|
|
children: [
|
|
|
|
BlocSelector<ChatBloc, ChatState, LoadingState>(
|
|
|
|
selector: (state) => state.streamingStatus,
|
|
|
|
builder: (context, state) {
|
|
|
|
return ChatInput(
|
|
|
|
chatId: widget.view.id,
|
|
|
|
onSendPressed: (message) =>
|
|
|
|
onSendPressed(context, message.text),
|
|
|
|
isStreaming: state != const LoadingState.finish(),
|
|
|
|
onStopStreaming: () {
|
|
|
|
context
|
|
|
|
.read<ChatBloc>()
|
|
|
|
.add(const ChatEvent.stopStream());
|
|
|
|
},
|
|
|
|
hintText: hintText,
|
|
|
|
);
|
|
|
|
},
|
|
|
|
),
|
|
|
|
const VSpace(6),
|
|
|
|
Opacity(
|
|
|
|
opacity: 0.6,
|
|
|
|
child: FlowyText(
|
|
|
|
LocaleKeys.chat_aiMistakePrompt.tr(),
|
|
|
|
fontSize: 12,
|
2024-07-18 20:54:35 +08:00
|
|
|
),
|
2024-07-27 23:47:08 +08:00
|
|
|
),
|
|
|
|
],
|
2024-07-18 20:54:35 +08:00
|
|
|
);
|
|
|
|
},
|
2024-06-03 14:27:28 +08:00
|
|
|
),
|
2024-06-03 23:20:33 +08:00
|
|
|
),
|
2024-06-03 14:27:28 +08:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
AFDefaultChatTheme buildTheme(BuildContext context) {
|
|
|
|
return AFDefaultChatTheme(
|
|
|
|
backgroundColor: AFThemeExtension.of(context).background,
|
|
|
|
primaryColor: Theme.of(context).colorScheme.primary,
|
|
|
|
secondaryColor: AFThemeExtension.of(context).tint1,
|
|
|
|
receivedMessageDocumentIconColor: Theme.of(context).primaryColor,
|
|
|
|
receivedMessageCaptionTextStyle: TextStyle(
|
|
|
|
color: AFThemeExtension.of(context).textColor,
|
|
|
|
fontSize: 16,
|
|
|
|
fontWeight: FontWeight.w500,
|
|
|
|
height: 1.5,
|
|
|
|
),
|
|
|
|
receivedMessageBodyTextStyle: TextStyle(
|
|
|
|
color: AFThemeExtension.of(context).textColor,
|
|
|
|
fontSize: 16,
|
|
|
|
fontWeight: FontWeight.w500,
|
|
|
|
height: 1.5,
|
|
|
|
),
|
|
|
|
receivedMessageLinkTitleTextStyle: TextStyle(
|
|
|
|
color: AFThemeExtension.of(context).textColor,
|
|
|
|
fontSize: 16,
|
|
|
|
fontWeight: FontWeight.w500,
|
|
|
|
height: 1.5,
|
|
|
|
),
|
|
|
|
receivedMessageBodyLinkTextStyle: const TextStyle(
|
|
|
|
color: Colors.lightBlue,
|
|
|
|
fontSize: 16,
|
|
|
|
fontWeight: FontWeight.w500,
|
|
|
|
height: 1.5,
|
|
|
|
),
|
|
|
|
sentMessageBodyTextStyle: TextStyle(
|
|
|
|
color: AFThemeExtension.of(context).textColor,
|
|
|
|
fontSize: 16,
|
|
|
|
fontWeight: FontWeight.w500,
|
|
|
|
height: 1.5,
|
|
|
|
),
|
|
|
|
sentMessageBodyLinkTextStyle: const TextStyle(
|
|
|
|
color: Colors.blue,
|
|
|
|
fontSize: 16,
|
|
|
|
fontWeight: FontWeight.w500,
|
|
|
|
height: 1.5,
|
|
|
|
),
|
|
|
|
inputElevation: 2,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
void onSendPressed(BuildContext context, String message) {
|
|
|
|
context.read<ChatBloc>().add(ChatEvent.sendMessage(message));
|
|
|
|
}
|
|
|
|
}
|