feat: support mention a document as context on local ai (#5913)

* feat: support mention a document as context on local ai

* chore: rename

* chore: fix test

* chore: fix test
This commit is contained in:
Nathan.fooo 2024-08-09 21:55:20 +08:00 committed by GitHub
parent f84473c857
commit 758c304a74
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
39 changed files with 721 additions and 672 deletions

View File

@ -1,6 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'package:appflowy/plugins/ai_chat/application/chat_bloc.dart'; 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/dispatch/dispatch.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart';

View File

@ -1,13 +1,11 @@
import 'dart:async'; import 'dart:async';
import 'dart:collection'; import 'dart:collection';
import 'dart:ffi';
import 'dart:isolate';
import 'package:appflowy/plugins/ai_chat/application/chat_message_stream.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-error/code.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-error/code.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
@ -18,18 +16,12 @@ import 'package:flutter_chat_types/flutter_chat_types.dart';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:nanoid/nanoid.dart'; import 'package:nanoid/nanoid.dart';
import 'chat_entity.dart';
import 'chat_message_listener.dart'; import 'chat_message_listener.dart';
import 'chat_message_service.dart'; import 'chat_message_service.dart';
part 'chat_bloc.g.dart';
part 'chat_bloc.freezed.dart'; part 'chat_bloc.freezed.dart';
const sendMessageErrorKey = "sendMessageError";
const systemUserId = "system";
const aiResponseUserId = "0";
const messageMetadataKey = "metadata";
const messageQuestionIdKey = "question";
class ChatBloc extends Bloc<ChatEvent, ChatState> { class ChatBloc extends Bloc<ChatEvent, ChatState> {
ChatBloc({ ChatBloc({
required ViewPB view, required ViewPB view,
@ -100,7 +92,7 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
_loadPrevMessage(beforeMessageId); _loadPrevMessage(beforeMessageId);
emit( emit(
state.copyWith( state.copyWith(
loadingPreviousStatus: const LoadingState.loading(), loadingPreviousStatus: const ChatLoadingState.loading(),
), ),
); );
}, },
@ -116,7 +108,7 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
emit( emit(
state.copyWith( state.copyWith(
messages: uniqueMessages, messages: uniqueMessages,
loadingPreviousStatus: const LoadingState.finish(), loadingPreviousStatus: const ChatLoadingState.finish(),
hasMorePrevMessage: hasMore, hasMorePrevMessage: hasMore,
), ),
); );
@ -130,7 +122,7 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
emit( emit(
state.copyWith( state.copyWith(
messages: uniqueMessages, messages: uniqueMessages,
initialLoadingStatus: const LoadingState.finish(), initialLoadingStatus: const ChatLoadingState.finish(),
), ),
); );
}, },
@ -200,6 +192,14 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
sendMessage: (String message, Map<String, dynamic>? metadata) async { sendMessage: (String message, Map<String, dynamic>? metadata) async {
unawaited(_startStreamingMessage(message, metadata, emit)); unawaited(_startStreamingMessage(message, metadata, emit));
final allMessages = _perminentMessages(); final allMessages = _perminentMessages();
// allMessages.insert(
// 0,
// CustomMessage(
// metadata: OnetimeShotType.sendingMessage.toMap(),
// author: User(id: state.userProfile.id.toString()),
// id: state.userProfile.id.toString(),
// ),
// );
emit( emit(
state.copyWith( state.copyWith(
lastSentMessage: null, lastSentMessage: null,
@ -406,7 +406,6 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
Message _createStreamMessage(AnswerStream stream, Int64 questionMessageId) { Message _createStreamMessage(AnswerStream stream, Int64 questionMessageId) {
final streamMessageId = (questionMessageId + 1).toString(); final streamMessageId = (questionMessageId + 1).toString();
lastStreamMessageId = streamMessageId; lastStreamMessageId = streamMessageId;
return TextMessage( return TextMessage(
@ -488,10 +487,10 @@ class ChatState with _$ChatState {
required UserProfilePB userProfile, required UserProfilePB userProfile,
// When opening the chat, the initial loading status will be set as loading. // When opening the chat, the initial loading status will be set as loading.
//After the initial loading is done, the status will be set as finished. //After the initial loading is done, the status will be set as finished.
required LoadingState initialLoadingStatus, required ChatLoadingState initialLoadingStatus,
// When loading previous messages, the status will be set as loading. // When loading previous messages, the status will be set as loading.
// After the loading is done, the status will be set as finished. // After the loading is done, the status will be set as finished.
required LoadingState loadingPreviousStatus, required ChatLoadingState loadingPreviousStatus,
// When sending a user message, the status will be set as loading. // When sending a user message, the status will be set as loading.
// After the message is sent, the status will be set as finished. // After the message is sent, the status will be set as finished.
required StreamingState streamingState, required StreamingState streamingState,
@ -511,8 +510,8 @@ class ChatState with _$ChatState {
view: view, view: view,
messages: [], messages: [],
userProfile: userProfile, userProfile: userProfile,
initialLoadingStatus: const LoadingState.finish(), initialLoadingStatus: const ChatLoadingState.finish(),
loadingPreviousStatus: const LoadingState.finish(), loadingPreviousStatus: const ChatLoadingState.finish(),
streamingState: const StreamingState.done(), streamingState: const StreamingState.done(),
sendingState: const SendMessageState.done(), sendingState: const SendMessageState.done(),
hasMorePrevMessage: true, hasMorePrevMessage: true,
@ -525,169 +524,3 @@ bool isOtherUserMessage(Message message) {
message.author.id != systemUserId && message.author.id != systemUserId &&
!message.author.id.startsWith("streamId:"); !message.author.id.startsWith("streamId:");
} }
@freezed
class LoadingState with _$LoadingState {
const factory LoadingState.loading() = _Loading;
const factory LoadingState.finish({FlowyError? error}) = _Finish;
}
enum OnetimeShotType {
unknown,
sendingMessage,
relatedQuestion,
invalidSendMesssage,
}
const onetimeShotType = "OnetimeShotType";
extension OnetimeMessageTypeExtension on OnetimeShotType {
static OnetimeShotType fromString(String value) {
switch (value) {
case 'OnetimeShotType.relatedQuestion':
return OnetimeShotType.relatedQuestion;
case 'OnetimeShotType.invalidSendMesssage':
return OnetimeShotType.invalidSendMesssage;
default:
Log.error('Unknown OnetimeShotType: $value');
return OnetimeShotType.unknown;
}
}
Map<String, dynamic> toMap() {
return {
onetimeShotType: toString(),
};
}
}
OnetimeShotType? onetimeMessageTypeFromMeta(Map<String, dynamic>? metadata) {
if (metadata == null) {
return null;
}
for (final entry in metadata.entries) {
if (entry.key == onetimeShotType) {
return OnetimeMessageTypeExtension.fromString(entry.value as String);
}
}
return null;
}
class AnswerStream {
AnswerStream() {
_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("error:")) {
_error = event.substring(5);
if (_onError != null) {
_onError!(_error!);
}
} else if (event.startsWith("metadata:")) {
if (_onMetadata != null) {
final s = event.substring(9);
_onMetadata!(messageRefSourceFromString(s));
}
} else if (event == "AI_RESPONSE_LIMIT") {
if (_onAIResponseLimit != null) {
_onAIResponseLimit!();
}
}
},
onDone: () {
if (_onEnd != null) {
_onEnd!();
}
},
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()? _onStart;
void Function()? _onEnd;
void Function(String error)? _onError;
void Function()? _onAIResponseLimit;
void Function(List<ChatMessageRefSource> metadata)? _onMetadata;
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()? onStart,
void Function()? onEnd,
void Function(String error)? onError,
void Function()? onAIResponseLimit,
void Function(List<ChatMessageRefSource> metadata)? onMetadata,
}) {
_onData = onData;
_onStart = onStart;
_onEnd = onEnd;
_onError = onError;
_onAIResponseLimit = onAIResponseLimit;
_onMetadata = onMetadata;
if (_onStart != null) {
_onStart!();
}
}
}
@JsonSerializable()
class ChatMessageRefSource {
ChatMessageRefSource({
required this.id,
required this.name,
required this.source,
});
factory ChatMessageRefSource.fromJson(Map<String, dynamic> json) =>
_$ChatMessageRefSourceFromJson(json);
final String id;
final String name;
final String source;
Map<String, dynamic> toJson() => _$ChatMessageRefSourceToJson(this);
}
@freezed
class StreamingState with _$StreamingState {
const factory StreamingState.streaming() = _Streaming;
const factory StreamingState.done({FlowyError? error}) = _StreamDone;
}
@freezed
class SendMessageState with _$SendMessageState {
const factory SendMessageState.sending() = _Sending;
const factory SendMessageState.done({FlowyError? error}) = _SendDone;
}

View File

@ -0,0 +1,176 @@
import 'dart:io';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-ai/entities.pbenum.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:path/path.dart' as path;
part 'chat_entity.g.dart';
part 'chat_entity.freezed.dart';
const sendMessageErrorKey = "sendMessageError";
const systemUserId = "system";
const aiResponseUserId = "0";
const messageMetadataKey = "metadata";
const messageQuestionIdKey = "question";
@JsonSerializable()
class ChatMessageRefSource {
ChatMessageRefSource({
required this.id,
required this.name,
required this.source,
});
factory ChatMessageRefSource.fromJson(Map<String, dynamic> json) =>
_$ChatMessageRefSourceFromJson(json);
final String id;
final String name;
final String source;
Map<String, dynamic> toJson() => _$ChatMessageRefSourceToJson(this);
}
@freezed
class StreamingState with _$StreamingState {
const factory StreamingState.streaming() = _Streaming;
const factory StreamingState.done({FlowyError? error}) = _StreamDone;
}
@freezed
class SendMessageState with _$SendMessageState {
const factory SendMessageState.sending() = _Sending;
const factory SendMessageState.done({FlowyError? error}) = _SendDone;
}
class ChatFile extends Equatable {
const ChatFile({
required this.filePath,
required this.fileName,
required this.fileType,
});
static ChatFile? fromFilePath(String filePath) {
final file = File(filePath);
if (!file.existsSync()) {
return null;
}
final fileName = path.basename(filePath);
final extension = path.extension(filePath).toLowerCase();
ChatMessageMetaTypePB fileType;
switch (extension) {
case '.pdf':
fileType = ChatMessageMetaTypePB.PDF;
break;
case '.txt':
fileType = ChatMessageMetaTypePB.Txt;
break;
case '.md':
fileType = ChatMessageMetaTypePB.Markdown;
break;
default:
fileType = ChatMessageMetaTypePB.UnknownMetaType;
}
return ChatFile(
filePath: filePath,
fileName: fileName,
fileType: fileType,
);
}
final String filePath;
final String fileName;
final ChatMessageMetaTypePB fileType;
@override
List<Object?> get props => [filePath];
}
extension ChatFileTypeExtension on ChatMessageMetaTypePB {
Widget get icon {
switch (this) {
case ChatMessageMetaTypePB.PDF:
return const FlowySvg(
FlowySvgs.file_pdf_s,
color: Color(0xff00BCF0),
);
case ChatMessageMetaTypePB.Txt:
return const FlowySvg(
FlowySvgs.file_txt_s,
color: Color(0xff00BCF0),
);
case ChatMessageMetaTypePB.Markdown:
return const FlowySvg(
FlowySvgs.file_md_s,
color: Color(0xff00BCF0),
);
default:
return const FlowySvg(FlowySvgs.file_unknown_s);
}
}
}
typedef ChatInputFileMetadata = Map<String, ChatFile>;
@freezed
class ChatLoadingState with _$ChatLoadingState {
const factory ChatLoadingState.loading() = _Loading;
const factory ChatLoadingState.finish({FlowyError? error}) = _Finish;
}
extension ChatLoadingStateExtension on ChatLoadingState {
bool get isLoading => this is _Loading;
bool get isFinish => this is _Finish;
}
enum OnetimeShotType {
unknown,
sendingMessage,
relatedQuestion,
invalidSendMesssage,
}
const onetimeShotType = "OnetimeShotType";
extension OnetimeMessageTypeExtension on OnetimeShotType {
static OnetimeShotType fromString(String value) {
switch (value) {
case 'OnetimeShotType.sendingMessage':
return OnetimeShotType.sendingMessage;
case 'OnetimeShotType.relatedQuestion':
return OnetimeShotType.relatedQuestion;
case 'OnetimeShotType.invalidSendMesssage':
return OnetimeShotType.invalidSendMesssage;
default:
Log.error('Unknown OnetimeShotType: $value');
return OnetimeShotType.unknown;
}
}
Map<String, dynamic> toMap() {
return {
onetimeShotType: toString(),
};
}
}
OnetimeShotType? onetimeMessageTypeFromMeta(Map<String, dynamic>? metadata) {
if (metadata == null) {
return null;
}
for (final entry in metadata.entries) {
if (entry.key == onetimeShotType) {
return OnetimeMessageTypeExtension.fromString(entry.value as String);
}
}
return null;
}

View File

@ -1,23 +1,17 @@
import 'dart:async'; import 'dart:async';
import 'dart:io';
import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/plugins/ai_chat/application/chat_entity.dart';
import 'package:appflowy/workspace/application/settings/ai/local_llm_listener.dart'; import 'package:appflowy/workspace/application/settings/ai/local_llm_listener.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:path/path.dart' as path;
import 'chat_input_bloc.dart'; import 'chat_input_bloc.dart';
part 'chat_file_bloc.freezed.dart'; part 'chat_file_bloc.freezed.dart';
typedef ChatInputFileMetadata = Map<String, ChatFile>;
class ChatFileBloc extends Bloc<ChatFileEvent, ChatFileState> { class ChatFileBloc extends Bloc<ChatFileEvent, ChatFileState> {
ChatFileBloc() ChatFileBloc()
: listener = LocalLLMListener(), : listener = LocalLLMListener(),
@ -158,64 +152,3 @@ class ChatFileState with _$ChatFileState {
@Default(AIType.appflowyAI()) AIType aiType, @Default(AIType.appflowyAI()) AIType aiType,
}) = _ChatFileState; }) = _ChatFileState;
} }
class ChatFile extends Equatable {
const ChatFile({
required this.filePath,
required this.fileName,
required this.fileType,
});
static ChatFile? fromFilePath(String filePath) {
final file = File(filePath);
if (!file.existsSync()) {
return null;
}
final fileName = path.basename(filePath);
final extension = path.extension(filePath).toLowerCase();
ChatMessageMetaTypePB fileType;
switch (extension) {
case '.pdf':
fileType = ChatMessageMetaTypePB.PDF;
break;
case '.txt':
fileType = ChatMessageMetaTypePB.Txt;
break;
case '.md':
fileType = ChatMessageMetaTypePB.Markdown;
break;
default:
fileType = ChatMessageMetaTypePB.UnknownMetaType;
}
return ChatFile(
filePath: filePath,
fileName: fileName,
fileType: fileType,
);
}
final String filePath;
final String fileName;
final ChatMessageMetaTypePB fileType;
@override
List<Object?> get props => [filePath];
}
extension ChatFileTypeExtension on ChatMessageMetaTypePB {
Widget get icon {
switch (this) {
case ChatMessageMetaTypePB.PDF:
return const FlowySvg(FlowySvgs.file_pdf_s);
case ChatMessageMetaTypePB.Txt:
return const FlowySvg(FlowySvgs.file_txt_s);
case ChatMessageMetaTypePB.Markdown:
return const FlowySvg(FlowySvgs.file_md_s);
default:
return const FlowySvg(FlowySvgs.file_unknown_s);
}
}
}

View File

@ -1,6 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'package:appflowy/plugins/ai_chat/application/chat_file_bloc.dart'; import 'package:appflowy/plugins/ai_chat/application/chat_entity.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';

View File

@ -1,6 +1,6 @@
import 'dart:convert'; import 'dart:convert';
import 'package:appflowy/plugins/ai_chat/application/chat_bloc.dart'; import 'package:appflowy/plugins/ai_chat/application/chat_entity.dart';
import 'package:appflowy/plugins/ai_chat/application/chat_input_action_bloc.dart'; import 'package:appflowy/plugins/ai_chat/application/chat_input_action_bloc.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart'; import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart';
@ -10,8 +10,6 @@ import 'package:appflowy_backend/protobuf/flowy-document/entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:nanoid/nanoid.dart'; import 'package:nanoid/nanoid.dart';
import 'chat_file_bloc.dart';
List<ChatFile> fileListFromMessageMetadata( List<ChatFile> fileListFromMessageMetadata(
Map<String, dynamic>? map, Map<String, dynamic>? map,
) { ) {
@ -117,7 +115,7 @@ Future<List<ChatMessageMetaPB>> metadataPBFromMetadata(
name: view.name, name: view.name,
data: pb.text, data: pb.text,
dataType: ChatMessageMetaTypePB.Txt, dataType: ChatMessageMetaTypePB.Txt,
source: "appflowy document", source: "appflowy",
), ),
); );
}, (err) { }, (err) {

View File

@ -0,0 +1,94 @@
import 'dart:async';
import 'dart:ffi';
import 'dart:isolate';
import 'package:appflowy/plugins/ai_chat/application/chat_entity.dart';
import 'package:appflowy/plugins/ai_chat/application/chat_message_service.dart';
class AnswerStream {
AnswerStream() {
_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("error:")) {
_error = event.substring(5);
if (_onError != null) {
_onError!(_error!);
}
} else if (event.startsWith("metadata:")) {
if (_onMetadata != null) {
final s = event.substring(9);
_onMetadata!(messageRefSourceFromString(s));
}
} else if (event == "AI_RESPONSE_LIMIT") {
if (_onAIResponseLimit != null) {
_onAIResponseLimit!();
}
}
},
onDone: () {
if (_onEnd != null) {
_onEnd!();
}
},
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()? _onStart;
void Function()? _onEnd;
void Function(String error)? _onError;
void Function()? _onAIResponseLimit;
void Function(List<ChatMessageRefSource> metadata)? _onMetadata;
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()? onStart,
void Function()? onEnd,
void Function(String error)? onError,
void Function()? onAIResponseLimit,
void Function(List<ChatMessageRefSource> metadata)? onMetadata,
}) {
_onData = onData;
_onStart = onStart;
_onEnd = onEnd;
_onError = onError;
_onAIResponseLimit = onAIResponseLimit;
_onMetadata = onMetadata;
if (_onStart != null) {
_onStart!();
}
}
}

View File

@ -1,6 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'package:appflowy/plugins/ai_chat/application/chat_bloc.dart'; import 'package:appflowy/plugins/ai_chat/application/chat_entity.dart';
import 'package:appflowy/workspace/application/view/view_service.dart'; import 'package:appflowy/workspace/application/view/view_service.dart';
import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';

View File

@ -1,8 +1,8 @@
import 'package:appflowy/plugins/ai_chat/application/chat_entity.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_chat_types/flutter_chat_types.dart'; import 'package:flutter_chat_types/flutter_chat_types.dart';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
import 'chat_file_bloc.dart';
import 'chat_message_service.dart'; import 'chat_message_service.dart';
part 'chat_user_message_bloc.freezed.dart'; part 'chat_user_message_bloc.freezed.dart';
@ -10,7 +10,7 @@ part 'chat_user_message_bloc.freezed.dart';
class ChatUserMessageBloc class ChatUserMessageBloc
extends Bloc<ChatUserMessageEvent, ChatUserMessageState> { extends Bloc<ChatUserMessageEvent, ChatUserMessageState> {
ChatUserMessageBloc({ ChatUserMessageBloc({
required TextMessage message, required Message message,
required String? metadata, required String? metadata,
}) : super( }) : super(
ChatUserMessageState.initial( ChatUserMessageState.initial(
@ -36,12 +36,12 @@ class ChatUserMessageEvent with _$ChatUserMessageEvent {
@freezed @freezed
class ChatUserMessageState with _$ChatUserMessageState { class ChatUserMessageState with _$ChatUserMessageState {
const factory ChatUserMessageState({ const factory ChatUserMessageState({
required TextMessage message, required Message message,
required List<ChatFile> files, required List<ChatFile> files,
}) = _ChatUserMessageState; }) = _ChatUserMessageState;
factory ChatUserMessageState.initial( factory ChatUserMessageState.initial(
TextMessage message, Message message,
List<ChatFile> files, List<ChatFile> files,
) => ) =>
ChatUserMessageState(message: message, files: files); ChatUserMessageState(message: message, files: files);

View File

@ -2,8 +2,10 @@ import 'dart:math';
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/ai_chat/application/chat_bloc.dart'; import 'package:appflowy/plugins/ai_chat/application/chat_bloc.dart';
import 'package:appflowy/plugins/ai_chat/application/chat_entity.dart';
import 'package:appflowy/plugins/ai_chat/application/chat_file_bloc.dart'; import 'package:appflowy/plugins/ai_chat/application/chat_file_bloc.dart';
import 'package:appflowy/plugins/ai_chat/application/chat_input_bloc.dart'; import 'package:appflowy/plugins/ai_chat/application/chat_input_bloc.dart';
import 'package:appflowy/plugins/ai_chat/application/chat_message_stream.dart';
import 'package:appflowy/plugins/ai_chat/presentation/chat_related_question.dart'; import 'package:appflowy/plugins/ai_chat/presentation/chat_related_question.dart';
import 'package:appflowy/plugins/ai_chat/presentation/message/ai_message_bubble.dart'; import 'package:appflowy/plugins/ai_chat/presentation/message/ai_message_bubble.dart';
import 'package:appflowy/plugins/ai_chat/presentation/message/other_user_message_bubble.dart'; import 'package:appflowy/plugins/ai_chat/presentation/message/other_user_message_bubble.dart';
@ -256,27 +258,26 @@ class _ChatContentPageState extends State<_ChatContentPage> {
theme: buildTheme(context), theme: buildTheme(context),
onEndReached: () async { onEndReached: () async {
if (state.hasMorePrevMessage && if (state.hasMorePrevMessage &&
state.loadingPreviousStatus != const LoadingState.loading()) { state.loadingPreviousStatus.isFinish) {
blocContext blocContext
.read<ChatBloc>() .read<ChatBloc>()
.add(const ChatEvent.startLoadingPrevMessage()); .add(const ChatEvent.startLoadingPrevMessage());
} }
}, },
emptyState: BlocBuilder<ChatBloc, ChatState>( emptyState: BlocBuilder<ChatBloc, ChatState>(
builder: (_, state) => builder: (_, state) => state.initialLoadingStatus.isFinish
state.initialLoadingStatus == const LoadingState.finish() ? Padding(
? Padding( padding: AIChatUILayout.welcomePagePadding,
padding: AIChatUILayout.welcomePagePadding, child: ChatWelcomePage(
child: ChatWelcomePage( userProfile: widget.userProfile,
userProfile: widget.userProfile, onSelectedQuestion: (question) => blocContext
onSelectedQuestion: (question) => blocContext .read<ChatBloc>()
.read<ChatBloc>() .add(ChatEvent.sendMessage(message: question)),
.add(ChatEvent.sendMessage(message: question)), ),
), )
) : const Center(
: const Center( child: CircularProgressIndicator.adaptive(),
child: CircularProgressIndicator.adaptive(), ),
),
), ),
messageWidthRatio: AIChatUILayout.messageWidthRatio, messageWidthRatio: AIChatUILayout.messageWidthRatio,
textMessageBuilder: ( textMessageBuilder: (

View File

@ -1,3 +1,4 @@
import 'package:appflowy/plugins/ai_chat/application/chat_entity.dart';
import 'package:appflowy/plugins/ai_chat/application/chat_file_bloc.dart'; import 'package:appflowy/plugins/ai_chat/application/chat_file_bloc.dart';
import 'package:appflowy/plugins/ai_chat/application/chat_input_action_bloc.dart'; import 'package:appflowy/plugins/ai_chat/application/chat_input_action_bloc.dart';
import 'package:appflowy/plugins/ai_chat/application/chat_input_action_control.dart'; import 'package:appflowy/plugins/ai_chat/application/chat_input_action_control.dart';
@ -115,7 +116,7 @@ class _ChatInputState extends State<ChatInput> {
border: Border.all( border: Border.all(
color: _inputFocusNode.hasFocus && !isMobile color: _inputFocusNode.hasFocus && !isMobile
? Theme.of(context).colorScheme.primary.withOpacity(0.6) ? Theme.of(context).colorScheme.primary.withOpacity(0.6)
: Colors.grey.shade700, : Theme.of(context).colorScheme.secondary,
), ),
borderRadius: borderRadius, borderRadius: borderRadius,
), ),
@ -160,9 +161,8 @@ class _ChatInputState extends State<ChatInput> {
Expanded(child: _inputTextField(context, textPadding)), Expanded(child: _inputTextField(context, textPadding)),
// at button // at button
if (PlatformExtension.isDesktop && // TODO(lucas): support mobile
widget.aiType == const AIType.appflowyAI()) if (PlatformExtension.isDesktop) _atButton(buttonPadding),
_atButton(buttonPadding),
// send button // send button
_sendButton(buttonPadding), _sendButton(buttonPadding),
@ -267,10 +267,6 @@ class _ChatInputState extends State<ChatInput> {
} }
Future<void> _handleOnTextChange(BuildContext context, String text) async { Future<void> _handleOnTextChange(BuildContext context, String text) async {
if (widget.aiType != const AIType.appflowyAI()) {
return;
}
if (!_inputActionControl.onTextChanged(text)) { if (!_inputActionControl.onTextChanged(text)) {
return; return;
} }

View File

@ -1,5 +1,5 @@
import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/plugins/ai_chat/application/chat_file_bloc.dart'; import 'package:appflowy/plugins/ai_chat/application/chat_entity.dart';
import 'package:appflowy/plugins/ai_chat/application/chat_input_file_bloc.dart'; import 'package:appflowy/plugins/ai_chat/application/chat_input_file_bloc.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart'; import 'package:flowy_infra_ui/style_widget/hover.dart';
@ -67,17 +67,18 @@ class ChatFilePreview extends StatelessWidget {
decoration: BoxDecoration( decoration: BoxDecoration(
color: color:
Theme.of(context).colorScheme.surfaceContainerHighest, Theme.of(context).colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(8),
), ),
child: Stack( child: Stack(
clipBehavior: Clip.none, clipBehavior: Clip.none,
children: [ children: [
Padding( Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 8.0, horizontal: 10.0,
vertical: 10, vertical: 14,
), ),
child: Row( child: Row(
mainAxisSize: MainAxisSize.min,
children: [ children: [
file.fileType.icon, file.fileType.icon,
const HSpace(6), const HSpace(6),

View File

@ -1,4 +1,4 @@
import 'package:appflowy/plugins/ai_chat/application/chat_bloc.dart'; import 'package:appflowy/plugins/ai_chat/application/chat_entity.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_chat_types/flutter_chat_types.dart'; import 'package:flutter_chat_types/flutter_chat_types.dart';

View File

@ -2,7 +2,7 @@ import 'dart:convert';
import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/ai_chat/application/chat_bloc.dart'; import 'package:appflowy/plugins/ai_chat/application/chat_entity.dart';
import 'package:appflowy/plugins/ai_chat/presentation/chat_avatar.dart'; import 'package:appflowy/plugins/ai_chat/presentation/chat_avatar.dart';
import 'package:appflowy/plugins/ai_chat/presentation/chat_input/chat_input.dart'; import 'package:appflowy/plugins/ai_chat/presentation/chat_input/chat_input.dart';
import 'package:appflowy/plugins/ai_chat/presentation/chat_popmenu.dart'; import 'package:appflowy/plugins/ai_chat/presentation/chat_popmenu.dart';

View File

@ -1,5 +1,5 @@
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/ai_chat/application/chat_bloc.dart'; import 'package:appflowy/plugins/ai_chat/application/chat_entity.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/style_widget/button.dart'; import 'package:flowy_infra_ui/style_widget/button.dart';
import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flowy_infra_ui/style_widget/text.dart';

View File

@ -1,6 +1,6 @@
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/ai_chat/application/chat_ai_message_bloc.dart'; import 'package:appflowy/plugins/ai_chat/application/chat_ai_message_bloc.dart';
import 'package:appflowy/plugins/ai_chat/application/chat_bloc.dart'; import 'package:appflowy/plugins/ai_chat/application/chat_entity.dart';
import 'package:appflowy/plugins/ai_chat/presentation/chat_loading.dart'; import 'package:appflowy/plugins/ai_chat/presentation/chat_loading.dart';
import 'package:appflowy/plugins/ai_chat/presentation/message/ai_markdown_text.dart'; import 'package:appflowy/plugins/ai_chat/presentation/message/ai_markdown_text.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';

View File

@ -1,16 +1,12 @@
import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/plugins/ai_chat/application/chat_entity.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/ai_chat/application/chat_member_bloc.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/presentation/chat_avatar.dart'; import 'package:appflowy/plugins/ai_chat/presentation/chat_avatar.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra/size.dart'; import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_chat_types/flutter_chat_types.dart'; import 'package:flutter_chat_types/flutter_chat_types.dart';
import 'package:styled_widget/styled_widget.dart';
class ChatUserMessageBubble extends StatelessWidget { class ChatUserMessageBubble extends StatelessWidget {
const ChatUserMessageBubble({ const ChatUserMessageBubble({
@ -33,141 +29,130 @@ class ChatUserMessageBubble extends StatelessWidget {
.read<ChatMemberBloc>() .read<ChatMemberBloc>()
.add(ChatMemberEvent.getMemberInfo(message.author.id)); .add(ChatMemberEvent.getMemberInfo(message.author.id));
} }
final metadata = message.metadata?[messageMetadataKey] as String?;
return BlocConsumer<ChatMemberBloc, ChatMemberState>( return BlocProvider(
listenWhen: (previous, current) { create: (context) => ChatUserMessageBloc(
return previous.members[message.author.id] != message: message,
current.members[message.author.id]; metadata: metadata,
}, ),
listener: (context, state) {}, child: BlocBuilder<ChatUserMessageBloc, ChatUserMessageState>(
builder: (context, state) { builder: (context, state) {
final member = state.members[message.author.id]; return Column(
return Row( mainAxisSize: MainAxisSize.min,
mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.start, children: [
children: [ if (state.files.isNotEmpty) ...[
// _wrapHover( Padding(
Flexible( padding: const EdgeInsets.only(right: defaultAvatarSize + 32),
child: DecoratedBox( child: _MessageFileList(files: state.files),
decoration: BoxDecoration(
borderRadius: borderRadius,
color: backgroundColor,
), ),
child: Padding( const VSpace(6),
padding: const EdgeInsets.symmetric( ],
horizontal: 16, Row(
vertical: 12, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Flexible(
child: DecoratedBox(
decoration: BoxDecoration(
borderRadius: borderRadius,
color: backgroundColor,
),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
child: child,
),
),
), ),
child: child, Padding(
), padding: const EdgeInsets.symmetric(horizontal: 16),
child: BlocConsumer<ChatMemberBloc, ChatMemberState>(
listenWhen: (previous, current) =>
previous.members[message.author.id] !=
current.members[message.author.id],
listener: (context, state) {},
builder: (context, state) {
final member = state.members[message.author.id];
return ChatUserAvatar(
iconUrl: member?.info.avatarUrl ?? "",
name: member?.info.name ?? "",
);
},
),
),
],
), ),
), ],
// ), );
Padding( },
padding: const EdgeInsets.symmetric(horizontal: 16), ),
child: ChatUserAvatar( );
iconUrl: member?.info.avatarUrl ?? "", }
name: member?.info.name ?? "", }
class _MessageFileList extends StatelessWidget {
const _MessageFileList({required this.files});
final List<ChatFile> files;
@override
Widget build(BuildContext context) {
final List<Widget> children = files
.map(
(file) => _MessageFile(
file: file,
),
)
.toList();
return Wrap(
direction: Axis.vertical,
crossAxisAlignment: WrapCrossAlignment.end,
spacing: 6,
runSpacing: 6,
children: children,
);
}
}
class _MessageFile extends StatelessWidget {
const _MessageFile({required this.file});
final ChatFile file;
@override
Widget build(BuildContext context) {
return DecoratedBox(
decoration: BoxDecoration(
color: Colors.transparent,
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: Theme.of(context).colorScheme.secondary,
),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 16),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox.square(dimension: 16, child: file.fileType.icon),
const HSpace(6),
Flexible(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 400),
child: FlowyText(
file.fileName,
fontSize: 12,
maxLines: 6,
),
), ),
), ),
], ],
);
},
);
}
}
class ChatUserMessageHover extends StatefulWidget {
const ChatUserMessageHover({
super.key,
required this.child,
required this.message,
});
final Widget child;
final Message message;
final bool autoShowHover = true;
@override
State<ChatUserMessageHover> createState() => _ChatUserMessageHoverState();
}
class _ChatUserMessageHoverState extends State<ChatUserMessageHover> {
bool _isHover = false;
@override
void initState() {
super.initState();
_isHover = widget.autoShowHover ? false : true;
}
@override
Widget build(BuildContext context) {
final List<Widget> children = [
DecoratedBox(
decoration: const BoxDecoration(
color: Colors.transparent,
borderRadius: Corners.s6Border,
), ),
child: Padding(
padding: const EdgeInsets.only(bottom: 30),
child: widget.child,
),
),
];
if (_isHover) {
if (widget.message is TextMessage) {
children.add(
EditButton(
textMessage: widget.message as TextMessage,
).positioned(right: 0, bottom: 0),
);
}
}
return MouseRegion(
cursor: SystemMouseCursors.click,
opaque: false,
onEnter: (p) => setState(() {
if (widget.autoShowHover) {
_isHover = true;
}
}),
onExit: (p) => setState(() {
if (widget.autoShowHover) {
_isHover = false;
}
}),
child: Stack(
alignment: AlignmentDirectional.centerStart,
children: children,
),
);
}
}
class EditButton extends StatelessWidget {
const EditButton({
super.key,
required this.textMessage,
});
final TextMessage textMessage;
@override
Widget build(BuildContext context) {
return FlowyTooltip(
message: LocaleKeys.settings_menu_clickToCopy.tr(),
child: FlowyIconButton(
width: 24,
hoverColor: AFThemeExtension.of(context).lightGreyHover,
fillColor: Theme.of(context).cardColor,
icon: FlowySvg(
FlowySvgs.ai_copy_s,
size: const Size.square(14),
color: Theme.of(context).colorScheme.primary,
),
onPressed: () {},
), ),
); );
} }

View File

@ -1,10 +1,6 @@
import 'package:appflowy/plugins/ai_chat/application/chat_file_bloc.dart';
import 'package:appflowy/plugins/ai_chat/application/chat_user_message_bloc.dart';
import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_chat_types/flutter_chat_types.dart'; import 'package:flutter_chat_types/flutter_chat_types.dart';
class ChatUserTextMessageWidget extends StatelessWidget { class ChatUserTextMessageWidget extends StatelessWidget {
@ -23,28 +19,8 @@ class ChatUserTextMessageWidget extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider( return TextMessageText(
create: (context) => ChatUserMessageBloc( text: message.text,
message: message,
metadata: metadata,
),
child: BlocBuilder<ChatUserMessageBloc, ChatUserMessageState>(
builder: (context, state) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
if (state.files.isNotEmpty) ...[
_MessageFileList(files: state.files),
const VSpace(6),
],
TextMessageText(
text: message.text,
),
],
);
},
),
); );
} }
} }
@ -71,59 +47,3 @@ class TextMessageText extends StatelessWidget {
); );
} }
} }
class _MessageFileList extends StatelessWidget {
const _MessageFileList({required this.files});
final List<ChatFile> files;
@override
Widget build(BuildContext context) {
final List<Widget> children = files
.map(
(file) => _MessageFile(
file: file,
),
)
.toList();
return Wrap(
spacing: 6,
runSpacing: 6,
children: children,
);
}
}
class _MessageFile extends StatelessWidget {
const _MessageFile({required this.file});
final ChatFile file;
@override
Widget build(BuildContext context) {
return DecoratedBox(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(4),
),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
file.fileType.icon,
const HSpace(6),
Flexible(
child: FlowyText(
file.fileName,
fontSize: 12,
maxLines: 6,
),
),
],
),
),
);
}
}

View File

@ -2,7 +2,7 @@ import 'dart:async';
import 'dart:ffi'; import 'dart:ffi';
import 'dart:isolate'; import 'dart:isolate';
import 'package:appflowy/plugins/ai_chat/application/chat_bloc.dart'; import 'package:appflowy/plugins/ai_chat/application/chat_entity.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart';
@ -52,12 +52,16 @@ class DownloadModelBloc extends Bloc<DownloadModelEvent, DownloadModelState> {
emit( emit(
state.copyWith( state.copyWith(
downloadStream: downloadStream, downloadStream: downloadStream,
loadingState: const LoadingState.finish(), loadingState: const ChatLoadingState.finish(),
downloadError: null, downloadError: null,
), ),
); );
}, (err) { }, (err) {
emit(state.copyWith(loadingState: LoadingState.finish(error: err))); emit(
state.copyWith(
loadingState: ChatLoadingState.finish(error: err),
),
);
}); });
}, },
updatePercent: (String object, double percent) { updatePercent: (String object, double percent) {
@ -95,7 +99,7 @@ class DownloadModelState with _$DownloadModelState {
@Default("") String object, @Default("") String object,
@Default(0) double percent, @Default(0) double percent,
@Default(false) bool isFinish, @Default(false) bool isFinish,
@Default(LoadingState.loading()) LoadingState loadingState, @Default(ChatLoadingState.loading()) ChatLoadingState loadingState,
}) = _DownloadModelState; }) = _DownloadModelState;
} }

