feat: show indicator when send chat message with attachment/mention etc (#5919)

* chore: adjust line height

* chore: send stream message

* chore: index file

* chore: clippy
This commit is contained in:
Nathan.fooo 2024-08-10 17:23:37 +08:00 committed by GitHub
parent 758c304a74
commit 7abe9f4661
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
64 changed files with 676 additions and 172 deletions

View File

@ -102,6 +102,7 @@ class _TypeOptionMenuItem<T> extends StatelessWidget {
value.text,
fontSize: 14.0,
maxLines: 2,
lineHeight: 1.0,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
),

View File

@ -176,6 +176,7 @@ class DatabaseViewSettingTile extends StatelessWidget {
return Row(
children: [
FlowyText(
lineHeight: 1.0,
databaseLayoutFromViewLayout(view.layout).layoutName,
color: Theme.of(context).hintColor,
),

View File

@ -29,6 +29,7 @@ class FontSetting extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
children: [
FlowyText(
lineHeight: 1.0,
name,
color: theme.colorScheme.onSurface,
),

View File

@ -38,6 +38,7 @@ class _LanguageSettingGroupState extends State<LanguageSettingGroup> {
mainAxisSize: MainAxisSize.min,
children: [
FlowyText(
lineHeight: 1.0,
languageFromLocale(locale),
color: theme.colorScheme.onSurface,
),

View File

@ -39,7 +39,8 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
final String chatId;
/// The last streaming message id
String lastStreamMessageId = '';
String answerStreamMessageId = '';
String questionStreamMessageId = '';
/// Using a temporary map to associate the real message ID with the last streaming message ID.
///
@ -127,21 +128,11 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
);
},
// streaming message
streaming: (Message message) {
final allMessages = _perminentMessages();
allMessages.insert(0, message);
emit(
state.copyWith(
messages: allMessages,
streamingState: const StreamingState.streaming(),
canSendMessage: false,
),
);
},
finishStreaming: () {
finishAnswerStreaming: () {
emit(
state.copyWith(
streamingState: const StreamingState.done(),
acceptRelatedQuestion: true,
canSendMessage:
state.sendingState == const SendMessageState.done(),
),
@ -162,9 +153,9 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
// If the streaming is not started, remove the message from the list
if (!state.answerStream!.hasStarted) {
allMessages.removeWhere(
(element) => element.id == lastStreamMessageId,
(element) => element.id == answerStreamMessageId,
);
lastStreamMessageId = "";
answerStreamMessageId = "";
}
// when stop stream, we will set the answer stream to null. Which means the streaming
@ -189,22 +180,26 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
),
);
},
startAnswerStreaming: (Message message) {
final allMessages = _perminentMessages();
allMessages.insert(0, message);
emit(
state.copyWith(
messages: allMessages,
streamingState: const StreamingState.streaming(),
canSendMessage: false,
),
);
},
sendMessage: (String message, Map<String, dynamic>? metadata) async {
unawaited(_startStreamingMessage(message, metadata, emit));
final allMessages = _perminentMessages();
// allMessages.insert(
// 0,
// CustomMessage(
// metadata: OnetimeShotType.sendingMessage.toMap(),
// author: User(id: state.userProfile.id.toString()),
// id: state.userProfile.id.toString(),
// ),
// );
emit(
state.copyWith(
lastSentMessage: null,
messages: allMessages,
relatedQuestions: [],
acceptRelatedQuestion: false,
sendingState: const SendMessageState.sending(),
canSendMessage: false,
),
@ -257,10 +252,17 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
chatMessageCallback: (pb) {
if (!isClosed) {
// 3 mean message response from AI
if (pb.authorType == 3 && lastStreamMessageId.isNotEmpty) {
if (pb.authorType == 3 && answerStreamMessageId.isNotEmpty) {
temporaryMessageIDMap[pb.messageId.toString()] =
lastStreamMessageId;
lastStreamMessageId = "";
answerStreamMessageId;
answerStreamMessageId = "";
}
// 1 mean message response from User
if (pb.authorType == 1 && questionStreamMessageId.isNotEmpty) {
temporaryMessageIDMap[pb.messageId.toString()] =
questionStreamMessageId;
questionStreamMessageId = "";
}
final message = _createTextMessage(pb);
@ -270,7 +272,7 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
chatErrorMessageCallback: (err) {
if (!isClosed) {
Log.error("chat error: ${err.errorMessage}");
add(const ChatEvent.finishStreaming());
add(const ChatEvent.finishAnswerStreaming());
}
},
latestMessageCallback: (list) {
@ -287,7 +289,7 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
},
finishStreamingCallback: () {
if (!isClosed) {
add(const ChatEvent.finishStreaming());
add(const ChatEvent.finishAnswerStreaming());
// The answer strema will bet set to null after the streaming is finished or canceled.
// so if the answer stream is null, we will not get related question.
if (state.lastSentMessage != null && state.answerStream != null) {
@ -300,7 +302,9 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
if (!isClosed) {
result.fold(
(list) {
add(ChatEvent.didReceiveRelatedQuestion(list.items));
if (state.acceptRelatedQuestion) {
add(ChatEvent.didReceiveRelatedQuestion(list.items));
}
},
(err) {
Log.error("Failed to get related question: $err");
@ -358,16 +362,21 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
}
final answerStream = AnswerStream();
final questionStream = QuestionStream();
add(ChatEvent.didUpdateAnswerStream(answerStream));
final payload = StreamChatPayloadPB(
chatId: state.view.id,
message: message,
messageType: ChatMessageTypePB.User,
textStreamPort: Int64(answerStream.nativePort),
questionStreamPort: Int64(questionStream.nativePort),
answerStreamPort: Int64(answerStream.nativePort),
metadata: await metadataPBFromMetadata(metadata),
);
final questionStreamMessage = _createQuestionStreamMessage(questionStream);
add(ChatEvent.receveMessage(questionStreamMessage));
// Stream message to the server
final result = await AIEventStreamMessage(payload).send();
result.fold(
@ -375,13 +384,12 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
if (!isClosed) {
add(ChatEvent.finishSending(question));
final questionMessageId = question.messageId;
final message = _createTextMessage(question);
add(ChatEvent.receveMessage(message));
// final message = _createTextMessage(question);
// add(ChatEvent.receveMessage(message));
final streamAnswer =
_createStreamMessage(answerStream, questionMessageId);
add(ChatEvent.streaming(streamAnswer));
_createAnswerStreamMessage(answerStream, question.messageId);
add(ChatEvent.startAnswerStreaming(streamAnswer));
}
},
(err) {
@ -404,9 +412,12 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
);
}
Message _createStreamMessage(AnswerStream stream, Int64 questionMessageId) {
Message _createAnswerStreamMessage(
AnswerStream stream,
Int64 questionMessageId,
) {
final streamMessageId = (questionMessageId + 1).toString();
lastStreamMessageId = streamMessageId;
answerStreamMessageId = streamMessageId;
return TextMessage(
author: User(id: "streamId:${nanoid()}"),
@ -421,6 +432,20 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
);
}
Message _createQuestionStreamMessage(QuestionStream stream) {
questionStreamMessageId = nanoid();
return TextMessage(
author: User(id: state.userProfile.id.toString()),
metadata: {
"$QuestionStream": stream,
"chatId": chatId,
},
id: questionStreamMessageId,
createdAt: DateTime.now().millisecondsSinceEpoch,
text: '',
);
}
Message _createTextMessage(ChatMessagePB message) {
String messageId = message.messageId.toString();
@ -454,9 +479,10 @@ class ChatEvent with _$ChatEvent {
_FinishSendMessage;
// receive message
const factory ChatEvent.streaming(Message message) = _StreamingMessage;
const factory ChatEvent.startAnswerStreaming(Message message) =
_StartAnswerStreaming;
const factory ChatEvent.receveMessage(Message message) = _ReceiveMessage;
const factory ChatEvent.finishStreaming() = _FinishStreamingMessage;
const factory ChatEvent.finishAnswerStreaming() = _FinishAnswerStreaming;
// loading messages
const factory ChatEvent.startLoadingPrevMessage() = _StartLoadPrevMessage;
@ -499,6 +525,7 @@ class ChatState with _$ChatState {
required bool hasMorePrevMessage,
// The related questions that are received after the user message is sent.
required List<RelatedQuestionPB> relatedQuestions,
@Default(false) bool acceptRelatedQuestion,
// The last user message that is sent to the server.
ChatMessagePB? lastSentMessage,
AnswerStream? answerStream,

View File

@ -92,3 +92,102 @@ class AnswerStream {
}
}
}
class QuestionStream {
QuestionStream() {
_port.handler = _controller.add;
_subscription = _controller.stream.listen(
(event) {
if (event.startsWith("data:")) {
_hasStarted = true;
final newText = event.substring(5);
_text += newText;
if (_onData != null) {
_onData!(_text);
}
} else if (event.startsWith("message_id:")) {
final messageId = event.substring(11);
_onMessageId?.call(messageId);
} else if (event.startsWith("start_index_file:")) {
final indexName = event.substring(17);
_onFileIndexStart?.call(indexName);
} else if (event.startsWith("end_index_file:")) {
final indexName = event.substring(10);
_onFileIndexEnd?.call(indexName);
} else if (event.startsWith("index_file_error:")) {
final indexName = event.substring(16);
_onFileIndexError?.call(indexName);
} else if (event.startsWith("index_start:")) {
_onIndexStart?.call();
} else if (event.startsWith("index_end:")) {
_onIndexEnd?.call();
} else if (event.startsWith("done:")) {
_onDone?.call();
} else if (event.startsWith("error:")) {
_error = event.substring(5);
if (_onError != null) {
_onError!(_error!);
}
}
},
onError: (error) {
if (_onError != null) {
_onError!(error.toString());
}
},
);
}
final RawReceivePort _port = RawReceivePort();
final StreamController<String> _controller = StreamController.broadcast();
late StreamSubscription<String> _subscription;
bool _hasStarted = false;
String? _error;
String _text = "";
// Callbacks
void Function(String text)? _onData;
void Function(String error)? _onError;
void Function(String messageId)? _onMessageId;
void Function(String indexName)? _onFileIndexStart;
void Function(String indexName)? _onFileIndexEnd;
void Function(String indexName)? _onFileIndexError;
void Function()? _onIndexStart;
void Function()? _onIndexEnd;
void Function()? _onDone;
int get nativePort => _port.sendPort.nativePort;
bool get hasStarted => _hasStarted;
String? get error => _error;
String get text => _text;
Future<void> dispose() async {
await _controller.close();
await _subscription.cancel();
_port.close();
}
void listen({
void Function(String text)? onData,
void Function(String error)? onError,
void Function(String messageId)? onMessageId,
void Function(String indexName)? onFileIndexStart,
void Function(String indexName)? onFileIndexEnd,
void Function(String indexName)? onFileIndexFail,
void Function()? onIndexStart,
void Function()? onIndexEnd,
void Function()? onDone,
}) {
_onData = onData;
_onError = onError;
_onMessageId = onMessageId;
_onFileIndexStart = onFileIndexStart;
_onFileIndexEnd = onFileIndexEnd;
_onFileIndexError = onFileIndexFail;
_onIndexStart = onIndexStart;
_onIndexEnd = onIndexEnd;
_onDone = onDone;
}
}

View File

@ -1,27 +1,91 @@
import 'package:appflowy/plugins/ai_chat/application/chat_entity.dart';
import 'package:appflowy/plugins/ai_chat/application/chat_message_stream.dart';
import 'package:appflowy_backend/log.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_chat_types/flutter_chat_types.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'chat_message_service.dart';
part 'chat_user_message_bloc.freezed.dart';
class ChatUserMessageBloc
extends Bloc<ChatUserMessageEvent, ChatUserMessageState> {
ChatUserMessageBloc({
required Message message,
required String? metadata,
required dynamic message,
}) : super(
ChatUserMessageState.initial(
message,
chatFilesFromMetadataString(metadata),
),
) {
on<ChatUserMessageEvent>(
(event, emit) async {
event.when(
initial: () {},
initial: () {
if (state.stream != null) {
add(ChatUserMessageEvent.updateText(state.stream!.text));
}
state.stream?.listen(
onData: (text) {
if (!isClosed) {
add(ChatUserMessageEvent.updateText(text));
}
},
onMessageId: (messageId) {
if (!isClosed) {
add(ChatUserMessageEvent.updateMessageId(messageId));
}
},
onError: (error) {
if (!isClosed) {
add(ChatUserMessageEvent.receiveError(error.toString()));
}
},
onFileIndexStart: (indexName) {
Log.debug("index start: $indexName");
},
onFileIndexEnd: (indexName) {
Log.info("index end: $indexName");
},
onFileIndexFail: (indexName) {
Log.debug("index fail: $indexName");
},
onIndexStart: () {
if (!isClosed) {
add(
const ChatUserMessageEvent.updateQuestionState(
QuestionMessageState.indexStart(),
),
);
}
},
onIndexEnd: () {
if (!isClosed) {
add(
const ChatUserMessageEvent.updateQuestionState(
QuestionMessageState.indexEnd(),
),
);
}
},
onDone: () {
if (!isClosed) {
add(
const ChatUserMessageEvent.updateQuestionState(
QuestionMessageState.finish(),
),
);
}
},
);
},
updateText: (String text) {
emit(state.copyWith(text: text));
},
updateMessageId: (String messageId) {
emit(state.copyWith(messageId: messageId));
},
receiveError: (String error) {},
updateQuestionState: (QuestionMessageState newState) {
emit(state.copyWith(messageState: newState));
},
);
},
);
@ -31,18 +95,47 @@ class ChatUserMessageBloc
@freezed
class ChatUserMessageEvent with _$ChatUserMessageEvent {
const factory ChatUserMessageEvent.initial() = Initial;
const factory ChatUserMessageEvent.updateText(String text) = _UpdateText;
const factory ChatUserMessageEvent.updateQuestionState(
QuestionMessageState newState,
) = _UpdateQuestionState;
const factory ChatUserMessageEvent.updateMessageId(String messageId) =
_UpdateMessageId;
const factory ChatUserMessageEvent.receiveError(String error) = _ReceiveError;
}
@freezed
class ChatUserMessageState with _$ChatUserMessageState {
const factory ChatUserMessageState({
required Message message,
required List<ChatFile> files,
required String text,
QuestionStream? stream,
String? messageId,
@Default(QuestionMessageState.finish()) QuestionMessageState messageState,
}) = _ChatUserMessageState;
factory ChatUserMessageState.initial(
Message message,
List<ChatFile> files,
dynamic message,
) =>
ChatUserMessageState(message: message, files: files);
ChatUserMessageState(
text: message is String ? message : "",
stream: message is QuestionStream ? message : null,
);
}
@freezed
class QuestionMessageState with _$QuestionMessageState {
const factory QuestionMessageState.indexFileStart(String fileName) =
_IndexFileStart;
const factory QuestionMessageState.indexFileEnd(String fileName) =
_IndexFileEnd;
const factory QuestionMessageState.indexFileFail(String fileName) =
_IndexFileFail;
const factory QuestionMessageState.indexStart() = _IndexStart;
const factory QuestionMessageState.indexEnd() = _IndexEnd;
const factory QuestionMessageState.finish() = _Finish;
}
extension QuestionMessageStateX on QuestionMessageState {
bool get isFinish => this is _Finish;
}

View File

@ -0,0 +1,48 @@
import 'package:appflowy/plugins/ai_chat/application/chat_entity.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_chat_types/flutter_chat_types.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'chat_message_service.dart';
part 'chat_user_message_bubble_bloc.freezed.dart';
class ChatUserMessageBubbleBloc
extends Bloc<ChatUserMessageBubbleEvent, ChatUserMessageBubbleState> {
ChatUserMessageBubbleBloc({
required Message message,
required String? metadata,
}) : super(
ChatUserMessageBubbleState.initial(
message,
chatFilesFromMetadataString(metadata),
),
) {
on<ChatUserMessageBubbleEvent>(
(event, emit) async {
event.when(
initial: () {},
);
},
);
}
}
@freezed
class ChatUserMessageBubbleEvent with _$ChatUserMessageBubbleEvent {
const factory ChatUserMessageBubbleEvent.initial() = Initial;
}
@freezed
class ChatUserMessageBubbleState with _$ChatUserMessageBubbleState {
const factory ChatUserMessageBubbleState({
required Message message,
required List<ChatFile> files,
}) = _ChatUserMessageBubbleState;
factory ChatUserMessageBubbleState.initial(
Message message,
List<ChatFile> files,
) =>
ChatUserMessageBubbleState(message: message, files: files);
}

View File

@ -319,21 +319,22 @@ class _ChatContentPageState extends State<_ChatContentPage> {
Widget _buildTextMessage(BuildContext context, TextMessage message) {
if (message.author.id == _user.id) {
final stream = message.metadata?["$QuestionStream"];
final metadata = message.metadata?[messageMetadataKey] as String?;
return ChatUserTextMessageWidget(
return ChatUserMessageWidget(
key: ValueKey(message.id),
user: message.author,
messageUserId: message.id,
message: message,
message: stream is QuestionStream ? stream : message.text,
metadata: metadata,
);
} else {
final stream = message.metadata?["$AnswerStream"];
final questionId = message.metadata?[messageQuestionIdKey];
final metadata = message.metadata?[messageMetadataKey] as String?;
return ChatAITextMessageWidget(
return ChatAIMessageWidget(
user: message.author,
messageUserId: message.id,
text: stream is AnswerStream ? stream : message.text,
message: stream is AnswerStream ? stream : message.text,
key: ValueKey(message.id),
questionId: questionId,
chatId: widget.view.id,

View File

@ -156,6 +156,7 @@ class _ActionItem extends StatelessWidget {
margin: const EdgeInsets.symmetric(horizontal: 6),
iconPadding: 10.0,
text: FlowyText.regular(
lineHeight: 1.0,
item.title,
),
onTap: onTap,

View File

@ -49,6 +49,8 @@ class AIMessageMetadata extends StatelessWidget {
child: FlowyText(
m.name,
fontSize: 14,
lineHeight: 1.0,
overflow: TextOverflow.ellipsis,
),
),
onTap: () => onSelectedMetadata(m),

View File

@ -14,12 +14,12 @@ import 'package:flutter_chat_types/flutter_chat_types.dart';
import 'ai_metadata.dart';
class ChatAITextMessageWidget extends StatelessWidget {
const ChatAITextMessageWidget({
class ChatAIMessageWidget extends StatelessWidget {
const ChatAIMessageWidget({
super.key,
required this.user,
required this.messageUserId,
required this.text,
required this.message,
required this.questionId,
required this.chatId,
required this.metadata,
@ -28,7 +28,9 @@ class ChatAITextMessageWidget extends StatelessWidget {
final User user;
final String messageUserId;
final dynamic text;
/// message can be a striing or Stream<String>
final dynamic message;
final Int64? questionId;
final String chatId;
final String? metadata;
@ -38,7 +40,7 @@ class ChatAITextMessageWidget extends StatelessWidget {
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => ChatAIMessageBloc(
message: text,
message: message,
metadata: metadata,
chatId: chatId,
questionId: questionId,
@ -59,7 +61,6 @@ class ChatAITextMessageWidget extends StatelessWidget {
return FlowyText(
LocaleKeys.sideBar_askOwnerToUpgradeToAIMax.tr(),
maxLines: 10,
lineHeight: 1.5,
);
},
ready: () {

View File

@ -1,6 +1,6 @@
import 'package:appflowy/plugins/ai_chat/application/chat_entity.dart';
import 'package:appflowy/plugins/ai_chat/application/chat_member_bloc.dart';
import 'package:appflowy/plugins/ai_chat/application/chat_user_message_bloc.dart';
import 'package:appflowy/plugins/ai_chat/application/chat_user_message_bubble_bloc.dart';
import 'package:appflowy/plugins/ai_chat/presentation/chat_avatar.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
@ -32,11 +32,11 @@ class ChatUserMessageBubble extends StatelessWidget {
final metadata = message.metadata?[messageMetadataKey] as String?;
return BlocProvider(
create: (context) => ChatUserMessageBloc(
create: (context) => ChatUserMessageBubbleBloc(
message: message,
metadata: metadata,
),
child: BlocBuilder<ChatUserMessageBloc, ChatUserMessageState>(
child: BlocBuilder<ChatUserMessageBubbleBloc, ChatUserMessageBubbleState>(
builder: (context, state) {
return Column(
mainAxisSize: MainAxisSize.min,

View File

@ -1,26 +1,50 @@
import 'package:appflowy/plugins/ai_chat/application/chat_user_message_bloc.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_chat_types/flutter_chat_types.dart';
class ChatUserTextMessageWidget extends StatelessWidget {
const ChatUserTextMessageWidget({
class ChatUserMessageWidget extends StatelessWidget {
const ChatUserMessageWidget({
super.key,
required this.user,
required this.messageUserId,
required this.message,
required this.metadata,
});
final User user;
final String messageUserId;
final TextMessage message;
final dynamic message;
final String? metadata;
@override
Widget build(BuildContext context) {
return TextMessageText(
text: message.text,
return BlocProvider(
create: (context) => ChatUserMessageBloc(message: message)
..add(const ChatUserMessageEvent.initial()),
child: BlocBuilder<ChatUserMessageBloc, ChatUserMessageState>(
builder: (context, state) {
final List<Widget> children = [];
children.add(
Flexible(
child: TextMessageText(
text: state.text,
),
),
);
if (!state.messageState.isFinish) {
children.add(const HSpace(6));
children.add(const CircularProgressIndicator.adaptive());
}
return Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.end,
children: children,
);
},
),
);
}
}

View File

@ -237,6 +237,7 @@ class _BoardColumnHeaderState extends State<BoardColumnHeader> {
leftIcon: FlowySvg(action.icon),
text: FlowyText.medium(
action.text,
lineHeight: 1.0,
overflow: TextOverflow.ellipsis,
),
onTap: () {

View File

@ -173,7 +173,10 @@ class LayoutDateField extends StatelessWidget {
return SizedBox(
height: GridSize.popoverItemHeight,
child: FlowyButton(
text: FlowyText.medium(fieldInfo.name),
text: FlowyText.medium(
fieldInfo.name,
lineHeight: 1.0,
),
onTap: () {
onUpdated(fieldInfo.id);
popoverMutex.close();
@ -206,6 +209,7 @@ class LayoutDateField extends StatelessWidget {
child: FlowyButton(
margin: const EdgeInsets.symmetric(vertical: 2.0, horizontal: 10.0),
text: FlowyText.medium(
lineHeight: 1.0,
LocaleKeys.calendar_settings_layoutDateField.tr(),
),
),
@ -307,6 +311,7 @@ class FirstDayOfWeek extends StatelessWidget {
child: FlowyButton(
margin: const EdgeInsets.symmetric(vertical: 2.0, horizontal: 10.0),
text: FlowyText.medium(
lineHeight: 1.0,
LocaleKeys.calendar_settings_firstDayOfWeek.tr(),
),
),
@ -367,7 +372,10 @@ class StartFromButton extends StatelessWidget {
return SizedBox(
height: GridSize.popoverItemHeight,
child: FlowyButton(
text: FlowyText.medium(title),
text: FlowyText.medium(
title,
lineHeight: 1.0,
),
onTap: () => onTap(dayIndex),
rightIcon: isSelected ? const FlowySvg(FlowySvgs.check_s) : null,
),

View File

@ -167,6 +167,7 @@ class _CalculateCellState extends State<CalculateCell> {
mainAxisAlignment: MainAxisAlignment.end,
children: [
FlowyText(
lineHeight: 1.0,
widget.calculation!.calculationType.shortLabel
.toUpperCase(),
color: Theme.of(context).hintColor,
@ -175,6 +176,7 @@ class _CalculateCellState extends State<CalculateCell> {
if (widget.calculation!.value.isNotEmpty) ...[
const HSpace(8),
FlowyText(
lineHeight: 1.0,
calculateValue,
color: AFThemeExtension.of(context).textColor,
overflow: TextOverflow.ellipsis,

View File

@ -22,7 +22,11 @@ class CalculationTypeItem extends StatelessWidget {
return SizedBox(
height: GridSize.popoverItemHeight,
child: FlowyButton(
text: FlowyText.medium(type.label, overflow: TextOverflow.ellipsis),
text: FlowyText.medium(
type.label,
overflow: TextOverflow.ellipsis,
lineHeight: 1.0,
),
onTap: () {
onTap();
PopoverContainer.of(context).close();

View File

@ -39,6 +39,7 @@ class ChoiceChipButton extends StatelessWidget {
decoration: decoration,
useIntrinsicWidth: true,
text: FlowyText(
lineHeight: 1.0,
filterInfo.fieldInfo.field.name,
color: AFThemeExtension.of(context).textColor,
),

View File

@ -31,6 +31,7 @@ class ConditionButton extends StatelessWidget {
child: FlowyButton(
useIntrinsicWidth: true,
text: FlowyText(
lineHeight: 1.0,
conditionName,
fontSize: 10,
color: AFThemeExtension.of(context).textColor,

View File

@ -163,6 +163,7 @@ class GridFilterPropertyCell extends StatelessWidget {
return FlowyButton(
hoverColor: AFThemeExtension.of(context).lightGreyHover,
text: FlowyText.medium(
lineHeight: 1.0,
fieldInfo.field.name,
color: AFThemeExtension.of(context).textColor,
),

View File

@ -87,6 +87,7 @@ class _AddFilterButtonState extends State<AddFilterButton> {
height: 28,
child: FlowyButton(
text: FlowyText(
lineHeight: 1.0,
LocaleKeys.grid_settings_addFilter.tr(),
color: AFThemeExtension.of(context).textColor,
),

View File

@ -23,6 +23,7 @@ class GridAddRowButton extends StatelessWidget {
),
),
text: FlowyText(
lineHeight: 1.0,
LocaleKeys.grid_row_newRow.tr(),
color: Theme.of(context).hintColor,
),

View File

@ -229,6 +229,7 @@ class FieldCellButton extends StatelessWidget {
radius: radius,
text: FlowyText.medium(
field.name,
lineHeight: 1.0,
maxLines: maxLines,
overflow: TextOverflow.ellipsis,
color: AFThemeExtension.of(context).textColor,

View File

@ -195,6 +195,7 @@ class _CreateFieldButtonState extends State<CreateFieldButton> {
margin: GridSize.cellContentInsets,
radius: BorderRadius.zero,
text: FlowyText(
lineHeight: 1.0,
LocaleKeys.grid_field_newProperty.tr(),
overflow: TextOverflow.ellipsis,
),

View File

@ -51,7 +51,11 @@ class RowActionMenu extends StatelessWidget {
return SizedBox(
height: GridSize.popoverItemHeight,
child: FlowyButton(
text: FlowyText.medium(action.text, overflow: TextOverflow.ellipsis),
text: FlowyText.medium(
action.text,
overflow: TextOverflow.ellipsis,
lineHeight: 1.0,
),
onTap: () {
if (action == RowAction.delete) {
NavigatorOkCancelDialog(

View File

@ -120,6 +120,7 @@ class GridSortPropertyCell extends StatelessWidget {
hoverColor: AFThemeExtension.of(context).lightGreyHover,
text: FlowyText.medium(
fieldInfo.name,
lineHeight: 1.0,
color: AFThemeExtension.of(context).textColor,
),
onTap: onTap,

View File

@ -34,6 +34,7 @@ class SortChoiceButton extends StatelessWidget {
useIntrinsicWidth: true,
text: FlowyText(
text,
lineHeight: 1.0,
color: AFThemeExtension.of(context).textColor,
overflow: TextOverflow.ellipsis,
),

View File

@ -222,6 +222,7 @@ class TabBarItemButton extends StatelessWidget {
),
text: FlowyText(
view.name,
lineHeight: 1.0,
fontSize: FontSizes.s11,
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,

View File

@ -158,6 +158,7 @@ class _EditFieldButton extends StatelessWidget {
child: FlowyButton(
leftIcon: const FlowySvg(FlowySvgs.edit_s),
text: FlowyText.medium(
lineHeight: 1.0,
LocaleKeys.grid_field_editProperty.tr(),
),
onTap: onTap,
@ -193,6 +194,7 @@ class FieldActionCell extends StatelessWidget {
disable: !enable,
text: FlowyText.medium(
action.title(fieldInfo),
lineHeight: 1.0,
color: enable ? null : Theme.of(context).disabledColor,
),
onHover: (_) => popoverMutex?.close(),
@ -613,6 +615,7 @@ class _SwitchFieldButtonState extends State<SwitchFieldButton> {
},
text: FlowyText.medium(
state.field.fieldType.i18n,
lineHeight: 1.0,
color: isPrimary ? Theme.of(context).disabledColor : null,
),
leftIcon: FlowySvg(

View File

@ -75,9 +75,7 @@ class FieldTypeCell extends StatelessWidget {
return SizedBox(
height: GridSize.popoverItemHeight,
child: FlowyButton(
text: FlowyText.medium(
fieldType.i18n,
),
text: FlowyText.medium(fieldType.i18n, lineHeight: 1.0),
onTap: () => onSelectField(fieldType),
leftIcon: FlowySvg(
fieldType.svgData,

View File

@ -23,7 +23,10 @@ class DateFormatButton extends StatelessWidget {
return SizedBox(
height: GridSize.popoverItemHeight,
child: FlowyButton(
text: FlowyText.medium(LocaleKeys.grid_field_dateFormat.tr()),
text: FlowyText.medium(
LocaleKeys.grid_field_dateFormat.tr(),
lineHeight: 1.0,
),
onTap: onTap,
onHover: onHover,
rightIcon: const FlowySvg(FlowySvgs.more_s),
@ -47,7 +50,10 @@ class TimeFormatButton extends StatelessWidget {
return SizedBox(
height: GridSize.popoverItemHeight,
child: FlowyButton(
text: FlowyText.medium(LocaleKeys.grid_field_timeFormat.tr()),
text: FlowyText.medium(
LocaleKeys.grid_field_timeFormat.tr(),
lineHeight: 1.0,
),
onTap: onTap,
onHover: onHover,
rightIcon: const FlowySvg(FlowySvgs.more_s),
@ -114,7 +120,10 @@ class DateFormatCell extends StatelessWidget {
return SizedBox(
height: GridSize.popoverItemHeight,
child: FlowyButton(
text: FlowyText.medium(dateFormat.title()),
text: FlowyText.medium(
dateFormat.title(),
lineHeight: 1.0,
),
rightIcon: checkmark,
onTap: () => onSelected(dateFormat),
),
@ -199,7 +208,10 @@ class TimeFormatCell extends StatelessWidget {
return SizedBox(
height: GridSize.popoverItemHeight,
child: FlowyButton(
text: FlowyText.medium(timeFormat.title()),
text: FlowyText.medium(
timeFormat.title(),
lineHeight: 1.0,
),
rightIcon: checkmark,
onTap: () => onSelected(timeFormat),
),

View File

@ -32,6 +32,7 @@ class NumberTypeOptionEditorFactory implements TypeOptionEditorFactory {
child: FlowyButton(
rightIcon: const FlowySvg(FlowySvgs.more_s),
text: FlowyText.medium(
lineHeight: 1.0,
typeOption.format.title(),
),
),
@ -167,7 +168,10 @@ class NumberFormatCell extends StatelessWidget {
return SizedBox(
height: GridSize.popoverItemHeight,
child: FlowyButton(
text: FlowyText.medium(format.title()),
text: FlowyText.medium(
format.title(),
lineHeight: 1.0,
),
onTap: () => onSelected(format),
rightIcon: checkmark,
),

View File

@ -61,6 +61,7 @@ class RelationTypeOptionEditorFactory implements TypeOptionEditorFactory {
(meta) => meta.databaseId == typeOption.databaseId,
);
return FlowyText(
lineHeight: 1.0,
databaseMeta == null
? LocaleKeys
.grid_relation_relatedDatabasePlaceholder
@ -134,6 +135,7 @@ class _DatabaseList extends StatelessWidget {
child: FlowyButton(
onTap: () => onSelectDatabase(meta.databaseId),
text: FlowyText.medium(
lineHeight: 1.0,
meta.databaseName,
overflow: TextOverflow.ellipsis,
),

View File

@ -181,6 +181,7 @@ class _AddOptionButton extends StatelessWidget {
height: GridSize.popoverItemHeight,
child: FlowyButton(
text: FlowyText.medium(
lineHeight: 1.0,
LocaleKeys.grid_field_addSelectOption.tr(),
),
onTap: () {

View File

@ -107,6 +107,7 @@ class _DeleteTag extends StatelessWidget {
height: GridSize.popoverItemHeight,
child: FlowyButton(
text: FlowyText.medium(
lineHeight: 1.0,
LocaleKeys.grid_selectOption_deleteTag.tr(),
),
leftIcon: const FlowySvg(FlowySvgs.delete_s),
@ -230,6 +231,7 @@ class _SelectOptionColorCell extends StatelessWidget {
child: FlowyButton(
hoverColor: AFThemeExtension.of(context).lightGreyHover,
text: FlowyText.medium(
lineHeight: 1.0,
color.colorName(),
color: AFThemeExtension.of(context).textColor,
),

View File

@ -97,7 +97,12 @@ class SelectLanguageButton extends StatelessWidget {
Widget build(BuildContext context) {
return SizedBox(
height: 30,
child: FlowyButton(text: FlowyText(language)),
child: FlowyButton(
text: FlowyText(
language,
lineHeight: 1.0,
),
),
);
}
}
@ -159,7 +164,10 @@ class LanguageCell extends StatelessWidget {
return SizedBox(
height: GridSize.popoverItemHeight,
child: FlowyButton(
text: FlowyText.medium(languageTypeToLanguage(languageType)),
text: FlowyText.medium(
languageTypeToLanguage(languageType),
lineHeight: 1.0,
),
rightIcon: checkmark,
onTap: () => onSelected(languageType),
),

View File

@ -193,6 +193,7 @@ class _GridGroupCell extends StatelessWidget {
text: FlowyText.medium(
name,
color: AFThemeExtension.of(context).textColor,
lineHeight: 1.0,
),
leftIcon: icon != null
? FlowySvg(

View File

@ -50,7 +50,10 @@ class RowDetailPageDeleteButton extends StatelessWidget {
return SizedBox(
height: GridSize.popoverItemHeight,
child: FlowyButton(
text: FlowyText.regular(LocaleKeys.grid_row_delete.tr()),
text: FlowyText.regular(
LocaleKeys.grid_row_delete.tr(),
lineHeight: 1.0,
),
leftIcon: const FlowySvg(FlowySvgs.trash_m),
onTap: () {
RowBackendService.deleteRows(viewId, [rowId]);
@ -76,7 +79,10 @@ class RowDetailPageDuplicateButton extends StatelessWidget {
return SizedBox(
height: GridSize.popoverItemHeight,
child: FlowyButton(
text: FlowyText.regular(LocaleKeys.grid_row_duplicate.tr()),
text: FlowyText.regular(
LocaleKeys.grid_row_duplicate.tr(),
lineHeight: 1.0,
),
leftIcon: const FlowySvg(FlowySvgs.copy_s),
onTap: () {
RowBackendService.duplicateRow(viewId, rowId);

View File

@ -220,6 +220,7 @@ class AddEmojiButton extends StatelessWidget {
child: FlowyButton(
useIntrinsicWidth: true,
text: FlowyText.medium(
lineHeight: 1.0,
LocaleKeys.document_plugins_cover_addIcon.tr(),
),
leftIcon: const FlowySvg(FlowySvgs.emoji_s),
@ -242,6 +243,7 @@ class RemoveEmojiButton extends StatelessWidget {
child: FlowyButton(
useIntrinsicWidth: true,
text: FlowyText.medium(
lineHeight: 1.0,
LocaleKeys.document_plugins_cover_removeIcon.tr(),
),
leftIcon: const FlowySvg(FlowySvgs.emoji_s),

View File

@ -294,7 +294,11 @@ class ToggleHiddenFieldsVisibilityButton extends StatelessWidget {
return SizedBox(
height: 30,
child: FlowyButton(
text: FlowyText.medium(text, color: Theme.of(context).hintColor),
text: FlowyText.medium(
text,
lineHeight: 1.0,
color: Theme.of(context).hintColor,
),
hoverColor: AFThemeExtension.of(context).lightGreyHover,
leftIcon: RotatedBox(
quarterTurns: quarterTurns,
@ -381,6 +385,7 @@ class _CreateRowFieldButtonState extends State<CreateRowFieldButton> {
child: FlowyButton(
margin: const EdgeInsets.symmetric(horizontal: 4, vertical: 6),
text: FlowyText.medium(
lineHeight: 1.0,
LocaleKeys.grid_field_newProperty.tr(),
color: Theme.of(context).hintColor,
),

View File

@ -80,6 +80,7 @@ class DatabaseViewLayoutCell extends StatelessWidget {
child: FlowyButton(
hoverColor: AFThemeExtension.of(context).lightGreyHover,
text: FlowyText.medium(
lineHeight: 1.0,
databaseLayout.layoutName,
color: AFThemeExtension.of(context).textColor,
),

View File

@ -82,6 +82,7 @@ extension DatabaseSettingActionExtension on DatabaseSettingAction {
hoverColor: AFThemeExtension.of(context).lightGreyHover,
text: FlowyText.medium(
title(),
lineHeight: 1.0,
color: AFThemeExtension.of(context).textColor,
),
leftIcon: FlowySvg(

View File

@ -150,6 +150,7 @@ class _DatabasePropertyCellState extends State<DatabasePropertyCell> {
child: FlowyButton(
hoverColor: AFThemeExtension.of(context).lightGreyHover,
text: FlowyText.medium(
lineHeight: 1.0,
widget.fieldInfo.name,
color: AFThemeExtension.of(context).textColor,
),

View File

@ -53,7 +53,10 @@ class SelectableItem extends StatelessWidget {
return SizedBox(
height: 32,
child: FlowyButton(
text: FlowyText.medium(item),
text: FlowyText.medium(
item,
lineHeight: 1.0,
),
rightIcon: isSelected ? const FlowySvg(FlowySvgs.check_s) : null,
onTap: onTap,
),

View File

@ -163,7 +163,6 @@ class _FileUploadLocalState extends State<_FileUploadLocal> {
fontSize: 16,
maxLines: 2,
textAlign: TextAlign.center,
lineHeight: 1.5,
),
],
],

View File

@ -247,6 +247,7 @@ class _CoverImagePreviewWidgetState extends State<CoverImagePreviewWidget> {
size: Size(20, 20),
),
text: FlowyText(
lineHeight: 1.0,
LocaleKeys.document_plugins_cover_pickFromFiles.tr(),
),
),

View File

@ -161,7 +161,10 @@ class _ExportButton extends StatelessWidget {
borderRadius: radius,
),
radius: radius,
text: FlowyText(title),
text: FlowyText(
title,
lineHeight: 1.0,
),
leftIcon: FlowySvg(svg),
onTap: onTap,
);

View File

@ -173,6 +173,7 @@ class _PublishedWidgetState extends State<_PublishedWidget> {
),
radius: BorderRadius.circular(10),
text: FlowyText.regular(
lineHeight: 1.0,
LocaleKeys.shareAction_unPublish.tr(),
textAlign: TextAlign.center,
),

View File

@ -101,7 +101,10 @@ class _TrashPageState extends State<TrashPage> {
const Spacer(),
IntrinsicWidth(
child: FlowyButton(
text: FlowyText.medium(LocaleKeys.trash_restoreAll.tr()),
text: FlowyText.medium(
LocaleKeys.trash_restoreAll.tr(),
lineHeight: 1.0,
),
leftIcon: const FlowySvg(FlowySvgs.restore_s),
onTap: () {
NavigatorAlertDialog(
@ -118,7 +121,10 @@ class _TrashPageState extends State<TrashPage> {
const HSpace(6),
IntrinsicWidth(
child: FlowyButton(
text: FlowyText.medium(LocaleKeys.trash_deleteAll.tr()),
text: FlowyText.medium(
LocaleKeys.trash_deleteAll.tr(),
lineHeight: 1.0,
),
leftIcon: const FlowySvg(FlowySvgs.delete_s),
onTap: () {
NavigatorAlertDialog(

View File

@ -257,6 +257,7 @@ class ViewMoreActionTypeWrapper extends CustomActionCell {
textBuilder: (onHover) => FlowyText.regular(
inner.name,
fontSize: 14.0,
lineHeight: 1.0,
figmaLineHeight: 18.0,
color: inner == ViewMoreActionType.delete && onHover
? Theme.of(context).colorScheme.error

View File

@ -204,7 +204,6 @@ class _UpgradeToAILocalPlanState extends State<_UpgradeToAILocalPlan> {
FlowyText.medium(
LocaleKeys.sideBar_upgradeToAILocal.tr(),
maxLines: 10,
lineHeight: 1.5,
),
const VSpace(4),
Opacity(
@ -213,7 +212,6 @@ class _UpgradeToAILocalPlanState extends State<_UpgradeToAILocalPlan> {
LocaleKeys.sideBar_upgradeToAILocalDesc.tr(),
fontSize: 12,
maxLines: 10,
lineHeight: 1.5,
),
),
],

View File

@ -363,7 +363,6 @@ class _CurrentPathState extends State<_CurrentPath> {
resetHoverOnRebuild: false,
builder: (_, isHovering) => FlowyText.regular(
widget.path,
lineHeight: 1.5,
maxLines: 2,
overflow: TextOverflow.ellipsis,
decoration: isHovering ? TextDecoration.underline : null,

View File

@ -626,7 +626,6 @@ class _Heading extends StatelessWidget {
description!,
fontSize: 12,
maxLines: 5,
lineHeight: 1.5,
),
),
],

View File

@ -42,7 +42,7 @@ class FlowyText extends StatelessWidget {
this.fontFamily,
this.fallbackFontFamily,
// https://api.flutter.dev/flutter/painting/TextStyle/height.html
this.lineHeight = 1,
this.lineHeight = 1.5,
this.figmaLineHeight,
this.withTooltip = false,
this.isEmoji = false,
@ -61,7 +61,7 @@ class FlowyText extends StatelessWidget {
this.selectable = false,
this.fontFamily,
this.fallbackFontFamily,
this.lineHeight,
this.lineHeight = 1.5,
this.withTooltip = false,
this.isEmoji = false,
this.strutStyle,
@ -82,7 +82,7 @@ class FlowyText extends StatelessWidget {
this.selectable = false,
this.fontFamily,
this.fallbackFontFamily,
this.lineHeight,
this.lineHeight = 1.5,
this.withTooltip = false,
this.isEmoji = false,
this.strutStyle,
@ -102,7 +102,7 @@ class FlowyText extends StatelessWidget {
this.selectable = false,
this.fontFamily,
this.fallbackFontFamily,
this.lineHeight = 1,
this.lineHeight = 1.5,
this.withTooltip = false,
this.isEmoji = false,
this.strutStyle,
@ -122,7 +122,7 @@ class FlowyText extends StatelessWidget {
this.selectable = false,
this.fontFamily,
this.fallbackFontFamily,
this.lineHeight = 1,
this.lineHeight = 1.5,
this.withTooltip = false,
this.isEmoji = false,
this.strutStyle,

View File

@ -1510,10 +1510,10 @@ packages:
dependency: transitive
description:
name: pdf
sha256: "81d5522bddc1ef5c28e8f0ee40b71708761753c163e0c93a40df56fd515ea0f0"
sha256: "05df53f8791587402493ac97b9869d3824eccbc77d97855f4545cf72df3cae07"
url: "https://pub.dev"
source: hosted
version: "3.11.0"
version: "3.11.1"
pdf_widget_wrapper:
dependency: transitive
description:
@ -1694,10 +1694,10 @@ packages:
dependency: transitive
description:
name: qr
sha256: "64957a3930367bf97cc211a5af99551d630f2f4625e38af10edd6b19131b64b3"
sha256: "5a1d2586170e172b8a8c8470bbbffd5eb0cd38a66c0d77155ea138d3af3a4445"
url: "https://pub.dev"
source: hosted
version: "3.0.1"
version: "3.0.2"
realtime_client:
dependency: transitive
description:

View File

@ -144,12 +144,19 @@ impl AIManager {
chat_id: &str,
message: &str,
message_type: ChatMessageType,
text_stream_port: i64,
answer_stream_port: i64,
question_stream_port: i64,
metadata: Vec<ChatMessageMetadata>,
) -> Result<ChatMessagePB, FlowyError> {
let chat = self.get_or_create_chat_instance(chat_id).await?;
let question = chat
.stream_chat_message(message, message_type, text_stream_port, metadata)
.stream_chat_message(
message,
message_type,
answer_stream_port,
question_stream_port,
metadata,
)
.await?;
Ok(question)
}

View File

@ -5,6 +5,7 @@ use crate::entities::{
use crate::middleware::chat_service_mw::AICloudServiceMiddleware;
use crate::notification::{make_notification, ChatNotification};
use crate::persistence::{insert_chat_messages, select_chat_messages, ChatMessageTable};
use crate::stream_message::StreamMessage;
use allo_isolate::Isolate;
use flowy_ai_pub::cloud::{
ChatCloudService, ChatMessage, ChatMessageMetadata, ChatMessageType, MessageCursor,
@ -81,7 +82,8 @@ impl Chat {
&self,
message: &str,
message_type: ChatMessageType,
text_stream_port: i64,
answer_stream_port: i64,
question_stream_port: i64,
metadata: Vec<ChatMessageMetadata>,
) -> Result<ChatMessagePB, FlowyError> {
if message.len() > 2000 {
@ -93,10 +95,19 @@ impl Chat {
.store(false, std::sync::atomic::Ordering::SeqCst);
self.stream_buffer.lock().await.clear();
let stream_buffer = self.stream_buffer.clone();
let mut question_sink = IsolateSink::new(Isolate::new(question_stream_port));
let answer_stream_buffer = self.stream_buffer.clone();
let uid = self.user_service.user_id()?;
let workspace_id = self.user_service.workspace_id()?;
let _ = question_sink
.send(
StreamMessage::Text {
text: message.to_string(),
}
.to_string(),
)
.await;
let question = self
.chat_service
.create_question(
@ -112,15 +123,31 @@ impl Chat {
FlowyError::server_error()
})?;
if self.chat_service.is_local_ai_enabled() {
let _ = question_sink
.send(
StreamMessage::MessageId {
message_id: question.message_id,
}
.to_string(),
)
.await;
if self.chat_service.is_local_ai_enabled() && !metadata.is_empty() {
let _ = question_sink
.send(StreamMessage::IndexStart.to_string())
.await;
if let Err(err) = self
.chat_service
.index_message_metadata(&self.chat_id, &metadata)
.index_message_metadata(&self.chat_id, &metadata, &mut question_sink)
.await
{
error!("Failed to index file: {}", err);
}
let _ = question_sink
.send(StreamMessage::IndexEnd.to_string())
.await;
}
let _ = question_sink.send(StreamMessage::Done.to_string()).await;
save_chat_message(
self.user_service.sqlite_connection(uid)?,
@ -134,7 +161,7 @@ impl Chat {
let cloud_service = self.chat_service.clone();
let user_service = self.user_service.clone();
tokio::spawn(async move {
let mut text_sink = IsolateSink::new(Isolate::new(text_stream_port));
let mut answer_sink = IsolateSink::new(Isolate::new(answer_stream_port));
match cloud_service
.stream_answer(&workspace_id, &chat_id, question_id)
.await
@ -149,20 +176,20 @@ impl Chat {
}
match message {
QuestionStreamValue::Answer { value } => {
stream_buffer.lock().await.push_str(&value);
let _ = text_sink.send(format!("data:{}", value)).await;
answer_stream_buffer.lock().await.push_str(&value);
let _ = answer_sink.send(format!("data:{}", value)).await;
},
QuestionStreamValue::Metadata { value } => {
if let Ok(s) = serde_json::to_string(&value) {
stream_buffer.lock().await.set_metadata(value);
let _ = text_sink.send(format!("metadata:{}", s)).await;
answer_stream_buffer.lock().await.set_metadata(value);
let _ = answer_sink.send(format!("metadata:{}", s)).await;
}
},
}
},
Err(err) => {
error!("[Chat] failed to stream answer: {}", err);
let _ = text_sink.send(format!("error:{}", err)).await;
let _ = answer_sink.send(format!("error:{}", err)).await;
let pb = ChatMessageErrorPB {
chat_id: chat_id.clone(),
error_message: err.to_string(),
@ -178,9 +205,9 @@ impl Chat {
Err(err) => {
error!("[Chat] failed to stream answer: {}", err);
if err.is_ai_response_limit_exceeded() {
let _ = text_sink.send("AI_RESPONSE_LIMIT".to_string()).await;
let _ = answer_sink.send("AI_RESPONSE_LIMIT".to_string()).await;
} else {
let _ = text_sink.send(format!("error:{}", err)).await;
let _ = answer_sink.send(format!("error:{}", err)).await;
}
let pb = ChatMessageErrorPB {
@ -195,11 +222,11 @@ impl Chat {
}
make_notification(&chat_id, ChatNotification::FinishStreaming).send();
if stream_buffer.lock().await.is_empty() {
if answer_stream_buffer.lock().await.is_empty() {
return Ok(());
}
let content = stream_buffer.lock().await.take_content();
let metadata = stream_buffer.lock().await.take_metadata();
let content = answer_stream_buffer.lock().await.take_content();
let metadata = answer_stream_buffer.lock().await.take_metadata();
let answer = cloud_service
.create_answer(&workspace_id, &chat_id, &content, question_id, metadata)

View File

@ -61,9 +61,12 @@ pub struct StreamChatPayloadPB {
pub message_type: ChatMessageTypePB,
#[pb(index = 4)]
pub text_stream_port: i64,
pub answer_stream_port: i64,
#[pb(index = 5)]
pub question_stream_port: i64,
#[pb(index = 6)]
pub metadata: Vec<ChatMessageMetaPB>,
}

View File

@ -72,7 +72,8 @@ pub(crate) async fn stream_chat_message_handler(
&data.chat_id,
&data.message,
message_type,
data.text_stream_port,
data.answer_stream_port,
data.question_stream_port,
metadata,
)
.await;

View File

@ -10,3 +10,4 @@ mod middleware;
pub mod notification;
mod persistence;
mod protobuf;
mod stream_message;

View File

@ -16,11 +16,13 @@ use futures::Sink;
use lib_infra::async_trait::async_trait;
use std::collections::HashMap;
use crate::stream_message::StreamMessage;
use futures_util::SinkExt;
use parking_lot::Mutex;
use serde::{Deserialize, Serialize};
use serde_json::json;
use std::ops::Deref;
use std::path::Path;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use tokio::select;
use tokio_stream::StreamExt;
@ -339,11 +341,11 @@ impl LocalAIController {
.set_bool(APPFLOWY_LOCAL_AI_CHAT_RAG_ENABLED, enabled)?;
Ok(enabled)
}
pub async fn index_message_metadata(
&self,
chat_id: &str,
metadata_list: &[ChatMessageMetadata],
index_process_sink: &mut (impl Sink<String> + Unpin),
) -> FlowyResult<()> {
for metadata in metadata_list {
let mut index_metadata = HashMap::new();
@ -351,52 +353,94 @@ impl LocalAIController {
index_metadata.insert("at_name".to_string(), json!(format!("@{}", &metadata.name)));
index_metadata.insert("source".to_string(), json!(&metadata.source));
match &metadata.data.url {
None => match &metadata.data.content_type {
ChatMetadataContentType::Text | ChatMetadataContentType::Markdown => {
if metadata.data.validate() {
if let Err(err) = self
.index_file(
chat_id,
None,
Some(metadata.data.content.clone()),
Some(index_metadata),
)
.await
{
error!("[AI Plugin] failed to index file: {:?}", err);
}
}
},
_ => {
error!(
"[AI Plugin] unsupported content type: {:?}",
metadata.data.content_type
);
},
},
Some(url) => {
let file_path = Path::new(url);
if file_path.exists() {
if let Err(err) = self
.index_file(
chat_id,
Some(file_path.to_path_buf()),
None,
Some(index_metadata),
)
.await
{
error!("[AI Plugin] failed to index file: {:?}", err);
}
}
},
if let Some(url) = &metadata.data.url {
let file_path = Path::new(url);
if file_path.exists() {
self
.process_index_file(
chat_id,
Some(file_path.to_path_buf()),
None,
metadata,
&index_metadata,
index_process_sink,
)
.await?;
}
} else if matches!(
metadata.data.content_type,
ChatMetadataContentType::Text | ChatMetadataContentType::Markdown
) && metadata.data.validate()
{
self
.process_index_file(
chat_id,
None,
Some(metadata.data.content.clone()),
metadata,
&index_metadata,
index_process_sink,
)
.await?;
} else {
error!(
"[AI Plugin] unsupported content type: {:?}",
metadata.data.content_type
);
}
}
Ok(())
}
async fn process_index_file(
&self,
chat_id: &str,
file_path: Option<PathBuf>,
content: Option<String>,
metadata: &ChatMessageMetadata,
index_metadata: &HashMap<String, serde_json::Value>,
index_process_sink: &mut (impl Sink<String> + Unpin),
) -> Result<(), FlowyError> {
let _ = index_process_sink
.send(
StreamMessage::StartIndexFile {
file_name: metadata.name.clone(),
}
.to_string(),
)
.await;
let result = self
.index_file(chat_id, file_path, content, Some(index_metadata.clone()))
.await;
match result {
Ok(_) => {
let _ = index_process_sink
.send(
StreamMessage::EndIndexFile {
file_name: metadata.name.clone(),
}
.to_string(),
)
.await;
},
Err(err) => {
let _ = index_process_sink
.send(
StreamMessage::IndexFileError {
file_name: metadata.name.clone(),
}
.to_string(),
)
.await;
error!("[AI Plugin] failed to index file: {:?}", err);
},
}
Ok(())
}
async fn enable_chat_plugin(&self, enabled: bool) -> FlowyResult<()> {
info!("[AI Plugin] enable chat plugin: {}", enabled);
if enabled {

View File

@ -11,7 +11,7 @@ use flowy_ai_pub::cloud::{
RepeatedRelatedQuestion, StreamAnswer, StreamComplete,
};
use flowy_error::{FlowyError, FlowyResult};
use futures::{stream, StreamExt, TryStreamExt};
use futures::{stream, Sink, StreamExt, TryStreamExt};
use lib_infra::async_trait::async_trait;
use lib_infra::future::FutureResult;
@ -48,10 +48,11 @@ impl AICloudServiceMiddleware {
&self,
chat_id: &str,
metadata_list: &[ChatMessageMetadata],
index_process_sink: &mut (impl Sink<String> + Unpin),
) -> Result<(), FlowyError> {
self
.local_llm_controller
.index_message_metadata(chat_id, metadata_list)
.index_message_metadata(chat_id, metadata_list, index_process_sink)
.await?;
Ok(())
}

View File

@ -0,0 +1,34 @@
use std::fmt::Display;
pub enum StreamMessage {
MessageId { message_id: i64 },
IndexStart,
IndexEnd,
Text { text: String },
Done,
StartIndexFile { file_name: String },
EndIndexFile { file_name: String },
IndexFileError { file_name: String },
}
impl Display for StreamMessage {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
StreamMessage::MessageId { message_id } => write!(f, "message_id:{}", message_id),
StreamMessage::IndexStart => write!(f, "index_start:"),
StreamMessage::IndexEnd => write!(f, "index_end"),
StreamMessage::Text { text } => {
write!(f, "data:{}", text)
},
StreamMessage::Done => write!(f, "done:"),
StreamMessage::StartIndexFile { file_name } => {
write!(f, "start_index_file:{}", file_name)
},
StreamMessage::EndIndexFile { file_name } => {
write!(f, "end_index_file:{}", file_name)
},
StreamMessage::IndexFileError { file_name } => {
write!(f, "index_file_error:{}", file_name)
},
}
}
}