mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
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:
parent
758c304a74
commit
7abe9f4661
@ -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,
|
||||
),
|
||||
|
@ -176,6 +176,7 @@ class DatabaseViewSettingTile extends StatelessWidget {
|
||||
return Row(
|
||||
children: [
|
||||
FlowyText(
|
||||
lineHeight: 1.0,
|
||||
databaseLayoutFromViewLayout(view.layout).layoutName,
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
|
@ -29,6 +29,7 @@ class FontSetting extends StatelessWidget {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
FlowyText(
|
||||
lineHeight: 1.0,
|
||||
name,
|
||||
color: theme.colorScheme.onSurface,
|
||||
),
|
||||
|
@ -38,6 +38,7 @@ class _LanguageSettingGroupState extends State<LanguageSettingGroup> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
FlowyText(
|
||||
lineHeight: 1.0,
|
||||
languageFromLocale(locale),
|
||||
color: theme.colorScheme.onSurface,
|
||||
),
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -49,6 +49,8 @@ class AIMessageMetadata extends StatelessWidget {
|
||||
child: FlowyText(
|
||||
m.name,
|
||||
fontSize: 14,
|
||||
lineHeight: 1.0,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
onTap: () => onSelectedMetadata(m),
|
||||
|
@ -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: () {
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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: () {
|
||||
|
@ -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,
|
||||
),
|
||||
|
@ -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,
|
||||
|
@ -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();
|
||||
|
@ -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,
|
||||
),
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
),
|
||||
|
@ -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,
|
||||
),
|
||||
|
@ -23,6 +23,7 @@ class GridAddRowButton extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
text: FlowyText(
|
||||
lineHeight: 1.0,
|
||||
LocaleKeys.grid_row_newRow.tr(),
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
),
|
||||
|
@ -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(
|
||||
|
@ -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,
|
||||
|
@ -34,6 +34,7 @@ class SortChoiceButton extends StatelessWidget {
|
||||
useIntrinsicWidth: true,
|
||||
text: FlowyText(
|
||||
text,
|
||||
lineHeight: 1.0,
|
||||
color: AFThemeExtension.of(context).textColor,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
|
@ -222,6 +222,7 @@ class TabBarItemButton extends StatelessWidget {
|
||||
),
|
||||
text: FlowyText(
|
||||
view.name,
|
||||
lineHeight: 1.0,
|
||||
fontSize: FontSizes.s11,
|
||||
textAlign: TextAlign.center,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
|
@ -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(
|
||||
|
@ -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,
|
||||
|
@ -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),
|
||||
),
|
||||
|
@ -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,
|
||||
),
|
||||
|
@ -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,
|
||||
),
|
||||
|
@ -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: () {
|
||||
|
@ -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,
|
||||
),
|
||||
|
@ -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),
|
||||
),
|
||||
|
@ -193,6 +193,7 @@ class _GridGroupCell extends StatelessWidget {
|
||||
text: FlowyText.medium(
|
||||
name,
|
||||
color: AFThemeExtension.of(context).textColor,
|
||||
lineHeight: 1.0,
|
||||
),
|
||||
leftIcon: icon != null
|
||||
? FlowySvg(
|
||||
|
@ -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);
|
||||
|
@ -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),
|
||||
|
@ -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,
|
||||
),
|
||||
|
@ -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,
|
||||
),
|
||||
|
@ -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(
|
||||
|
@ -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,
|
||||
),
|
||||
|
@ -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,
|
||||
),
|
||||
|
@ -163,7 +163,6 @@ class _FileUploadLocalState extends State<_FileUploadLocal> {
|
||||
fontSize: 16,
|
||||
maxLines: 2,
|
||||
textAlign: TextAlign.center,
|
||||
lineHeight: 1.5,
|
||||
),
|
||||
],
|
||||
],
|
||||
|
@ -247,6 +247,7 @@ class _CoverImagePreviewWidgetState extends State<CoverImagePreviewWidget> {
|
||||
size: Size(20, 20),
|
||||
),
|
||||
text: FlowyText(
|
||||
lineHeight: 1.0,
|
||||
LocaleKeys.document_plugins_cover_pickFromFiles.tr(),
|
||||
),
|
||||
),
|
||||
|
@ -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,
|
||||
);
|
||||
|
@ -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,
|
||||
),
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
@ -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,
|
||||
|
@ -626,7 +626,6 @@ class _Heading extends StatelessWidget {
|
||||
description!,
|
||||
fontSize: 12,
|
||||
maxLines: 5,
|
||||
lineHeight: 1.5,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
@ -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,
|
||||
|
@ -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:
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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>,
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -10,3 +10,4 @@ mod middleware;
|
||||
pub mod notification;
|
||||
mod persistence;
|
||||
mod protobuf;
|
||||
mod stream_message;
|
||||
|
@ -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 {
|
||||
|
@ -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(())
|
||||
}
|
||||
|
34
frontend/rust-lib/flowy-ai/src/stream_message.rs
Normal file
34
frontend/rust-lib/flowy-ai/src/stream_message.rs
Normal 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)
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user