View File

@ -1,6 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'package:appflowy/plugins/ai_chat/application/chat_bloc.dart'; import 'package:appflowy/plugins/ai_chat/application/chat_entity.dart';
import 'package:appflowy/workspace/application/settings/ai/local_llm_listener.dart'; import 'package:appflowy/workspace/application/settings/ai/local_llm_listener.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/log.dart';
@ -72,14 +72,14 @@ class LocalAIChatSettingBloc
llmResource, llmResource,
llmModel, llmModel,
), ),
selectLLMState: const LoadingState.finish(), selectLLMState: const ChatLoadingState.finish(),
), ),
); );
} else { } else {
emit( emit(
state.copyWith( state.copyWith(
selectedLLMModel: llmModel, selectedLLMModel: llmModel,
selectLLMState: const LoadingState.finish(), selectLLMState: const ChatLoadingState.finish(),
progressIndicator: const LocalAIProgress.checkPluginState(), progressIndicator: const LocalAIProgress.checkPluginState(),
), ),
); );
@ -88,7 +88,7 @@ class LocalAIChatSettingBloc
(err) { (err) {
emit( emit(
state.copyWith( state.copyWith(
selectLLMState: LoadingState.finish(error: err), selectLLMState: ChatLoadingState.finish(error: err),
), ),
); );
}, },
@ -117,7 +117,7 @@ class LocalAIChatSettingBloc
state.copyWith( state.copyWith(
progressIndicator: progressIndicator:
LocalAIProgress.startDownloading(state.selectedLLMModel!), LocalAIProgress.startDownloading(state.selectedLLMModel!),
selectLLMState: const LoadingState.finish(), selectLLMState: const ChatLoadingState.finish(),
), ),
); );
return; return;
@ -128,7 +128,7 @@ class LocalAIChatSettingBloc
llmResource, llmResource,
state.selectedLLMModel!, state.selectedLLMModel!,
), ),
selectLLMState: const LoadingState.finish(), selectLLMState: const ChatLoadingState.finish(),
), ),
); );
} }
@ -139,7 +139,7 @@ class LocalAIChatSettingBloc
emit( emit(
state.copyWith( state.copyWith(
progressIndicator: LocalAIProgress.startDownloading(llmModel), progressIndicator: LocalAIProgress.startDownloading(llmModel),
selectLLMState: const LoadingState.finish(), selectLLMState: const ChatLoadingState.finish(),
), ),
); );
}, },
@ -257,7 +257,7 @@ class LocalAIChatSettingState with _$LocalAIChatSettingState {
LLMModelPB? selectedLLMModel, LLMModelPB? selectedLLMModel,
LocalAIProgress? progressIndicator, LocalAIProgress? progressIndicator,
@Default(AIModelProgress.init()) AIModelProgress aiModelProgress, @Default(AIModelProgress.init()) AIModelProgress aiModelProgress,
@Default(LoadingState.loading()) LoadingState selectLLMState, @Default(ChatLoadingState.loading()) ChatLoadingState selectLLMState,
@Default([]) List<LLMModelPB> models, @Default([]) List<LLMModelPB> models,
@Default(RunningStatePB.Connecting) RunningStatePB runningState, @Default(RunningStatePB.Connecting) RunningStatePB runningState,
}) = _LocalAIChatSettingState; }) = _LocalAIChatSettingState;

View File

@ -118,6 +118,10 @@ extension ViewExtension on ViewPB {
bool get isSpace { bool get isSpace {
try { try {
if (extra.isEmpty) {
return false;
}
final ext = jsonDecode(extra); final ext = jsonDecode(extra);
final isSpace = ext[ViewExtKeys.isSpaceKey] ?? false; final isSpace = ext[ViewExtKeys.isSpaceKey] ?? false;
return isSpace; return isSpace;
@ -138,6 +142,10 @@ extension ViewExtension on ViewPB {
FlowySvg? buildSpaceIconSvg(BuildContext context, {Size? size}) { FlowySvg? buildSpaceIconSvg(BuildContext context, {Size? size}) {
try { try {
if (extra.isEmpty) {
return null;
}
final ext = jsonDecode(extra); final ext = jsonDecode(extra);
final icon = ext[ViewExtKeys.spaceIconKey]; final icon = ext[ViewExtKeys.spaceIconKey];
final color = ext[ViewExtKeys.spaceIconColorKey]; final color = ext[ViewExtKeys.spaceIconColorKey];
@ -214,6 +222,11 @@ extension ViewExtension on ViewPB {
if (layout != ViewLayoutPB.Document) { if (layout != ViewLayoutPB.Document) {
return null; return null;
} }
if (extra.isEmpty) {
return null;
}
try { try {
final ext = jsonDecode(extra); final ext = jsonDecode(extra);
final cover = ext[ViewExtKeys.coverKey] ?? {}; final cover = ext[ViewExtKeys.coverKey] ?? {};

View File

@ -172,7 +172,7 @@ checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
[[package]] [[package]]
name = "app-error" name = "app-error"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=99410fb7662440e75493df110de2283f75ab2418#99410fb7662440e75493df110de2283f75ab2418" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=135a67dc79848c39e9c53b4a99b6d14f444686ef#135a67dc79848c39e9c53b4a99b6d14f444686ef"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bincode", "bincode",
@ -192,7 +192,7 @@ dependencies = [
[[package]] [[package]]
name = "appflowy-ai-client" name = "appflowy-ai-client"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=99410fb7662440e75493df110de2283f75ab2418#99410fb7662440e75493df110de2283f75ab2418" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=135a67dc79848c39e9c53b4a99b6d14f444686ef#135a67dc79848c39e9c53b4a99b6d14f444686ef"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bytes", "bytes",
@ -207,7 +207,7 @@ dependencies = [
[[package]] [[package]]
name = "appflowy-local-ai" name = "appflowy-local-ai"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=7dd879a6b9b3246e5cd06f1647e620553db9b960#7dd879a6b9b3246e5cd06f1647e620553db9b960" source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=65802795ad8778de11c45b5af65d05c973709613#65802795ad8778de11c45b5af65d05c973709613"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"appflowy-plugin", "appflowy-plugin",
@ -226,7 +226,7 @@ dependencies = [
[[package]] [[package]]
name = "appflowy-plugin" name = "appflowy-plugin"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=7dd879a6b9b3246e5cd06f1647e620553db9b960#7dd879a6b9b3246e5cd06f1647e620553db9b960" source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=65802795ad8778de11c45b5af65d05c973709613#65802795ad8778de11c45b5af65d05c973709613"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"cfg-if", "cfg-if",
@ -315,9 +315,9 @@ dependencies = [
[[package]] [[package]]
name = "async-trait" name = "async-trait"
version = "0.1.77" version = "0.1.81"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9" checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -423,7 +423,7 @@ dependencies = [
"bitflags 2.4.0", "bitflags 2.4.0",
"cexpr", "cexpr",
"clang-sys", "clang-sys",
"itertools 0.10.5", "itertools 0.12.1",
"lazy_static", "lazy_static",
"lazycell", "lazycell",
"proc-macro2", "proc-macro2",
@ -826,7 +826,7 @@ dependencies = [
[[package]] [[package]]
name = "client-api" name = "client-api"
version = "0.2.0" version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=99410fb7662440e75493df110de2283f75ab2418#99410fb7662440e75493df110de2283f75ab2418" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=135a67dc79848c39e9c53b4a99b6d14f444686ef#135a67dc79848c39e9c53b4a99b6d14f444686ef"
dependencies = [ dependencies = [
"again", "again",
"anyhow", "anyhow",
@ -876,7 +876,7 @@ dependencies = [
[[package]] [[package]]
name = "client-api-entity" name = "client-api-entity"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=99410fb7662440e75493df110de2283f75ab2418#99410fb7662440e75493df110de2283f75ab2418" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=135a67dc79848c39e9c53b4a99b6d14f444686ef#135a67dc79848c39e9c53b4a99b6d14f444686ef"
dependencies = [ dependencies = [
"collab-entity", "collab-entity",
"collab-rt-entity", "collab-rt-entity",
@ -888,7 +888,7 @@ dependencies = [
[[package]] [[package]]
name = "client-websocket" name = "client-websocket"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=99410fb7662440e75493df110de2283f75ab2418#99410fb7662440e75493df110de2283f75ab2418" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=135a67dc79848c39e9c53b4a99b6d14f444686ef#135a67dc79848c39e9c53b4a99b6d14f444686ef"
dependencies = [ dependencies = [
"futures-channel", "futures-channel",
"futures-util", "futures-util",
@ -1132,7 +1132,7 @@ dependencies = [
[[package]] [[package]]
name = "collab-rt-entity" name = "collab-rt-entity"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=99410fb7662440e75493df110de2283f75ab2418#99410fb7662440e75493df110de2283f75ab2418" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=135a67dc79848c39e9c53b4a99b6d14f444686ef#135a67dc79848c39e9c53b4a99b6d14f444686ef"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bincode", "bincode",
@ -1157,7 +1157,7 @@ dependencies = [
[[package]] [[package]]
name = "collab-rt-protocol" name = "collab-rt-protocol"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=99410fb7662440e75493df110de2283f75ab2418#99410fb7662440e75493df110de2283f75ab2418" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=135a67dc79848c39e9c53b4a99b6d14f444686ef#135a67dc79848c39e9c53b4a99b6d14f444686ef"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@ -1532,7 +1532,7 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308"
[[package]] [[package]]
name = "database-entity" name = "database-entity"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=99410fb7662440e75493df110de2283f75ab2418#99410fb7662440e75493df110de2283f75ab2418" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=135a67dc79848c39e9c53b4a99b6d14f444686ef#135a67dc79848c39e9c53b4a99b6d14f444686ef"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"app-error", "app-error",
@ -3051,7 +3051,7 @@ dependencies = [
[[package]] [[package]]
name = "gotrue" name = "gotrue"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=99410fb7662440e75493df110de2283f75ab2418#99410fb7662440e75493df110de2283f75ab2418" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=135a67dc79848c39e9c53b4a99b6d14f444686ef#135a67dc79848c39e9c53b4a99b6d14f444686ef"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"futures-util", "futures-util",
@ -3068,7 +3068,7 @@ dependencies = [
[[package]] [[package]]
name = "gotrue-entity" name = "gotrue-entity"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=99410fb7662440e75493df110de2283f75ab2418#99410fb7662440e75493df110de2283f75ab2418" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=135a67dc79848c39e9c53b4a99b6d14f444686ef#135a67dc79848c39e9c53b4a99b6d14f444686ef"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"app-error", "app-error",
@ -3500,7 +3500,7 @@ dependencies = [
[[package]] [[package]]
name = "infra" name = "infra"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=99410fb7662440e75493df110de2283f75ab2418#99410fb7662440e75493df110de2283f75ab2418" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=135a67dc79848c39e9c53b4a99b6d14f444686ef#135a67dc79848c39e9c53b4a99b6d14f444686ef"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bytes", "bytes",
@ -6098,7 +6098,7 @@ dependencies = [
[[package]] [[package]]
name = "shared-entity" name = "shared-entity"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=99410fb7662440e75493df110de2283f75ab2418#99410fb7662440e75493df110de2283f75ab2418" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=135a67dc79848c39e9c53b4a99b6d14f444686ef#135a67dc79848c39e9c53b4a99b6d14f444686ef"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"app-error", "app-error",

View File

@ -53,7 +53,7 @@ collab-user = { version = "0.2" }
# Run the script: # Run the script:
# scripts/tool/update_client_api_rev.sh new_rev_id # scripts/tool/update_client_api_rev.sh new_rev_id
# ⚠️⚠️⚠️️ # ⚠️⚠️⚠️️
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "99410fb7662440e75493df110de2283f75ab2418" } client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "135a67dc79848c39e9c53b4a99b6d14f444686ef" }
[dependencies] [dependencies]
serde_json.workspace = true serde_json.workspace = true
@ -128,5 +128,5 @@ collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-
# To update the commit ID, run: # To update the commit ID, run:
# scripts/tool/update_local_ai_rev.sh new_rev_id # scripts/tool/update_local_ai_rev.sh new_rev_id
# ⚠️⚠️⚠️️ # ⚠️⚠️⚠️️
appflowy-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "7dd879a6b9b3246e5cd06f1647e620553db9b960" } appflowy-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "65802795ad8778de11c45b5af65d05c973709613" }
appflowy-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "7dd879a6b9b3246e5cd06f1647e620553db9b960" } appflowy-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "65802795ad8778de11c45b5af65d05c973709613" }

View File

@ -163,7 +163,7 @@ checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
[[package]] [[package]]
name = "app-error" name = "app-error"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=99410fb7662440e75493df110de2283f75ab2418#99410fb7662440e75493df110de2283f75ab2418" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=135a67dc79848c39e9c53b4a99b6d14f444686ef#135a67dc79848c39e9c53b4a99b6d14f444686ef"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bincode", "bincode",
@ -183,7 +183,7 @@ dependencies = [
[[package]] [[package]]
name = "appflowy-ai-client" name = "appflowy-ai-client"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=99410fb7662440e75493df110de2283f75ab2418#99410fb7662440e75493df110de2283f75ab2418" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=135a67dc79848c39e9c53b4a99b6d14f444686ef#135a67dc79848c39e9c53b4a99b6d14f444686ef"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bytes", "bytes",
@ -198,7 +198,7 @@ dependencies = [
[[package]] [[package]]
name = "appflowy-local-ai" name = "appflowy-local-ai"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=7dd879a6b9b3246e5cd06f1647e620553db9b960#7dd879a6b9b3246e5cd06f1647e620553db9b960" source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=65802795ad8778de11c45b5af65d05c973709613#65802795ad8778de11c45b5af65d05c973709613"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"appflowy-plugin", "appflowy-plugin",
@ -217,7 +217,7 @@ dependencies = [
[[package]] [[package]]
name = "appflowy-plugin" name = "appflowy-plugin"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=7dd879a6b9b3246e5cd06f1647e620553db9b960#7dd879a6b9b3246e5cd06f1647e620553db9b960" source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=65802795ad8778de11c45b5af65d05c973709613#65802795ad8778de11c45b5af65d05c973709613"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"cfg-if", "cfg-if",
@ -325,9 +325,9 @@ dependencies = [
[[package]] [[package]]
name = "async-trait" name = "async-trait"
version = "0.1.79" version = "0.1.81"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a507401cad91ec6a857ed5513a2073c82a9b9048762b885bb98655b306964681" checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -800,7 +800,7 @@ dependencies = [
[[package]] [[package]]
name = "client-api" name = "client-api"
version = "0.2.0" version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=99410fb7662440e75493df110de2283f75ab2418#99410fb7662440e75493df110de2283f75ab2418" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=135a67dc79848c39e9c53b4a99b6d14f444686ef#135a67dc79848c39e9c53b4a99b6d14f444686ef"
dependencies = [ dependencies = [
"again", "again",
"anyhow", "anyhow",
@ -850,7 +850,7 @@ dependencies = [
[[package]] [[package]]
name = "client-api-entity" name = "client-api-entity"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=99410fb7662440e75493df110de2283f75ab2418#99410fb7662440e75493df110de2283f75ab2418" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=135a67dc79848c39e9c53b4a99b6d14f444686ef#135a67dc79848c39e9c53b4a99b6d14f444686ef"
dependencies = [ dependencies = [
"collab-entity", "collab-entity",
"collab-rt-entity", "collab-rt-entity",
@ -862,7 +862,7 @@ dependencies = [
[[package]] [[package]]
name = "client-websocket" name = "client-websocket"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=99410fb7662440e75493df110de2283f75ab2418#99410fb7662440e75493df110de2283f75ab2418" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=135a67dc79848c39e9c53b4a99b6d14f444686ef#135a67dc79848c39e9c53b4a99b6d14f444686ef"
dependencies = [ dependencies = [
"futures-channel", "futures-channel",
"futures-util", "futures-util",
@ -1115,7 +1115,7 @@ dependencies = [
[[package]] [[package]]
name = "collab-rt-entity" name = "collab-rt-entity"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=99410fb7662440e75493df110de2283f75ab2418#99410fb7662440e75493df110de2283f75ab2418" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=135a67dc79848c39e9c53b4a99b6d14f444686ef#135a67dc79848c39e9c53b4a99b6d14f444686ef"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bincode", "bincode",
@ -1140,7 +1140,7 @@ dependencies = [
[[package]] [[package]]
name = "collab-rt-protocol" name = "collab-rt-protocol"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=99410fb7662440e75493df110de2283f75ab2418#99410fb7662440e75493df110de2283f75ab2418" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=135a67dc79848c39e9c53b4a99b6d14f444686ef#135a67dc79848c39e9c53b4a99b6d14f444686ef"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@ -1522,7 +1522,7 @@ checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5"
[[package]] [[package]]
name = "database-entity" name = "database-entity"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=99410fb7662440e75493df110de2283f75ab2418#99410fb7662440e75493df110de2283f75ab2418" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=135a67dc79848c39e9c53b4a99b6d14f444686ef#135a67dc79848c39e9c53b4a99b6d14f444686ef"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"app-error", "app-error",
@ -3118,7 +3118,7 @@ dependencies = [
[[package]] [[package]]
name = "gotrue" name = "gotrue"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=99410fb7662440e75493df110de2283f75ab2418#99410fb7662440e75493df110de2283f75ab2418" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=135a67dc79848c39e9c53b4a99b6d14f444686ef#135a67dc79848c39e9c53b4a99b6d14f444686ef"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"futures-util", "futures-util",
@ -3135,7 +3135,7 @@ dependencies = [
[[package]] [[package]]
name = "gotrue-entity" name = "gotrue-entity"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=99410fb7662440e75493df110de2283f75ab2418#99410fb7662440e75493df110de2283f75ab2418" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=135a67dc79848c39e9c53b4a99b6d14f444686ef#135a67dc79848c39e9c53b4a99b6d14f444686ef"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"app-error", "app-error",
@ -3572,7 +3572,7 @@ dependencies = [
[[package]] [[package]]
name = "infra" name = "infra"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=99410fb7662440e75493df110de2283f75ab2418#99410fb7662440e75493df110de2283f75ab2418" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=135a67dc79848c39e9c53b4a99b6d14f444686ef#135a67dc79848c39e9c53b4a99b6d14f444686ef"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bytes", "bytes",
@ -6162,7 +6162,7 @@ dependencies = [
[[package]] [[package]]
name = "shared-entity" name = "shared-entity"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=99410fb7662440e75493df110de2283f75ab2418#99410fb7662440e75493df110de2283f75ab2418" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=135a67dc79848c39e9c53b4a99b6d14f444686ef#135a67dc79848c39e9c53b4a99b6d14f444686ef"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"app-error", "app-error",

View File

@ -52,7 +52,7 @@ collab-user = { version = "0.2" }
# Run the script: # Run the script:
# scripts/tool/update_client_api_rev.sh new_rev_id # scripts/tool/update_client_api_rev.sh new_rev_id
# ⚠️⚠️⚠️️ # ⚠️⚠️⚠️️
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "99410fb7662440e75493df110de2283f75ab2418" } client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "135a67dc79848c39e9c53b4a99b6d14f444686ef" }
[dependencies] [dependencies]
serde_json.workspace = true serde_json.workspace = true
@ -128,6 +128,6 @@ collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-
# To update the commit ID, run: # To update the commit ID, run:
# scripts/tool/update_local_ai_rev.sh new_rev_id # scripts/tool/update_local_ai_rev.sh new_rev_id
# ⚠️⚠️⚠️️ # ⚠️⚠️⚠️️
appflowy-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "7dd879a6b9b3246e5cd06f1647e620553db9b960" } appflowy-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "65802795ad8778de11c45b5af65d05c973709613" }
appflowy-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "7dd879a6b9b3246e5cd06f1647e620553db9b960" } appflowy-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "65802795ad8778de11c45b5af65d05c973709613" }

View File

@ -163,7 +163,7 @@ checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
[[package]] [[package]]
name = "app-error" name = "app-error"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=99410fb7662440e75493df110de2283f75ab2418#99410fb7662440e75493df110de2283f75ab2418" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=135a67dc79848c39e9c53b4a99b6d14f444686ef#135a67dc79848c39e9c53b4a99b6d14f444686ef"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bincode", "bincode",
@ -183,7 +183,7 @@ dependencies = [
[[package]] [[package]]
name = "appflowy-ai-client" name = "appflowy-ai-client"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=99410fb7662440e75493df110de2283f75ab2418#99410fb7662440e75493df110de2283f75ab2418" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=135a67dc79848c39e9c53b4a99b6d14f444686ef#135a67dc79848c39e9c53b4a99b6d14f444686ef"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bytes", "bytes",
@ -198,7 +198,7 @@ dependencies = [
[[package]] [[package]]
name = "appflowy-local-ai" name = "appflowy-local-ai"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=7dd879a6b9b3246e5cd06f1647e620553db9b960#7dd879a6b9b3246e5cd06f1647e620553db9b960" source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=65802795ad8778de11c45b5af65d05c973709613#65802795ad8778de11c45b5af65d05c973709613"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"appflowy-plugin", "appflowy-plugin",
@ -217,7 +217,7 @@ dependencies = [
[[package]] [[package]]
name = "appflowy-plugin" name = "appflowy-plugin"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=7dd879a6b9b3246e5cd06f1647e620553db9b960#7dd879a6b9b3246e5cd06f1647e620553db9b960" source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=65802795ad8778de11c45b5af65d05c973709613#65802795ad8778de11c45b5af65d05c973709613"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"cfg-if", "cfg-if",
@ -289,9 +289,9 @@ dependencies = [
[[package]] [[package]]
name = "async-trait" name = "async-trait"
version = "0.1.77" version = "0.1.81"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9" checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -718,7 +718,7 @@ dependencies = [
[[package]] [[package]]
name = "client-api" name = "client-api"
version = "0.2.0" version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=99410fb7662440e75493df110de2283f75ab2418#99410fb7662440e75493df110de2283f75ab2418" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=135a67dc79848c39e9c53b4a99b6d14f444686ef#135a67dc79848c39e9c53b4a99b6d14f444686ef"
dependencies = [ dependencies = [
"again", "again",
"anyhow", "anyhow",
@ -768,7 +768,7 @@ dependencies = [
[[package]] [[package]]
name = "client-api-entity" name = "client-api-entity"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=99410fb7662440e75493df110de2283f75ab2418#99410fb7662440e75493df110de2283f75ab2418" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=135a67dc79848c39e9c53b4a99b6d14f444686ef#135a67dc79848c39e9c53b4a99b6d14f444686ef"
dependencies = [ dependencies = [
"collab-entity", "collab-entity",
"collab-rt-entity", "collab-rt-entity",
@ -780,7 +780,7 @@ dependencies = [
[[package]] [[package]]
name = "client-websocket" name = "client-websocket"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=99410fb7662440e75493df110de2283f75ab2418#99410fb7662440e75493df110de2283f75ab2418" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=135a67dc79848c39e9c53b4a99b6d14f444686ef#135a67dc79848c39e9c53b4a99b6d14f444686ef"
dependencies = [ dependencies = [
"futures-channel", "futures-channel",
"futures-util", "futures-util",
@ -993,7 +993,7 @@ dependencies = [
[[package]] [[package]]
name = "collab-rt-entity" name = "collab-rt-entity"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=99410fb7662440e75493df110de2283f75ab2418#99410fb7662440e75493df110de2283f75ab2418" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=135a67dc79848c39e9c53b4a99b6d14f444686ef#135a67dc79848c39e9c53b4a99b6d14f444686ef"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bincode", "bincode",
@ -1018,7 +1018,7 @@ dependencies = [
[[package]] [[package]]
name = "collab-rt-protocol" name = "collab-rt-protocol"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=99410fb7662440e75493df110de2283f75ab2418#99410fb7662440e75493df110de2283f75ab2418" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=135a67dc79848c39e9c53b4a99b6d14f444686ef#135a67dc79848c39e9c53b4a99b6d14f444686ef"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@ -1256,7 +1256,7 @@ dependencies = [
"cssparser-macros", "cssparser-macros",
"dtoa-short", "dtoa-short",
"itoa", "itoa",
"phf 0.8.0", "phf 0.11.2",
"smallvec", "smallvec",
] ]
@ -1356,7 +1356,7 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308"
[[package]] [[package]]
name = "database-entity" name = "database-entity"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=99410fb7662440e75493df110de2283f75ab2418#99410fb7662440e75493df110de2283f75ab2418" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=135a67dc79848c39e9c53b4a99b6d14f444686ef#135a67dc79848c39e9c53b4a99b6d14f444686ef"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"app-error", "app-error",
@ -2730,7 +2730,7 @@ dependencies = [
[[package]] [[package]]
name = "gotrue" name = "gotrue"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=99410fb7662440e75493df110de2283f75ab2418#99410fb7662440e75493df110de2283f75ab2418" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=135a67dc79848c39e9c53b4a99b6d14f444686ef#135a67dc79848c39e9c53b4a99b6d14f444686ef"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"futures-util", "futures-util",
@ -2747,7 +2747,7 @@ dependencies = [
[[package]] [[package]]
name = "gotrue-entity" name = "gotrue-entity"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=99410fb7662440e75493df110de2283f75ab2418#99410fb7662440e75493df110de2283f75ab2418" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=135a67dc79848c39e9c53b4a99b6d14f444686ef#135a67dc79848c39e9c53b4a99b6d14f444686ef"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"app-error", "app-error",
@ -3112,7 +3112,7 @@ dependencies = [
[[package]] [[package]]
name = "infra" name = "infra"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=99410fb7662440e75493df110de2283f75ab2418#99410fb7662440e75493df110de2283f75ab2418" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=135a67dc79848c39e9c53b4a99b6d14f444686ef#135a67dc79848c39e9c53b4a99b6d14f444686ef"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bytes", "bytes",
@ -4068,7 +4068,7 @@ version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12"
dependencies = [ dependencies = [
"phf_macros", "phf_macros 0.8.0",
"phf_shared 0.8.0", "phf_shared 0.8.0",
"proc-macro-hack", "proc-macro-hack",
] ]
@ -4088,6 +4088,7 @@ version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc"
dependencies = [ dependencies = [
"phf_macros 0.11.2",
"phf_shared 0.11.2", "phf_shared 0.11.2",
] ]
@ -4155,6 +4156,19 @@ dependencies = [
"syn 1.0.109", "syn 1.0.109",
] ]
[[package]]
name = "phf_macros"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b"
dependencies = [
"phf_generator 0.11.2",
"phf_shared 0.11.2",
"proc-macro2",
"quote",
"syn 2.0.47",
]
[[package]] [[package]]
name = "phf_shared" name = "phf_shared"
version = "0.8.0" version = "0.8.0"
@ -5307,7 +5321,7 @@ dependencies = [
[[package]] [[package]]
name = "shared-entity" name = "shared-entity"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=99410fb7662440e75493df110de2283f75ab2418#99410fb7662440e75493df110de2283f75ab2418" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=135a67dc79848c39e9c53b4a99b6d14f444686ef#135a67dc79848c39e9c53b4a99b6d14f444686ef"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"app-error", "app-error",

View File

@ -80,7 +80,7 @@ parking_lot = "0.12"
futures = "0.3.29" futures = "0.3.29"
tokio = "1.38.0" tokio = "1.38.0"
tokio-stream = "0.1.14" tokio-stream = "0.1.14"
async-trait = "0.1.74" async-trait = "0.1.81"
chrono = { version = "0.4.31", default-features = false, features = ["clock"] } chrono = { version = "0.4.31", default-features = false, features = ["clock"] }
collab = { version = "0.2" } collab = { version = "0.2" }
collab-entity = { version = "0.2" } collab-entity = { version = "0.2" }
@ -99,8 +99,8 @@ zip = "2.1.3"
# Run the script.add_workspace_members: # Run the script.add_workspace_members:
# scripts/tool/update_client_api_rev.sh new_rev_id # scripts/tool/update_client_api_rev.sh new_rev_id
# ⚠️⚠️⚠️️ # ⚠️⚠️⚠️️
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "99410fb7662440e75493df110de2283f75ab2418" } client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "135a67dc79848c39e9c53b4a99b6d14f444686ef" }
client-api-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "99410fb7662440e75493df110de2283f75ab2418" } client-api-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "135a67dc79848c39e9c53b4a99b6d14f444686ef" }
[profile.dev] [profile.dev]
opt-level = 0 opt-level = 0
@ -147,5 +147,5 @@ collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-
# To update the commit ID, run: # To update the commit ID, run:
# scripts/tool/update_local_ai_rev.sh new_rev_id # scripts/tool/update_local_ai_rev.sh new_rev_id
# ⚠️⚠️⚠️️ # ⚠️⚠️⚠️️
appflowy-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "7dd879a6b9b3246e5cd06f1647e620553db9b960" } appflowy-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "65802795ad8778de11c45b5af65d05c973709613" }
appflowy-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "7dd879a6b9b3246e5cd06f1647e620553db9b960" } appflowy-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "65802795ad8778de11c45b5af65d05c973709613" }

View File

@ -25,7 +25,7 @@ async fn af_cloud_create_chat_message_test() {
&chat_id, &chat_id,
&format!("hello world {}", i), &format!("hello world {}", i),
ChatMessageType::System, ChatMessageType::System,
vec![], &[],
) )
.await .await
.unwrap(); .unwrap();
@ -81,7 +81,7 @@ async fn af_cloud_load_remote_system_message_test() {
&chat_id, &chat_id,
&format!("hello server {}", i), &format!("hello server {}", i),
ChatMessageType::System, ChatMessageType::System,
vec![], &[],
) )
.await .await
.unwrap(); .unwrap();

View File

@ -26,14 +26,14 @@ pub trait ChatCloudService: Send + Sync + 'static {
chat_id: &str, chat_id: &str,
) -> FutureResult<(), FlowyError>; ) -> FutureResult<(), FlowyError>;
fn create_question( async fn create_question(
&self, &self,
workspace_id: &str, workspace_id: &str,
chat_id: &str, chat_id: &str,
message: &str, message: &str,
message_type: ChatMessageType, message_type: ChatMessageType,
metadata: Vec<ChatMessageMetadata>, metadata: &[ChatMessageMetadata],
) -> FutureResult<ChatMessage, FlowyError>; ) -> Result<ChatMessage, FlowyError>;
fn create_answer( fn create_answer(
&self, &self,

View File

@ -23,7 +23,7 @@ pub trait AIUserService: Send + Sync + 'static {
fn device_id(&self) -> Result<String, FlowyError>; fn device_id(&self) -> Result<String, FlowyError>;
fn workspace_id(&self) -> Result<String, FlowyError>; fn workspace_id(&self) -> Result<String, FlowyError>;
fn sqlite_connection(&self, uid: i64) -> Result<DBConnection, FlowyError>; fn sqlite_connection(&self, uid: i64) -> Result<DBConnection, FlowyError>;
fn data_root_dir(&self) -> Result<PathBuf, FlowyError>; fn application_root_dir(&self) -> Result<PathBuf, FlowyError>;
} }
pub struct AIManager { pub struct AIManager {
@ -91,10 +91,6 @@ impl AIManager {
Ok(()) Ok(())
} }
pub fn is_using_local_ai(&self) -> bool {
self.local_ai_controller.is_running()
}
pub async fn delete_chat(&self, chat_id: &str) -> Result<(), FlowyError> { pub async fn delete_chat(&self, chat_id: &str) -> Result<(), FlowyError> {
if let Some((_, chat)) = self.chats.remove(chat_id) { if let Some((_, chat)) = self.chats.remove(chat_id) {
chat.close(); chat.close();

View File

@ -104,7 +104,7 @@ impl Chat {
&self.chat_id, &self.chat_id,
message, message,
message_type, message_type,
metadata, &metadata,
) )
.await .await
.map_err(|err| { .map_err(|err| {
@ -112,6 +112,16 @@ impl Chat {
FlowyError::server_error() FlowyError::server_error()
})?; })?;
if self.chat_service.is_local_ai_enabled() {
if let Err(err) = self
.chat_service
.index_message_metadata(&self.chat_id, &metadata)
.await
{
error!("Failed to index file: {}", err);
}
}
save_chat_message( save_chat_message(
self.user_service.sqlite_connection(uid)?, self.user_service.sqlite_connection(uid)?,
&self.chat_id, &self.chat_id,

View File

@ -15,6 +15,7 @@ use lib_dispatch::prelude::{data_result_ok, AFPluginData, AFPluginState, DataRes
use lib_infra::isolate_stream::IsolateSink; use lib_infra::isolate_stream::IsolateSink;
use std::sync::{Arc, Weak}; use std::sync::{Arc, Weak};
use tokio::sync::oneshot; use tokio::sync::oneshot;
use tracing::trace;
use validator::Validate; use validator::Validate;
fn upgrade_ai_manager(ai_manager: AFPluginState<Weak<AIManager>>) -> FlowyResult<Arc<AIManager>> { fn upgrade_ai_manager(ai_manager: AFPluginState<Weak<AIManager>>) -> FlowyResult<Arc<AIManager>> {
@ -29,7 +30,6 @@ pub(crate) async fn stream_chat_message_handler(
data: AFPluginData<StreamChatPayloadPB>, data: AFPluginData<StreamChatPayloadPB>,
ai_manager: AFPluginState<Weak<AIManager>>, ai_manager: AFPluginState<Weak<AIManager>>,
) -> DataResult<ChatMessagePB, FlowyError> { ) -> DataResult<ChatMessagePB, FlowyError> {
let ai_manager = upgrade_ai_manager(ai_manager)?;
let data = data.into_inner(); let data = data.into_inner();
data.validate()?; data.validate()?;
@ -37,28 +37,21 @@ pub(crate) async fn stream_chat_message_handler(
ChatMessageTypePB::System => ChatMessageType::System, ChatMessageTypePB::System => ChatMessageType::System,
ChatMessageTypePB::User => ChatMessageType::User, ChatMessageTypePB::User => ChatMessageType::User,
}; };
let is_using_local_ai = ai_manager.is_using_local_ai();
let metadata = data let metadata = data
.metadata .metadata
.into_iter() .into_iter()
.map(|metadata| { .map(|metadata| {
let (content_type, content_len) = if is_using_local_ai { let (content_type, content_len) = match metadata.data_type {
(ChatMetadataContentType::Unknown, 0) ChatMessageMetaTypePB::Txt => (ChatMetadataContentType::Text, metadata.data.len()),
} else { ChatMessageMetaTypePB::Markdown => (ChatMetadataContentType::Markdown, metadata.data.len()),
match metadata.data_type { ChatMessageMetaTypePB::PDF => (ChatMetadataContentType::PDF, 0),
ChatMessageMetaTypePB::Txt => (ChatMetadataContentType::Text, metadata.data.len()), ChatMessageMetaTypePB::UnknownMetaType => (ChatMetadataContentType::Unknown, 0),
ChatMessageMetaTypePB::Markdown => {
(ChatMetadataContentType::Markdown, metadata.data.len())
},
ChatMessageMetaTypePB::PDF => (ChatMetadataContentType::PDF, 0),
ChatMessageMetaTypePB::UnknownMetaType => (ChatMetadataContentType::Unknown, 0),
}
}; };
ChatMessageMetadata { ChatMessageMetadata {
data: ChatMetadataData { data: ChatMetadataData {
content: metadata.data, content: metadata.data,
url: None,
content_type, content_type,
size: content_len as i64, size: content_len as i64,
}, },
@ -70,15 +63,23 @@ pub(crate) async fn stream_chat_message_handler(
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let question = ai_manager trace!("Stream chat message with metadata: {:?}", metadata);
.stream_chat_message( let (tx, rx) = oneshot::channel::<Result<ChatMessagePB, FlowyError>>();
&data.chat_id, let ai_manager = upgrade_ai_manager(ai_manager)?;
&data.message, tokio::spawn(async move {
message_type, let result = ai_manager
data.text_stream_port, .stream_chat_message(
metadata, &data.chat_id,
) &data.message,
.await?; message_type,
data.text_stream_port,
metadata,
)
.await;
let _ = tx.send(result);
});
let question = rx.await??;
data_result_ok(question) data_result_ok(question)
} }

View File

@ -6,15 +6,21 @@ use anyhow::Error;
use appflowy_local_ai::chat_plugin::{AIPluginConfig, AppFlowyLocalAI}; use appflowy_local_ai::chat_plugin::{AIPluginConfig, AppFlowyLocalAI};
use appflowy_plugin::manager::PluginManager; use appflowy_plugin::manager::PluginManager;
use appflowy_plugin::util::is_apple_silicon; use appflowy_plugin::util::is_apple_silicon;
use flowy_ai_pub::cloud::{AppFlowyOfflineAI, ChatCloudService, LLMModel, LocalAIConfig}; use flowy_ai_pub::cloud::{
AppFlowyOfflineAI, ChatCloudService, ChatMessageMetadata, ChatMetadataContentType, LLMModel,
LocalAIConfig,
};
use flowy_error::{FlowyError, FlowyResult}; use flowy_error::{FlowyError, FlowyResult};
use flowy_sqlite::kv::KVStorePreferences; use flowy_sqlite::kv::KVStorePreferences;
use futures::Sink; use futures::Sink;
use lib_infra::async_trait::async_trait; use lib_infra::async_trait::async_trait;
use std::collections::HashMap;
use parking_lot::Mutex; use parking_lot::Mutex;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::json;
use std::ops::Deref; use std::ops::Deref;
use std::path::Path;
use std::sync::Arc; use std::sync::Arc;
use tokio::select; use tokio::select;
use tokio_stream::StreamExt; use tokio_stream::StreamExt;
@ -173,7 +179,7 @@ impl LocalAIController {
} }
pub fn open_chat(&self, chat_id: &str) { pub fn open_chat(&self, chat_id: &str) {
if !self.is_running() { if !self.is_enabled() {
return; return;
} }
@ -334,6 +340,63 @@ impl LocalAIController {
Ok(enabled) Ok(enabled)
} }
pub async fn index_message_metadata(
&self,
chat_id: &str,
metadata_list: &[ChatMessageMetadata],
) -> FlowyResult<()> {
for metadata in metadata_list {
let mut index_metadata = HashMap::new();
index_metadata.insert("name".to_string(), json!(&metadata.name));
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);
}
}
},
}
}
Ok(())
}
async fn enable_chat_plugin(&self, enabled: bool) -> FlowyResult<()> { async fn enable_chat_plugin(&self, enabled: bool) -> FlowyResult<()> {
info!("[AI Plugin] enable chat plugin: {}", enabled); info!("[AI Plugin] enable chat plugin: {}", enabled);
if enabled { if enabled {

View File

@ -514,7 +514,7 @@ impl LocalAIResourceController {
} }
pub(crate) fn resource_dir(&self) -> FlowyResult<PathBuf> { pub(crate) fn resource_dir(&self) -> FlowyResult<PathBuf> {
let user_data_dir = self.user_service.data_root_dir()?; let user_data_dir = self.user_service.application_root_dir()?;
Ok(user_data_dir.join("ai")) Ok(user_data_dir.join("ai"))
} }
} }

View File

@ -2,7 +2,7 @@ use crate::ai_manager::AIUserService;
use crate::entities::{ChatStatePB, ModelTypePB}; use crate::entities::{ChatStatePB, ModelTypePB};
use crate::local_ai::local_llm_chat::LocalAIController; use crate::local_ai::local_llm_chat::LocalAIController;
use crate::notification::{make_notification, ChatNotification, APPFLOWY_AI_NOTIFICATION_KEY}; use crate::notification::{make_notification, ChatNotification, APPFLOWY_AI_NOTIFICATION_KEY};
use crate::persistence::select_single_message; use crate::persistence::{select_single_message, ChatMessageTable};
use appflowy_plugin::error::PluginError; use appflowy_plugin::error::PluginError;
use flowy_ai_pub::cloud::{ use flowy_ai_pub::cloud::{
@ -16,8 +16,10 @@ use lib_infra::async_trait::async_trait;
use lib_infra::future::FutureResult; use lib_infra::future::FutureResult;
use crate::local_ai::stream_util::LocalAIStreamAdaptor; use crate::local_ai::stream_util::LocalAIStreamAdaptor;
use serde_json::json;
use std::path::Path; use std::path::Path;
use std::sync::Arc; use std::sync::Arc;
use tracing::trace;
pub struct AICloudServiceMiddleware { pub struct AICloudServiceMiddleware {
cloud_service: Arc<dyn ChatCloudService>, cloud_service: Arc<dyn ChatCloudService>,
@ -38,16 +40,30 @@ impl AICloudServiceMiddleware {
} }
} }
fn get_message_content(&self, message_id: i64) -> FlowyResult<String> { pub fn is_local_ai_enabled(&self) -> bool {
self.local_llm_controller.is_enabled()
}
pub async fn index_message_metadata(
&self,
chat_id: &str,
metadata_list: &[ChatMessageMetadata],
) -> Result<(), FlowyError> {
self
.local_llm_controller
.index_message_metadata(chat_id, metadata_list)
.await?;
Ok(())
}
fn get_message_record(&self, message_id: i64) -> FlowyResult<ChatMessageTable> {
let uid = self.user_service.user_id()?; let uid = self.user_service.user_id()?;
let conn = self.user_service.sqlite_connection(uid)?; let conn = self.user_service.sqlite_connection(uid)?;
let content = select_single_message(conn, message_id)? let row = select_single_message(conn, message_id)?.ok_or_else(|| {
.map(|data| data.content) FlowyError::record_not_found().with_context(format!("Message not found: {}", message_id))
.ok_or_else(|| { })?;
FlowyError::record_not_found().with_context(format!("Message not found: {}", message_id))
})?;
Ok(content) Ok(row)
} }
fn handle_plugin_error(&self, err: PluginError) { fn handle_plugin_error(&self, err: PluginError) {
@ -79,17 +95,18 @@ impl ChatCloudService for AICloudServiceMiddleware {
self.cloud_service.create_chat(uid, workspace_id, chat_id) self.cloud_service.create_chat(uid, workspace_id, chat_id)
} }
fn create_question( async fn create_question(
&self, &self,
workspace_id: &str, workspace_id: &str,
chat_id: &str, chat_id: &str,
message: &str, message: &str,
message_type: ChatMessageType, message_type: ChatMessageType,
metadata: Vec<ChatMessageMetadata>, metadata: &[ChatMessageMetadata],
) -> FutureResult<ChatMessage, FlowyError> { ) -> Result<ChatMessage, FlowyError> {
self self
.cloud_service .cloud_service
.create_question(workspace_id, chat_id, message, message_type, metadata) .create_question(workspace_id, chat_id, message, message_type, metadata)
.await
} }
fn create_answer( fn create_answer(
@ -109,13 +126,13 @@ impl ChatCloudService for AICloudServiceMiddleware {
&self, &self,
workspace_id: &str, workspace_id: &str,
chat_id: &str, chat_id: &str,
message_id: i64, question_id: i64,
) -> Result<StreamAnswer, FlowyError> { ) -> Result<StreamAnswer, FlowyError> {
if self.local_llm_controller.is_running() { if self.local_llm_controller.is_running() {
let content = self.get_message_content(message_id)?; let row = self.get_message_record(question_id)?;
match self match self
.local_llm_controller .local_llm_controller
.stream_question(chat_id, &content) .stream_question(chat_id, &row.content, json!([]))
.await .await
{ {
Ok(stream) => Ok(LocalAIStreamAdaptor::new(stream).boxed()), Ok(stream) => Ok(LocalAIStreamAdaptor::new(stream).boxed()),
@ -127,7 +144,7 @@ impl ChatCloudService for AICloudServiceMiddleware {
} else { } else {
self self
.cloud_service .cloud_service
.stream_answer(workspace_id, chat_id, message_id) .stream_answer(workspace_id, chat_id, question_id)
.await .await
} }
} }
@ -139,7 +156,7 @@ impl ChatCloudService for AICloudServiceMiddleware {
question_message_id: i64, question_message_id: i64,
) -> Result<ChatMessage, FlowyError> { ) -> Result<ChatMessage, FlowyError> {
if self.local_llm_controller.is_running() { if self.local_llm_controller.is_running() {
let content = self.get_message_content(question_message_id)?; let content = self.get_message_record(question_message_id)?.content;
match self match self
.local_llm_controller .local_llm_controller
.ask_question(chat_id, &content) .ask_question(chat_id, &content)
@ -189,7 +206,10 @@ impl ChatCloudService for AICloudServiceMiddleware {
.local_llm_controller .local_llm_controller
.get_related_question(chat_id) .get_related_question(chat_id)
.await .await
.map_err(|err| FlowyError::local_ai().with_context(err))? .map_err(|err| FlowyError::local_ai().with_context(err))?;
trace!("LocalAI related questions: {:?}", questions);
let items = questions
.into_iter() .into_iter()
.map(|content| RelatedQuestion { .map(|content| RelatedQuestion {
content, content,
@ -197,10 +217,7 @@ impl ChatCloudService for AICloudServiceMiddleware {
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
Ok(RepeatedRelatedQuestion { Ok(RepeatedRelatedQuestion { message_id, items })
message_id,
items: questions,
})
} else { } else {
self self
.cloud_service .cloud_service
@ -248,7 +265,7 @@ impl ChatCloudService for AICloudServiceMiddleware {
if self.local_llm_controller.is_running() { if self.local_llm_controller.is_running() {
self self
.local_llm_controller .local_llm_controller
.index_file(chat_id, file_path.to_path_buf()) .index_file(chat_id, Some(file_path.to_path_buf()), None, None)
.await .await
.map_err(|err| FlowyError::local_ai().with_context(err))?; .map_err(|err| FlowyError::local_ai().with_context(err))?;
Ok(()) Ok(())

View File

@ -52,7 +52,7 @@ impl AIUserService for ChatUserServiceImpl {
self.upgrade_user()?.get_sqlite_connection(uid) self.upgrade_user()?.get_sqlite_connection(uid)
} }
fn data_root_dir(&self) -> Result<PathBuf, FlowyError> { fn application_root_dir(&self) -> Result<PathBuf, FlowyError> {
Ok(PathBuf::from( Ok(PathBuf::from(
self.upgrade_user()?.get_application_root_dir(), self.upgrade_user()?.get_application_root_dir(),
)) ))

View File

@ -611,25 +611,22 @@ impl ChatCloudService for ServerProvider {
}) })
} }
fn create_question( async fn create_question(
&self, &self,
workspace_id: &str, workspace_id: &str,
chat_id: &str, chat_id: &str,
message: &str, message: &str,
message_type: ChatMessageType, message_type: ChatMessageType,
metadata: Vec<ChatMessageMetadata>, metadata: &[ChatMessageMetadata],
) -> FutureResult<ChatMessage, FlowyError> { ) -> Result<ChatMessage, FlowyError> {
let workspace_id = workspace_id.to_string(); let workspace_id = workspace_id.to_string();
let chat_id = chat_id.to_string(); let chat_id = chat_id.to_string();
let message = message.to_string(); let message = message.to_string();
let server = self.get_server(); self
.get_server()?
FutureResult::new(async move { .chat_service()
server? .create_question(&workspace_id, &chat_id, &message, message_type, metadata)
.chat_service() .await
.create_question(&workspace_id, &chat_id, &message, message_type, metadata)
.await
})
} }
fn create_answer( fn create_answer(

View File

@ -52,14 +52,14 @@ where
}) })
} }
fn create_question( async fn create_question(
&self, &self,
workspace_id: &str, workspace_id: &str,
chat_id: &str, chat_id: &str,
message: &str, message: &str,
message_type: ChatMessageType, message_type: ChatMessageType,
metadata: Vec<ChatMessageMetadata>, metadata: &[ChatMessageMetadata],
) -> FutureResult<ChatMessage, FlowyError> { ) -> Result<ChatMessage, FlowyError> {
let workspace_id = workspace_id.to_string(); let workspace_id = workspace_id.to_string();
let chat_id = chat_id.to_string(); let chat_id = chat_id.to_string();
let try_get_client = self.inner.try_get_client(); let try_get_client = self.inner.try_get_client();
@ -69,13 +69,11 @@ where
metadata: Some(json!(metadata)), metadata: Some(json!(metadata)),
}; };
FutureResult::new(async move { let message = try_get_client?
let message = try_get_client? .create_question(&workspace_id, &chat_id, params)
.create_question(&workspace_id, &chat_id, params) .await
.await .map_err(FlowyError::from)?;
.map_err(FlowyError::from)?; Ok(message)
Ok(message)
})
} }
fn create_answer( fn create_answer(

View File

@ -23,17 +23,15 @@ impl ChatCloudService for DefaultChatCloudServiceImpl {
}) })
} }
fn create_question( async fn create_question(
&self, &self,
_workspace_id: &str, _workspace_id: &str,
_chat_id: &str, _chat_id: &str,
_message: &str, _message: &str,
_message_type: ChatMessageType, _message_type: ChatMessageType,
_metadata: Vec<ChatMessageMetadata>, _metadata: &[ChatMessageMetadata],
) -> FutureResult<ChatMessage, FlowyError> { ) -> Result<ChatMessage, FlowyError> {
FutureResult::new(async move { Err(FlowyError::not_support().with_context("Chat is not supported in local server."))
Err(FlowyError::not_support().with_context("Chat is not supported in local server."))
})
} }
fn create_answer( fn create_answer(