chore: polish UI and display attachment when using local ai (#5906)

* chore: polish UI

* chore: fix compile
This commit is contained in:
Nathan.fooo 2024-08-09 07:40:24 +08:00 committed by GitHub
parent f57297e76d
commit e82edc0419
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
49 changed files with 986 additions and 562 deletions

View File

@ -175,7 +175,7 @@ SPEC CHECKSUMS:
file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
flowy_infra_ui: 0455e1fa8c51885aa1437848e361e99419f34ebc
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
fluttertoast: e9a18c7be5413da53898f660530c56f35edfba9c
fluttertoast: 723e187574b149e68e63ca4d39b837586b903cfa
image_gallery_saver: cb43cc43141711190510e92c460eb1655cd343cb
image_picker_ios: 99dfe1854b4fa34d0364e74a78448a0151025425
integration_test: ce0a3ffa1de96d1a89ca0ac26fca7ea18a749ef4
@ -197,4 +197,4 @@ SPEC CHECKSUMS:
PODFILE CHECKSUM: d0d9b4ff572d8695c38eb3f9b490f55cdfc57eca
COCOAPODS: 1.15.2
COCOAPODS: 1.11.3

View File

@ -8,6 +8,8 @@ import 'package:fixnum/fixnum.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'chat_message_service.dart';
part 'chat_ai_message_bloc.freezed.dart';
class ChatAIMessageBloc extends Bloc<ChatAIMessageEvent, ChatAIMessageState> {
@ -16,10 +18,12 @@ class ChatAIMessageBloc extends Bloc<ChatAIMessageEvent, ChatAIMessageState> {
String? metadata,
required this.chatId,
required this.questionId,
}) : super(ChatAIMessageState.initial(
message,
chatMessageMetadataFromString(metadata),
),) {
}) : super(
ChatAIMessageState.initial(
message,
messageRefSourceFromString(metadata),
),
) {
if (state.stream != null) {
state.stream!.listen(
onData: (text) {
@ -37,9 +41,9 @@ class ChatAIMessageBloc extends Bloc<ChatAIMessageEvent, ChatAIMessageState> {
add(const ChatAIMessageEvent.onAIResponseLimit());
}
},
onMetadata: (metadata) {
onMetadata: (sources) {
if (!isClosed) {
add(ChatAIMessageEvent.receiveMetadata(metadata));
add(ChatAIMessageEvent.receiveSources(sources));
}
},
);
@ -112,10 +116,10 @@ class ChatAIMessageBloc extends Bloc<ChatAIMessageEvent, ChatAIMessageState> {
),
);
},
receiveMetadata: (List<ChatMessageMetadata> metadata) {
receiveSources: (List<ChatMessageRefSource> sources) {
emit(
state.copyWith(
metadata: metadata,
sources: sources,
),
);
},
@ -136,8 +140,8 @@ class ChatAIMessageEvent with _$ChatAIMessageEvent {
const factory ChatAIMessageEvent.retry() = _Retry;
const factory ChatAIMessageEvent.retryResult(String text) = _RetryResult;
const factory ChatAIMessageEvent.onAIResponseLimit() = _OnAIResponseLimit;
const factory ChatAIMessageEvent.receiveMetadata(
List<ChatMessageMetadata> data,
const factory ChatAIMessageEvent.receiveSources(
List<ChatMessageRefSource> sources,
) = _ReceiveMetadata;
}
@ -147,16 +151,18 @@ class ChatAIMessageState with _$ChatAIMessageState {
AnswerStream? stream,
required String text,
required MessageState messageState,
required List<ChatMessageMetadata> metadata,
required List<ChatMessageRefSource> sources,
}) = _ChatAIMessageState;
factory ChatAIMessageState.initial(
dynamic text, List<ChatMessageMetadata> metadata,) {
dynamic text,
List<ChatMessageRefSource> sources,
) {
return ChatAIMessageState(
text: text is String ? text : "",
stream: text is AnswerStream ? text : null,
messageState: const MessageState.ready(),
metadata: metadata,
sources: sources,
);
}
}

View File

@ -1,6 +1,5 @@
import 'dart:async';
import 'dart:collection';
import 'dart:convert';
import 'dart:ffi';
import 'dart:isolate';
@ -28,6 +27,8 @@ 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> {
ChatBloc({
@ -412,7 +413,7 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
author: User(id: "streamId:${nanoid()}"),
metadata: {
"$AnswerStream": stream,
"question": questionMessageId,
messageQuestionIdKey: questionMessageId,
"chatId": chatId,
},
id: streamMessageId,
@ -435,7 +436,7 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
text: message.content,
createdAt: message.createdAt.toInt() * 1000,
metadata: {
"metadata": message.metadata,
messageMetadataKey: message.metadata,
},
);
}
@ -593,7 +594,7 @@ class AnswerStream {
} else if (event.startsWith("metadata:")) {
if (_onMetadata != null) {
final s = event.substring(9);
_onMetadata!(chatMessageMetadataFromString(s));
_onMetadata!(messageRefSourceFromString(s));
}
} else if (event == "AI_RESPONSE_LIMIT") {
if (_onAIResponseLimit != null) {
@ -627,7 +628,7 @@ class AnswerStream {
void Function()? _onEnd;
void Function(String error)? _onError;
void Function()? _onAIResponseLimit;
void Function(List<ChatMessageMetadata> metadata)? _onMetadata;
void Function(List<ChatMessageRefSource> metadata)? _onMetadata;
int get nativePort => _port.sendPort.nativePort;
bool get hasStarted => _hasStarted;
@ -646,7 +647,7 @@ class AnswerStream {
void Function()? onEnd,
void Function(String error)? onError,
void Function()? onAIResponseLimit,
void Function(List<ChatMessageMetadata> metadata)? onMetadata,
void Function(List<ChatMessageRefSource> metadata)? onMetadata,
}) {
_onData = onData;
_onStart = onStart;
@ -661,55 +662,22 @@ class AnswerStream {
}
}
List<ChatMessageMetadata> chatMessageMetadataFromString(String? s) {
if (s == null || s.isEmpty || s == "null") {
return [];
}
final List<ChatMessageMetadata> metadata = [];
try {
final metadataJson = jsonDecode(s);
if (metadataJson == null) {
Log.warn("metadata is null");
return [];
}
if (metadataJson is Map<String, dynamic>) {
if (metadataJson.isNotEmpty) {
metadata.add(ChatMessageMetadata.fromJson(metadataJson));
}
} else if (metadataJson is List) {
metadata.addAll(
metadataJson.map(
(e) => ChatMessageMetadata.fromJson(e as Map<String, dynamic>),
),
);
} else {
Log.error("Invalid metadata: $metadataJson");
}
} catch (e) {
Log.error("Failed to parse metadata: $e");
}
return metadata;
}
@JsonSerializable()
class ChatMessageMetadata {
ChatMessageMetadata({
class ChatMessageRefSource {
ChatMessageRefSource({
required this.id,
required this.name,
required this.source,
});
factory ChatMessageMetadata.fromJson(Map<String, dynamic> json) =>
_$ChatMessageMetadataFromJson(json);
factory ChatMessageRefSource.fromJson(Map<String, dynamic> json) =>
_$ChatMessageRefSourceFromJson(json);
final String id;
final String name;
final String source;
Map<String, dynamic> toJson() => _$ChatMessageMetadataToJson(this);
Map<String, dynamic> toJson() => _$ChatMessageRefSourceToJson(this);
}
@freezed

View File

@ -1,20 +1,26 @@
import 'dart:async';
import 'dart:io';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/workspace/application/settings/ai/local_llm_listener.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/log.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:freezed_annotation/freezed_annotation.dart';
import 'package:path/path.dart' as path;
import 'chat_input_bloc.dart';
part 'chat_file_bloc.freezed.dart';
typedef ChatInputFileMetadata = Map<String, ChatFile>;
class ChatFileBloc extends Bloc<ChatFileEvent, ChatFileState> {
ChatFileBloc({
required String chatId,
}) : listener = LocalLLMListener(),
ChatFileBloc()
: listener = LocalLLMListener(),
super(const ChatFileState()) {
listener.start(
stateCallback: (pluginState) {
@ -49,38 +55,15 @@ class ChatFileBloc extends Bloc<ChatFileEvent, ChatFileState> {
},
newFile: (String filePath, String fileName) async {
final files = List<ChatFile>.from(state.uploadFiles);
files.add(ChatFile(filePath: filePath, fileName: fileName));
emit(
state.copyWith(
uploadFiles: files,
),
);
emit(
state.copyWith(
uploadFileIndicator: UploadFileIndicator.uploading(fileName),
),
);
final payload = ChatFilePB(filePath: filePath, chatId: chatId);
unawaited(
AIEventChatWithFile(payload).send().then((result) {
if (!isClosed) {
result.fold((_) {
add(
ChatFileEvent.updateUploadState(
UploadFileIndicator.finish(fileName),
),
);
}, (err) {
add(
ChatFileEvent.updateUploadState(
UploadFileIndicator.error(err.msg),
),
);
});
}
}),
);
final newFile = ChatFile.fromFilePath(filePath);
if (newFile != null) {
files.add(newFile);
emit(
state.copyWith(
uploadFiles: files,
),
);
}
},
updateChatState: (LocalAIChatPB chatState) {
// Only user enable chat with file and the plugin is already running
@ -109,6 +92,15 @@ class ChatFileBloc extends Bloc<ChatFileEvent, ChatFileState> {
),
);
},
deleteFile: (file) {
final files = List<ChatFile>.from(state.uploadFiles);
files.remove(file);
emit(
state.copyWith(
uploadFiles: files,
),
);
},
clear: () {
emit(
state.copyWith(
@ -116,14 +108,24 @@ class ChatFileBloc extends Bloc<ChatFileEvent, ChatFileState> {
),
);
},
updateUploadState: (UploadFileIndicator indicator) {
emit(state.copyWith(uploadFileIndicator: indicator));
},
);
},
);
}
ChatInputFileMetadata consumeMetaData() {
final metadata = state.uploadFiles.fold(
<String, ChatFile>{},
(map, file) => map..putIfAbsent(file.filePath, () => file),
);
if (metadata.isNotEmpty) {
add(const ChatFileEvent.clear());
}
return metadata;
}
final LocalLLMListener listener;
@override
@ -138,9 +140,8 @@ class ChatFileEvent with _$ChatFileEvent {
const factory ChatFileEvent.initial() = Initial;
const factory ChatFileEvent.newFile(String filePath, String fileName) =
_NewFile;
const factory ChatFileEvent.deleteFile(ChatFile file) = _DeleteFile;
const factory ChatFileEvent.clear() = _ClearFile;
const factory ChatFileEvent.updateUploadState(UploadFileIndicator indicator) =
_UpdateUploadState;
const factory ChatFileEvent.updateChatState(LocalAIChatPB chatState) =
_UpdateChatState;
const factory ChatFileEvent.updatePluginState(
@ -152,26 +153,69 @@ class ChatFileEvent with _$ChatFileEvent {
class ChatFileState with _$ChatFileState {
const factory ChatFileState({
@Default(false) bool supportChatWithFile,
UploadFileIndicator? uploadFileIndicator,
LocalAIChatPB? chatState,
@Default([]) List<ChatFile> uploadFiles,
@Default(AIType.appflowyAI()) AIType aiType,
}) = _ChatFileState;
}
@freezed
class UploadFileIndicator with _$UploadFileIndicator {
const factory UploadFileIndicator.finish(String fileName) = _Finish;
const factory UploadFileIndicator.uploading(String fileName) = _Uploading;
const factory UploadFileIndicator.error(String error) = _Error;
}
class ChatFile {
ChatFile({
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

@ -81,7 +81,7 @@ class ChatInputActionBloc
),
);
},
addPage: (ChatInputActionPage page) {
addPage: (ChatInputMention page) {
if (!state.selectedPages.any((p) => p.pageId == page.pageId)) {
final List<ViewActionPage> pages = _filterPages(
state.views,
@ -97,7 +97,7 @@ class ChatInputActionBloc
}
},
removePage: (String text) {
final List<ChatInputActionPage> selectedPages =
final List<ChatInputMention> selectedPages =
List.from(state.selectedPages);
selectedPages.retainWhere((t) => !text.contains(t.title));
@ -128,7 +128,7 @@ class ChatInputActionBloc
List<ViewActionPage> _filterPages(
List<ViewPB> views,
List<ChatInputActionPage> selectedPages,
List<ChatInputMention> selectedPages,
String filter,
) {
final pages = views
@ -152,7 +152,7 @@ List<ViewActionPage> _filterPages(
.toList();
}
class ViewActionPage extends ChatInputActionPage {
class ViewActionPage extends ChatInputMention {
ViewActionPage({required this.view});
final ViewPB view;
@ -182,8 +182,7 @@ class ChatInputActionEvent with _$ChatInputActionEvent {
const factory ChatInputActionEvent.handleKeyEvent(
PhysicalKeyboardKey keyboardKey,
) = _HandleKeyEvent;
const factory ChatInputActionEvent.addPage(ChatInputActionPage page) =
_AddPage;
const factory ChatInputActionEvent.addPage(ChatInputMention page) = _AddPage;
const factory ChatInputActionEvent.removePage(String text) = _RemovePage;
const factory ChatInputActionEvent.clear() = _Clear;
}
@ -192,8 +191,8 @@ class ChatInputActionEvent with _$ChatInputActionEvent {
class ChatInputActionState with _$ChatInputActionState {
const factory ChatInputActionState({
@Default([]) List<ViewPB> views,
@Default([]) List<ChatInputActionPage> pages,
@Default([]) List<ChatInputActionPage> selectedPages,
@Default([]) List<ChatInputMention> pages,
@Default([]) List<ChatInputMention> selectedPages,
@Default("") String filter,
ChatInputKeyboardEvent? keyboardKey,
@Default(ChatActionMenuIndicator.loading())

View File

@ -5,14 +5,15 @@ import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
abstract class ChatInputActionPage extends Equatable {
abstract class ChatInputMention extends Equatable {
String get title;
String get pageId;
dynamic get page;
Widget get icon;
}
typedef ChatInputMetadata = Map<String, ChatInputActionPage>;
/// Key: the key is the pageId
typedef ChatInputMentionMetadata = Map<String, ChatInputMention>;
class ChatInputActionControl extends ChatActionHandler {
ChatInputActionControl({
@ -35,9 +36,9 @@ class ChatInputActionControl extends ChatActionHandler {
List<String> get tags =>
_commandBloc.state.selectedPages.map((e) => e.title).toList();
ChatInputMetadata consumeMetaData() {
ChatInputMentionMetadata consumeMetaData() {
final metadata = _commandBloc.state.selectedPages.fold(
<String, ChatInputActionPage>{},
<String, ChatInputMention>{},
(map, page) => map..putIfAbsent(page.pageId, () => page),
);
@ -70,7 +71,7 @@ class ChatInputActionControl extends ChatActionHandler {
}
@override
void onSelected(ChatInputActionPage page) {
void onSelected(ChatInputMention page) {
_commandBloc.add(ChatInputActionEvent.addPage(page));
textController.text = "$_showMenuText${page.title}";

View File

@ -81,3 +81,7 @@ class AIType with _$AIType {
const factory AIType.appflowyAI() = _AppFlowyAI;
const factory AIType.localAI() = _LocalAI;
}
extension AITypeX on AIType {
bool isLocalAI() => this is _LocalAI;
}

View File

@ -0,0 +1,78 @@
import 'dart:async';
import 'package:appflowy/plugins/ai_chat/application/chat_file_bloc.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'chat_input_file_bloc.freezed.dart';
class ChatInputFileBloc extends Bloc<ChatInputFileEvent, ChatInputFileState> {
ChatInputFileBloc({
required String chatId,
required this.file,
}) : super(const ChatInputFileState()) {
on<ChatInputFileEvent>(
(event, emit) async {
await event.when(
initial: () async {
final payload = ChatFilePB(
filePath: file.filePath,
chatId: chatId,
);
unawaited(
AIEventChatWithFile(payload).send().then((result) {
if (!isClosed) {
result.fold(
(_) {
add(
const ChatInputFileEvent.updateUploadState(
UploadFileIndicator.finish(),
),
);
},
(err) {
add(
ChatInputFileEvent.updateUploadState(
UploadFileIndicator.error(err.toString()),
),
);
},
);
}
}),
);
},
updateUploadState: (UploadFileIndicator indicator) {
emit(state.copyWith(uploadFileIndicator: indicator));
},
);
},
);
}
final ChatFile file;
}
@freezed
class ChatInputFileEvent with _$ChatInputFileEvent {
const factory ChatInputFileEvent.initial() = Initial;
const factory ChatInputFileEvent.updateUploadState(
UploadFileIndicator indicator,
) = _UpdateUploadState;
}
@freezed
class ChatInputFileState with _$ChatInputFileState {
const factory ChatInputFileState({
UploadFileIndicator? uploadFileIndicator,
}) = _ChatInputFileState;
}
@freezed
class UploadFileIndicator with _$UploadFileIndicator {
const factory UploadFileIndicator.finish() = _Finish;
const factory UploadFileIndicator.uploading() = _Uploading;
const factory UploadFileIndicator.error(String error) = _Error;
}

View File

@ -1,3 +1,6 @@
import 'dart:convert';
import 'package:appflowy/plugins/ai_chat/application/chat_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_backend/dispatch/dispatch.dart';
@ -5,6 +8,95 @@ import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-ai/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-document/entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:nanoid/nanoid.dart';
import 'chat_file_bloc.dart';
List<ChatFile> fileListFromMessageMetadata(
Map<String, dynamic>? map,
) {
final List<ChatFile> metadata = [];
if (map != null) {
for (final entry in map.entries) {
if (entry.value is ChatFile) {
metadata.add(entry.value);
}
}
}
return metadata;
}
List<ChatFile> chatFilesFromMetadataString(String? s) {
if (s == null || s.isEmpty || s == "null") {
return [];
}
final metadataJson = jsonDecode(s);
if (metadataJson is Map<String, dynamic>) {
return _parseChatFile(metadataJson);
} else if (metadataJson is List) {
return metadataJson
.map((e) => e as Map<String, dynamic>)
.map(chatFileFromMap)
.where((file) => file != null)
.cast<ChatFile>()
.toList();
} else {
Log.error("Invalid metadata: $metadataJson");
return [];
}
}
List<ChatFile> _parseChatFile(Map<String, dynamic> map) {
final file = chatFileFromMap(map);
return file != null ? [file] : [];
}
ChatFile? chatFileFromMap(Map<String, dynamic>? map) {
if (map == null) return null;
final filePath = map['source'] as String?;
final fileName = map['name'] as String?;
if (filePath == null || fileName == null) {
return null;
}
return ChatFile.fromFilePath(filePath);
}
List<ChatMessageRefSource> messageRefSourceFromString(String? s) {
if (s == null || s.isEmpty || s == "null") {
return [];
}
final List<ChatMessageRefSource> metadata = [];
try {
final metadataJson = jsonDecode(s);
if (metadataJson == null) {
Log.warn("metadata is null");
return [];
}
if (metadataJson is Map<String, dynamic>) {
if (metadataJson.isNotEmpty) {
metadata.add(ChatMessageRefSource.fromJson(metadataJson));
}
} else if (metadataJson is List) {
metadata.addAll(
metadataJson.map(
(e) => ChatMessageRefSource.fromJson(e as Map<String, dynamic>),
),
);
} else {
Log.error("Invalid metadata: $metadataJson");
}
} catch (e) {
Log.error("Failed to parse metadata: $e");
}
return metadata;
}
Future<List<ChatMessageMetaPB>> metadataPBFromMetadata(
Map<String, dynamic>? map,
@ -24,6 +116,7 @@ Future<List<ChatMessageMetaPB>> metadataPBFromMetadata(
id: view.id,
name: view.name,
data: pb.text,
dataType: ChatMessageMetaTypePB.Txt,
source: "appflowy document",
),
);
@ -32,6 +125,16 @@ Future<List<ChatMessageMetaPB>> metadataPBFromMetadata(
});
}
}
} else if (entry.value is ChatFile) {
metadata.add(
ChatMessageMetaPB(
id: nanoid(8),
name: entry.value.fileName,
data: entry.value.filePath,
dataType: entry.value.fileType,
source: entry.value.filePath,
),
);
}
}
}

View File

@ -19,7 +19,7 @@ class ChatSidePannelBloc
on<ChatSidePannelEvent>(
(event, emit) async {
await event.when(
selectedMetadata: (ChatMessageMetadata metadata) async {
selectedMetadata: (ChatMessageRefSource metadata) async {
emit(
state.copyWith(
metadata: metadata,
@ -62,7 +62,7 @@ class ChatSidePannelBloc
@freezed
class ChatSidePannelEvent with _$ChatSidePannelEvent {
const factory ChatSidePannelEvent.selectedMetadata(
ChatMessageMetadata metadata,
ChatMessageRefSource metadata,
) = _SelectedMetadata;
const factory ChatSidePannelEvent.close() = _Close;
const factory ChatSidePannelEvent.open(ViewPB view) = _Open;
@ -71,7 +71,7 @@ class ChatSidePannelEvent with _$ChatSidePannelEvent {
@freezed
class ChatSidePannelState with _$ChatSidePannelState {
const factory ChatSidePannelState({
ChatMessageMetadata? metadata,
ChatMessageRefSource? metadata,
@Default(ChatSidePannelIndicator.loading())
ChatSidePannelIndicator indicator,
@Default(false) bool isShowPannel,

View File

@ -1,23 +1,27 @@
import 'package:appflowy/plugins/ai_chat/application/chat_member_bloc.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_chat_types/flutter_chat_types.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'chat_file_bloc.dart';
import 'chat_message_service.dart';
part 'chat_user_message_bloc.freezed.dart';
class ChatUserMessageBloc
extends Bloc<ChatUserMessageEvent, ChatUserMessageState> {
ChatUserMessageBloc({
required Message message,
required ChatMember? member,
}) : super(ChatUserMessageState.initial(message, member)) {
required TextMessage message,
required String? metadata,
}) : super(
ChatUserMessageState.initial(
message,
chatFilesFromMetadataString(metadata),
),
) {
on<ChatUserMessageEvent>(
(event, emit) async {
event.when(
initial: () {},
refreshMember: (ChatMember member) {
emit(state.copyWith(member: member));
},
);
},
);
@ -27,20 +31,18 @@ class ChatUserMessageBloc
@freezed
class ChatUserMessageEvent with _$ChatUserMessageEvent {
const factory ChatUserMessageEvent.initial() = Initial;
const factory ChatUserMessageEvent.refreshMember(ChatMember member) =
_MemberInfo;
}
@freezed
class ChatUserMessageState with _$ChatUserMessageState {
const factory ChatUserMessageState({
required Message message,
ChatMember? member,
required TextMessage message,
required List<ChatFile> files,
}) = _ChatUserMessageState;
factory ChatUserMessageState.initial(
Message message,
ChatMember? member,
TextMessage message,
List<ChatFile> files,
) =>
ChatUserMessageState(message: message, member: member);
ChatUserMessageState(message: message, files: files);
}

View File

@ -8,9 +8,6 @@ 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/other_user_message_bubble.dart';
import 'package:appflowy/plugins/ai_chat/presentation/message/user_message_bubble.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart';
import 'package:appflowy/workspace/presentation/home/toast.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:desktop_drop/desktop_drop.dart';
@ -19,7 +16,6 @@ import 'package:flowy_infra/platform_extension.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_chat_types/flutter_chat_types.dart' as types;
import 'package:flutter_chat_types/flutter_chat_types.dart';
@ -29,7 +25,6 @@ import 'package:styled_widget/styled_widget.dart';
import 'application/chat_member_bloc.dart';
import 'application/chat_side_pannel_bloc.dart';
import 'presentation/chat_input/chat_input.dart';
import 'presentation/chat_popmenu.dart';
import 'presentation/chat_side_pannel.dart';
import 'presentation/chat_theme.dart';
import 'presentation/chat_user_invalid_message.dart';
@ -88,8 +83,7 @@ class AIChatPage extends StatelessWidget {
/// [ChatFileBloc] is used to handle file indexing as a chat context
BlocProvider(
create: (_) => ChatFileBloc(chatId: view.id)
..add(const ChatFileEvent.initial()),
create: (_) => ChatFileBloc()..add(const ChatFileEvent.initial()),
),
/// [ChatInputStateBloc] is used to handle chat input text field state
@ -100,40 +94,24 @@ class AIChatPage extends StatelessWidget {
BlocProvider(create: (_) => ChatSidePannelBloc(chatId: view.id)),
BlocProvider(create: (_) => ChatMemberBloc()),
],
child: BlocListener<ChatFileBloc, ChatFileState>(
listenWhen: (previous, current) =>
previous.uploadFileIndicator != current.uploadFileIndicator,
listener: (context, state) {
_handleIndexIndicator(state.uploadFileIndicator, context);
},
child: BlocBuilder<ChatFileBloc, ChatFileState>(
builder: (context, state) {
return DropTarget(
onDragDone: (DropDoneDetails detail) async {
if (state.supportChatWithFile) {
await showConfirmDialog(
context: context,
style: ConfirmPopupStyle.cancelAndOk,
title: LocaleKeys.chat_chatWithFilePrompt.tr(),
confirmLabel: LocaleKeys.button_confirm.tr(),
onConfirm: () {
for (final file in detail.files) {
context
.read<ChatFileBloc>()
.add(ChatFileEvent.newFile(file.path, file.name));
}
},
description: '',
);
child: BlocBuilder<ChatFileBloc, ChatFileState>(
builder: (context, state) {
return DropTarget(
onDragDone: (DropDoneDetails detail) async {
if (state.supportChatWithFile) {
for (final file in detail.files) {
context
.read<ChatFileBloc>()
.add(ChatFileEvent.newFile(file.path, file.name));
}
},
child: _ChatContentPage(
view: view,
userProfile: userProfile,
),
);
},
),
}
},
child: _ChatContentPage(
view: view,
userProfile: userProfile,
),
);
},
),
);
}
@ -145,35 +123,6 @@ class AIChatPage extends StatelessWidget {
),
);
}
void _handleIndexIndicator(
UploadFileIndicator? indicator,
BuildContext context,
) {
if (indicator != null) {
indicator.when(
finish: (fileName) {
showSnackBarMessage(
context,
LocaleKeys.chat_indexFileSuccess.tr(args: [fileName]),
);
},
uploading: (fileName) {
showSnackBarMessage(
context,
LocaleKeys.chat_indexingFile.tr(args: [fileName]),
duration: const Duration(seconds: 2),
);
},
error: (err) {
showSnackBarMessage(
context,
err,
);
},
);
}
}
}
class _ChatContentPage extends StatefulWidget {
@ -302,7 +251,7 @@ class _ChatContentPageState extends State<_ChatContentPage> {
// We use custom bottom widget for chat input, so
// do not need to handle this event.
},
customBottomWidget: buildBottom(blocContext),
customBottomWidget: _buildBottom(blocContext),
user: _user,
theme: buildTheme(context),
onEndReached: () async {
@ -319,6 +268,7 @@ class _ChatContentPageState extends State<_ChatContentPage> {
? Padding(
padding: AIChatUILayout.welcomePagePadding,
child: ChatWelcomePage(
userProfile: widget.userProfile,
onSelectedQuestion: (question) => blocContext
.read<ChatBloc>()
.add(ChatEvent.sendMessage(message: question)),
@ -339,36 +289,46 @@ class _ChatContentPageState extends State<_ChatContentPage> {
child, {
required message,
required nextMessageInGroup,
}) {
if (message.author.id == _user.id) {
return ChatUserMessageBubble(
message: message,
child: child,
);
} else if (isOtherUserMessage(message)) {
return OtherUserMessageBubble(
message: message,
child: child,
);
} else {
return _buildAIBubble(message, blocContext, state, child);
}
},
}) =>
_buildBubble(blocContext, message, child, state),
),
);
}
Widget _buildBubble(
BuildContext blocContext,
Message message,
Widget child,
ChatState state,
) {
if (message.author.id == _user.id) {
return ChatUserMessageBubble(
message: message,
child: child,
);
} else if (isOtherUserMessage(message)) {
return OtherUserMessageBubble(
message: message,
child: child,
);
} else {
return _buildAIBubble(message, blocContext, state, child);
}
}
Widget _buildTextMessage(BuildContext context, TextMessage message) {
if (message.author.id == _user.id) {
return ChatTextMessageWidget(
final metadata = message.metadata?[messageMetadataKey] as String?;
return ChatUserTextMessageWidget(
user: message.author,
messageUserId: message.id,
text: message.text,
message: message,
metadata: metadata,
);
} else {
final stream = message.metadata?["$AnswerStream"];
final questionId = message.metadata?["question"];
final metadata = message.metadata?["metadata"] as String?;
final questionId = message.metadata?[messageQuestionIdKey];
final metadata = message.metadata?[messageMetadataKey] as String?;
return ChatAITextMessageWidget(
user: message.author,
messageUserId: message.id,
@ -377,7 +337,7 @@ class _ChatContentPageState extends State<_ChatContentPage> {
questionId: questionId,
chatId: widget.view.id,
metadata: metadata,
onSelectedMetadata: (ChatMessageMetadata metadata) {
onSelectedMetadata: (ChatMessageRefSource metadata) {
context.read<ChatSidePannelBloc>().add(
ChatSidePannelEvent.selectedMetadata(metadata),
);
@ -424,68 +384,7 @@ class _ChatContentPageState extends State<_ChatContentPage> {
);
}
Widget buildBubble(Message message, Widget child) {
final isAuthor = message.author.id == _user.id;
const borderRadius = BorderRadius.all(Radius.circular(6));
final childWithPadding = isAuthor
? Padding(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
child: child,
)
: Padding(
padding: const EdgeInsets.all(8),
child: child,
);
// If the message is from the author, we will decorate it with a different color
final decoratedChild = isAuthor
? DecoratedBox(
decoration: BoxDecoration(
borderRadius: borderRadius,
color: !isAuthor || message.type == types.MessageType.image
? AFThemeExtension.of(context).tint1
: Theme.of(context).colorScheme.secondary,
),
child: childWithPadding,
)
: childWithPadding;
// If the message is from the author, no further actions are needed
if (isAuthor) {
return ClipRRect(
borderRadius: borderRadius,
child: decoratedChild,
);
} else {
if (isMobile) {
return ChatPopupMenu(
onAction: (action) {
switch (action) {
case ChatMessageAction.copy:
if (message is TextMessage) {
Clipboard.setData(ClipboardData(text: message.text));
showMessageToast(LocaleKeys.grid_row_copyProperty.tr());
}
break;
}
},
builder: (context) =>
ClipRRect(borderRadius: borderRadius, child: decoratedChild),
);
} else {
// Show hover effect only on desktop
return ClipRRect(
borderRadius: borderRadius,
child: ChatAIMessageHover(
message: message,
child: decoratedChild,
),
);
}
}
}
Widget buildBottom(BuildContext context) {
Widget _buildBottom(BuildContext context) {
return ClipRect(
child: Padding(
padding: AIChatUILayout.safeAreaInsets(context),

View File

@ -17,10 +17,11 @@ class ChatInputAtButton extends StatelessWidget {
message: LocaleKeys.chat_clickToMention.tr(),
child: FlowyIconButton(
hoverColor: AFThemeExtension.of(context).lightGreyHover,
radius: BorderRadius.circular(18),
icon: const FlowySvg(
FlowySvgs.mention_s,
size: Size.square(20),
radius: BorderRadius.circular(6),
icon: FlowySvg(
FlowySvgs.chat_at_s,
size: const Size.square(20),
color: Colors.grey.shade600,
),
onPressed: onTap,
),

View File

@ -2,6 +2,7 @@ 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_control.dart';
import 'package:appflowy/plugins/ai_chat/application/chat_input_bloc.dart';
import 'package:appflowy/plugins/ai_chat/presentation/chat_input/chat_input_file.dart';
import 'package:appflowy/plugins/ai_chat/presentation/chat_input_action_menu.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mobile_page_selector_sheet.dart';
import 'package:appflowy/startup/startup.dart';
@ -18,9 +19,10 @@ import 'package:flutter_chat_types/flutter_chat_types.dart' as types;
import 'package:flutter_chat_ui/flutter_chat_ui.dart';
import 'chat_at_button.dart';
import 'chat_attachment.dart';
import 'chat_input_span.dart';
import 'chat_input_attachment.dart';
import 'chat_send_button.dart';
import 'chat_input_span.dart';
import 'layout_define.dart';
class ChatInput extends StatefulWidget {
/// Creates [ChatInput] widget.
@ -105,16 +107,6 @@ class _ChatInputState extends State<ChatInput> {
@override
Widget build(BuildContext context) {
const buttonPadding = EdgeInsets.symmetric(horizontal: 2);
const inputPadding = EdgeInsets.all(6);
final textPadding = isMobile
? const EdgeInsets.only(left: 8.0, right: 4.0)
: const EdgeInsets.symmetric(horizontal: 16);
final borderRadius = BorderRadius.circular(isMobile ? 10 : 30);
final color = isMobile
? Colors.transparent
: Theme.of(context).colorScheme.surfaceContainerHighest;
return Padding(
padding: inputPadding,
// ignore: use_decorated_box
@ -123,7 +115,7 @@ class _ChatInputState extends State<ChatInput> {
border: Border.all(
color: _inputFocusNode.hasFocus && !isMobile
? Theme.of(context).colorScheme.primary.withOpacity(0.6)
: Colors.transparent,
: Colors.grey.shade700,
),
borderRadius: borderRadius,
),
@ -132,17 +124,50 @@ class _ChatInputState extends State<ChatInput> {
color: color,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 10),
child: Row(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// TODO(lucas): support mobile
if (PlatformExtension.isDesktop &&
widget.aiType == const AIType.localAI())
_attachmentButton(buttonPadding),
Expanded(child: _inputTextField(context, textPadding)),
if (context.read<ChatFileBloc>().state.uploadFiles.isNotEmpty)
Padding(
padding: EdgeInsets.only(
top: 12,
bottom: 12,
left: textPadding.left + sendButtonSize,
right: textPadding.right,
),
child: BlocBuilder<ChatFileBloc, ChatFileState>(
builder: (context, state) {
return ChatInputFile(
chatId: widget.chatId,
files: state.uploadFiles,
onDeleted: (file) => context.read<ChatFileBloc>().add(
ChatFileEvent.deleteFile(file),
),
);
},
),
),
if (widget.aiType == const AIType.appflowyAI())
_atButton(buttonPadding),
_sendButton(buttonPadding),
//
Row(
children: [
// TODO(lucas): support mobile
if (PlatformExtension.isDesktop &&
widget.aiType == const AIType.localAI())
_attachmentButton(buttonPadding),
// text field
Expanded(child: _inputTextField(context, textPadding)),
// at button
if (PlatformExtension.isDesktop &&
widget.aiType == const AIType.appflowyAI())
_atButton(buttonPadding),
// send button
_sendButton(buttonPadding),
],
),
],
),
),
@ -161,9 +186,20 @@ class _ChatInputState extends State<ChatInput> {
void _handleSendPressed() {
final trimmedText = _textController.text.trim();
if (trimmedText != '') {
// consume metadata
final ChatInputMentionMetadata mentionPageMetadata =
_inputActionControl.consumeMetaData();
final ChatInputFileMetadata fileMetadata =
context.read<ChatFileBloc>().consumeMetaData();
// combine metadata
final Map<String, dynamic> metadata = {}
..addAll(mentionPageMetadata)
..addAll(fileMetadata);
final partialText = types.PartialText(
text: trimmedText,
metadata: _inputActionControl.consumeMetaData(),
metadata: metadata,
);
widget.onSendPressed(partialText);
_textController.clear();
@ -206,37 +242,13 @@ class _ChatInputState extends State<ChatInput> {
}
InputDecoration _buildInputDecoration(BuildContext context) {
if (!isMobile) {
return InputDecoration(
border: InputBorder.none,
hintText: widget.hintText,
focusedBorder: InputBorder.none,
hintStyle: TextStyle(
color: AFThemeExtension.of(context).textColor.withOpacity(0.5),
),
);
}
final borderRadius = BorderRadius.circular(10);
return InputDecoration(
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
border: InputBorder.none,
hintText: widget.hintText,
focusedBorder: InputBorder.none,
hintStyle: TextStyle(
color: AFThemeExtension.of(context).textColor.withOpacity(0.5),
),
enabledBorder: OutlineInputBorder(
borderRadius: borderRadius,
borderSide: BorderSide(
color: Theme.of(context).colorScheme.outline,
),
),
focusedBorder: OutlineInputBorder(
borderRadius: borderRadius,
borderSide: BorderSide(
color: Theme.of(context).colorScheme.primary,
width: 1.2,
),
),
);
}
@ -293,7 +305,7 @@ class _ChatInputState extends State<ChatInput> {
return Padding(
padding: buttonPadding,
child: SizedBox.square(
dimension: 26,
dimension: sendButtonSize,
child: ChatInputSendButton(
onSendPressed: () {
if (!_sendButtonEnabled) {
@ -317,7 +329,7 @@ class _ChatInputState extends State<ChatInput> {
return Padding(
padding: buttonPadding,
child: SizedBox.square(
dimension: 26,
dimension: attachButtonSize,
child: ChatInputAttachment(
onTap: () async {
final path = await getIt<FilePickerService>().pickFiles(
@ -348,7 +360,7 @@ class _ChatInputState extends State<ChatInput> {
return Padding(
padding: buttonPadding,
child: SizedBox.square(
dimension: 26,
dimension: attachButtonSize,
child: ChatInputAtButton(
onTap: () {
_textController.text += '@';

View File

@ -21,7 +21,7 @@ class ChatInputAttachment extends StatelessWidget {
icon: FlowySvg(
FlowySvgs.ai_attachment_s,
size: const Size.square(20),
color: Theme.of(context).colorScheme.primary,
color: Colors.grey.shade600,
),
onPressed: onTap,
),

View File

@ -0,0 +1,129 @@
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_input_file_bloc.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:styled_widget/styled_widget.dart';
class ChatInputFile extends StatelessWidget {
const ChatInputFile({
required this.chatId,
required this.files,
required this.onDeleted,
super.key,
});
final List<ChatFile> files;
final String chatId;
final Function(ChatFile) onDeleted;
@override
Widget build(BuildContext context) {
final List<Widget> children = files
.map(
(file) => ChatFilePreview(
chatId: chatId,
file: file,
onDeleted: onDeleted,
),
)
.toList();
return Wrap(
spacing: 6,
runSpacing: 6,
children: children,
);
}
}
class ChatFilePreview extends StatelessWidget {
const ChatFilePreview({
required this.chatId,
required this.file,
required this.onDeleted,
super.key,
});
final String chatId;
final ChatFile file;
final Function(ChatFile) onDeleted;
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => ChatInputFileBloc(chatId: chatId, file: file)
..add(const ChatInputFileEvent.initial()),
child: BlocBuilder<ChatInputFileBloc, ChatInputFileState>(
builder: (context, state) {
return FlowyHover(
builder: (context, onHover) {
return ConstrainedBox(
constraints: const BoxConstraints(
maxWidth: 260,
),
child: DecoratedBox(
decoration: BoxDecoration(
color:
Theme.of(context).colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(4),
),
child: Stack(
clipBehavior: Clip.none,
children: [
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8.0,
vertical: 10,
),
child: Row(
children: [
file.fileType.icon,
const HSpace(6),
Flexible(
child: FlowyText(
file.fileName,
fontSize: 12,
maxLines: 6,
),
),
],
),
),
if (onHover)
_CloseButton(
onPressed: () => onDeleted(file),
).positioned(top: -6, right: -6),
],
),
),
);
},
);
},
),
);
}
}
class _CloseButton extends StatelessWidget {
const _CloseButton({required this.onPressed});
final VoidCallback onPressed;
@override
Widget build(BuildContext context) {
return FlowyIconButton(
width: 24,
height: 24,
isSelected: true,
radius: BorderRadius.circular(12),
fillColor: Theme.of(context).colorScheme.surfaceContainer,
icon: const FlowySvg(
FlowySvgs.close_s,
size: Size.square(20),
),
onPressed: onPressed,
);
}
}

View File

@ -39,7 +39,9 @@ class ChatInputSendButton extends StatelessWidget {
icon: FlowySvg(
FlowySvgs.send_s,
size: const Size.square(14),
color: enabled ? Theme.of(context).colorScheme.primary : null,
color: enabled
? Theme.of(context).colorScheme.primary
: Colors.grey.shade600,
),
onPressed: onSendPressed,
);

View File

@ -0,0 +1,13 @@
import 'package:flutter/material.dart';
import 'chat_input.dart';
const double sendButtonSize = 26;
const double attachButtonSize = 26;
const buttonPadding = EdgeInsets.symmetric(horizontal: 2);
const inputPadding = EdgeInsets.all(6);
final textPadding = isMobile
? const EdgeInsets.only(left: 8.0, right: 4.0)
: const EdgeInsets.symmetric(horizontal: 16);
final borderRadius = BorderRadius.circular(30);
const color = Colors.transparent;

View File

@ -13,7 +13,7 @@ import 'package:scroll_to_index/scroll_to_index.dart';
abstract class ChatActionHandler {
void onEnter();
void onSelected(ChatInputActionPage page);
void onSelected(ChatInputMention page);
void onExit();
ChatInputActionBloc get commandBloc;
void onFilter(String filter);
@ -136,7 +136,7 @@ class _ActionItem extends StatelessWidget {
required this.isSelected,
});
final ChatInputActionPage item;
final ChatInputMention item;
final VoidCallback? onTap;
final bool isSelected;
@ -175,7 +175,7 @@ class ActionList extends StatefulWidget {
final ChatActionHandler handler;
final VoidCallback? onDismiss;
final List<ChatInputActionPage> pages;
final List<ChatInputMention> pages;
final bool isLoading;
@override
@ -257,7 +257,7 @@ class _ActionListState extends State<ActionList> {
return widget.pages.asMap().entries.map((entry) {
final index = entry.key;
final ChatInputActionPage item = entry.value;
final ChatInputMention item = entry.value;
return AutoScrollTag(
key: ValueKey(item.pageId),
index: index,

View File

@ -1,66 +1,109 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
import 'package:easy_localization/easy_localization.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/text.dart';
import 'package:flutter/material.dart';
import 'chat_input/chat_input.dart';
class WelcomeQuestion {
WelcomeQuestion({
required this.text,
required this.iconData,
});
final String text;
final FlowySvgData iconData;
}
class ChatWelcomePage extends StatelessWidget {
ChatWelcomePage({required this.onSelectedQuestion, super.key});
ChatWelcomePage({
required this.userProfile,
required this.onSelectedQuestion,
super.key,
});
final void Function(String) onSelectedQuestion;
final UserProfilePB userProfile;
final List<String> items = [
LocaleKeys.chat_question1.tr(),
LocaleKeys.chat_question2.tr(),
LocaleKeys.chat_question3.tr(),
LocaleKeys.chat_question4.tr(),
final List<WelcomeQuestion> items = [
WelcomeQuestion(
text: LocaleKeys.chat_question1.tr(),
iconData: FlowySvgs.chat_lightbulb_s,
),
WelcomeQuestion(
text: LocaleKeys.chat_question2.tr(),
iconData: FlowySvgs.chat_scholar_s,
),
WelcomeQuestion(
text: LocaleKeys.chat_question3.tr(),
iconData: FlowySvgs.chat_question_s,
),
WelcomeQuestion(
text: LocaleKeys.chat_question4.tr(),
iconData: FlowySvgs.chat_feather_s,
),
];
@override
Widget build(BuildContext context) {
return AnimatedOpacity(
opacity: 1.0,
duration: const Duration(seconds: 3),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const FlowySvg(
FlowySvgs.flowy_ai_chat_logo_s,
size: Size.square(44),
),
const SizedBox(height: 40),
Wrap(
children: items
.map(
(i) => WelcomeQuestion(
question: i,
onSelected: onSelectedQuestion,
),
)
.toList(),
),
],
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Spacer(),
Opacity(
opacity: 0.8,
child: FlowyText(
fontSize: 15,
LocaleKeys.chat_questionDetail.tr(args: [userProfile.name]),
),
),
const VSpace(18),
Opacity(
opacity: 0.6,
child: FlowyText(
LocaleKeys.chat_questionTitle.tr(),
),
),
const VSpace(8),
Wrap(
direction: Axis.vertical,
children: items
.map(
(i) => WelcomeQuestionWidget(
question: i,
onSelected: onSelectedQuestion,
),
)
.toList(),
),
const VSpace(20),
],
),
),
);
}
}
class WelcomeQuestion extends StatelessWidget {
const WelcomeQuestion({
class WelcomeQuestionWidget extends StatelessWidget {
const WelcomeQuestionWidget({
required this.question,
required this.onSelected,
super.key,
});
final void Function(String) onSelected;
final String question;
final WelcomeQuestion question;
@override
Widget build(BuildContext context) {
return InkWell(
onTap: () => onSelected(question),
onTap: () => onSelected(question.text),
child: GestureDetector(
behavior: HitTestBehavior.opaque,
child: FlowyHover(
@ -70,12 +113,18 @@ class WelcomeQuestion extends StatelessWidget {
borderRadius: BorderRadius.circular(6),
),
child: Padding(
padding: const EdgeInsets.all(10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
FlowySvg(
question.iconData,
size: const Size.square(18),
blendMode: null,
),
const HSpace(16),
FlowyText(
question,
question.text,
maxLines: null,
),
],

View File

@ -8,23 +8,23 @@ import 'package:flutter/material.dart';
class AIMessageMetadata extends StatelessWidget {
const AIMessageMetadata({
required this.metadata,
required this.sources,
required this.onSelectedMetadata,
super.key,
});
final List<ChatMessageMetadata> metadata;
final Function(ChatMessageMetadata metadata) onSelectedMetadata;
final List<ChatMessageRefSource> sources;
final Function(ChatMessageRefSource metadata) onSelectedMetadata;
@override
Widget build(BuildContext context) {
final title = metadata.length == 1
? LocaleKeys.chat_referenceSource.tr(args: [metadata.length.toString()])
final title = sources.length == 1
? LocaleKeys.chat_referenceSource.tr(args: [sources.length.toString()])
: LocaleKeys.chat_referenceSources
.tr(args: [metadata.length.toString()]);
.tr(args: [sources.length.toString()]);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (metadata.isNotEmpty)
if (sources.isNotEmpty)
Opacity(
opacity: 0.5,
child: FlowyText(title, fontSize: 12),
@ -33,7 +33,7 @@ class AIMessageMetadata extends StatelessWidget {
Wrap(
spacing: 8.0,
runSpacing: 4.0,
children: metadata
children: sources
.map(
(m) => SizedBox(
height: 24,

View File

@ -32,7 +32,7 @@ class ChatAITextMessageWidget extends StatelessWidget {
final Int64? questionId;
final String chatId;
final String? metadata;
final void Function(ChatMessageMetadata metadata) onSelectedMetadata;
final void Function(ChatMessageRefSource metadata) onSelectedMetadata;
@override
Widget build(BuildContext context) {
@ -71,7 +71,7 @@ class ChatAITextMessageWidget extends StatelessWidget {
children: [
AIMarkdownText(markdown: state.text),
AIMessageMetadata(
metadata: state.metadata,
sources: state.sources,
onSelectedMetadata: onSelectedMetadata,
),
],

View File

@ -1,37 +1,50 @@
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_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_chat_types/flutter_chat_types.dart';
class ChatTextMessageWidget extends StatelessWidget {
const ChatTextMessageWidget({
class ChatUserTextMessageWidget extends StatelessWidget {
const ChatUserTextMessageWidget({
super.key,
required this.user,
required this.messageUserId,
required this.text,
required this.message,
required this.metadata,
});
final User user;
final String messageUserId;
final String text;
final TextMessage message;
final String? metadata;
@override
Widget build(BuildContext context) {
return _textWidgetBuilder(user, context, text);
}
Widget _textWidgetBuilder(
User user,
BuildContext context,
String text,
) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextMessageText(
text: text,
),
],
return BlocProvider(
create: (context) => ChatUserMessageBloc(
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,
),
],
);
},
),
);
}
}
@ -58,3 +71,59 @@ 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

@ -1,3 +1,7 @@
import 'package:appflowy/core/helpers/url_launcher.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/user_service.dart';
import 'package:appflowy/workspace/application/subscription_success_listenable/subscription_success_listenable.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
@ -13,10 +17,27 @@ class LocalAIOnBoardingBloc
extends Bloc<LocalAIOnBoardingEvent, LocalAIOnBoardingState> {
LocalAIOnBoardingBloc(this.userProfile)
: super(const LocalAIOnBoardingState()) {
_userService = UserBackendService(userId: userProfile.id);
_successListenable = getIt<SubscriptionSuccessListenable>();
_successListenable.addListener(_onPaymentSuccessful);
_dispatch();
}
Future<void> _onPaymentSuccessful() async {
if (isClosed) {
return;
}
add(
LocalAIOnBoardingEvent.paymentSuccessful(
_successListenable.subscribedPlan,
),
);
}
final UserProfilePB userProfile;
late final IUserBackendService _userService;
late final SubscriptionSuccessListenable _successListenable;
void _dispatch() {
on<LocalAIOnBoardingEvent>((event, emit) {
@ -24,6 +45,21 @@ class LocalAIOnBoardingBloc
started: () {
_loadSubscriptionPlans();
},
addSubscription: (plan) async {
emit(state.copyWith(isLoading: true));
final result = await _userService.createSubscription(
userProfile.workspaceId,
plan,
);
result.fold(
(pl) => afLaunchUrlString(pl.paymentLink),
(f) => Log.error(
'Failed to fetch paymentlink for $plan: ${f.msg}',
f,
),
);
},
didGetSubscriptionPlans: (result) {
result.fold(
(workspaceSubInfo) {
@ -40,6 +76,11 @@ class LocalAIOnBoardingBloc
},
);
},
paymentSuccessful: (SubscriptionPlanPB? plan) {
if (plan == SubscriptionPlanPB.AiLocal) {
emit(state.copyWith(isPurchaseAILocal: true, isLoading: false));
}
},
);
});
}
@ -57,6 +98,12 @@ class LocalAIOnBoardingBloc
@freezed
class LocalAIOnBoardingEvent with _$LocalAIOnBoardingEvent {
const factory LocalAIOnBoardingEvent.started() = _Started;
const factory LocalAIOnBoardingEvent.addSubscription(
SubscriptionPlanPB plan,
) = _AddSubscription;
const factory LocalAIOnBoardingEvent.paymentSuccessful(
SubscriptionPlanPB? plan,
) = _PaymentSuccessful;
const factory LocalAIOnBoardingEvent.didGetSubscriptionPlans(
FlowyResult<WorkspaceSubscriptionInfoPB, FlowyError> result,
) = _LoadSubscriptionPlans;
@ -66,5 +113,6 @@ class LocalAIOnBoardingEvent with _$LocalAIOnBoardingEvent {
class LocalAIOnBoardingState with _$LocalAIOnBoardingState {
const factory LocalAIOnBoardingState({
@Default(false) bool isPurchaseAILocal,
@Default(false) bool isLoading,
}) = _LocalAIOnBoardingState;
}

View File

@ -1,8 +1,6 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/shared/af_role_pb_extension.dart';
import 'package:appflowy/shared/feature_flags.dart';
import 'package:appflowy/workspace/application/settings/ai/local_ai_on_boarding_bloc.dart';
import 'package:appflowy/workspace/application/settings/settings_dialog_bloc.dart';
import 'package:appflowy/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting.dart';
import 'package:appflowy/workspace/presentation/settings/pages/setting_ai_view/model_selection.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart';
@ -151,9 +149,9 @@ class _LocalAIOnBoarding extends StatelessWidget {
// Show the upgrade to AI Local plan button if the user has not purchased the AI Local plan
return _UpgradeToAILocalPlan(
onTap: () {
context.read<SettingsDialogBloc>().add(
const SettingsDialogEvent.setSelectedPage(
SettingsPage.plan,
context.read<LocalAIOnBoardingBloc>().add(
const LocalAIOnBoardingEvent.addSubscription(
SubscriptionPlanPB.AiLocal,
),
);
},
@ -195,62 +193,45 @@ class _UpgradeToAILocalPlan extends StatefulWidget {
}
class _UpgradeToAILocalPlanState extends State<_UpgradeToAILocalPlan> {
bool _isHovered = false;
@override
Widget build(BuildContext context) {
const textGradient = LinearGradient(
begin: Alignment.bottomLeft,
end: Alignment.bottomRight,
colors: [Color(0xFF8032FF), Color(0xFFEF35FF)],
stops: [0.1545, 0.8225],
);
final backgroundGradient = LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
_isHovered
? const Color(0xFF8032FF).withOpacity(0.3)
: Colors.transparent,
_isHovered
? const Color(0xFFEF35FF).withOpacity(0.3)
: Colors.transparent,
],
);
return GestureDetector(
onTap: widget.onTap,
child: MouseRegion(
cursor: SystemMouseCursors.click,
onEnter: (_) => setState(() => _isHovered = true),
onExit: (_) => setState(() => _isHovered = false),
child: Container(
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 10),
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(
gradient: backgroundGradient,
borderRadius: BorderRadius.circular(10),
),
child: Row(
return Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const FlowySvg(
FlowySvgs.upgrade_storage_s,
blendMode: null,
FlowyText.medium(
LocaleKeys.sideBar_upgradeToAILocal.tr(),
maxLines: 10,
lineHeight: 1.5,
),
const HSpace(6),
ShaderMask(
shaderCallback: (bounds) => textGradient.createShader(bounds),
blendMode: BlendMode.srcIn,
const VSpace(4),
Opacity(
opacity: 0.6,
child: FlowyText(
LocaleKeys.sideBar_upgradeToAILocal.tr(),
color: AFThemeExtension.of(context).strongText,
LocaleKeys.sideBar_upgradeToAILocalDesc.tr(),
fontSize: 12,
maxLines: 10,
lineHeight: 1.5,
),
),
],
),
),
),
BlocBuilder<LocalAIOnBoardingBloc, LocalAIOnBoardingState>(
builder: (context, state) {
if (state.isLoading) {
return const CircularProgressIndicator.adaptive();
} else {
return Toggle(
value: false,
onChanged: (_) => widget.onTap(),
);
}
},
),
],
);
}
}

View File

@ -172,7 +172,7 @@ checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
[[package]]
name = "app-error"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=30c7acce96f1a7b8865c05e70b6e525eaa286b37#30c7acce96f1a7b8865c05e70b6e525eaa286b37"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=99410fb7662440e75493df110de2283f75ab2418#99410fb7662440e75493df110de2283f75ab2418"
dependencies = [
"anyhow",
"bincode",
@ -192,7 +192,7 @@ dependencies = [
[[package]]
name = "appflowy-ai-client"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=30c7acce96f1a7b8865c05e70b6e525eaa286b37#30c7acce96f1a7b8865c05e70b6e525eaa286b37"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=99410fb7662440e75493df110de2283f75ab2418#99410fb7662440e75493df110de2283f75ab2418"
dependencies = [
"anyhow",
"bytes",
@ -826,7 +826,7 @@ dependencies = [
[[package]]
name = "client-api"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=30c7acce96f1a7b8865c05e70b6e525eaa286b37#30c7acce96f1a7b8865c05e70b6e525eaa286b37"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=99410fb7662440e75493df110de2283f75ab2418#99410fb7662440e75493df110de2283f75ab2418"
dependencies = [
"again",
"anyhow",
@ -876,7 +876,7 @@ dependencies = [
[[package]]
name = "client-api-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=30c7acce96f1a7b8865c05e70b6e525eaa286b37#30c7acce96f1a7b8865c05e70b6e525eaa286b37"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=99410fb7662440e75493df110de2283f75ab2418#99410fb7662440e75493df110de2283f75ab2418"
dependencies = [
"collab-entity",
"collab-rt-entity",
@ -888,7 +888,7 @@ dependencies = [
[[package]]
name = "client-websocket"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=30c7acce96f1a7b8865c05e70b6e525eaa286b37#30c7acce96f1a7b8865c05e70b6e525eaa286b37"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=99410fb7662440e75493df110de2283f75ab2418#99410fb7662440e75493df110de2283f75ab2418"
dependencies = [
"futures-channel",
"futures-util",
@ -1132,7 +1132,7 @@ dependencies = [
[[package]]
name = "collab-rt-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=30c7acce96f1a7b8865c05e70b6e525eaa286b37#30c7acce96f1a7b8865c05e70b6e525eaa286b37"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=99410fb7662440e75493df110de2283f75ab2418#99410fb7662440e75493df110de2283f75ab2418"
dependencies = [
"anyhow",
"bincode",
@ -1157,7 +1157,7 @@ dependencies = [
[[package]]
name = "collab-rt-protocol"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=30c7acce96f1a7b8865c05e70b6e525eaa286b37#30c7acce96f1a7b8865c05e70b6e525eaa286b37"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=99410fb7662440e75493df110de2283f75ab2418#99410fb7662440e75493df110de2283f75ab2418"
dependencies = [
"anyhow",
"async-trait",
@ -1532,7 +1532,7 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308"
[[package]]
name = "database-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=30c7acce96f1a7b8865c05e70b6e525eaa286b37#30c7acce96f1a7b8865c05e70b6e525eaa286b37"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=99410fb7662440e75493df110de2283f75ab2418#99410fb7662440e75493df110de2283f75ab2418"
dependencies = [
"anyhow",
"app-error",
@ -3051,7 +3051,7 @@ dependencies = [
[[package]]
name = "gotrue"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=30c7acce96f1a7b8865c05e70b6e525eaa286b37#30c7acce96f1a7b8865c05e70b6e525eaa286b37"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=99410fb7662440e75493df110de2283f75ab2418#99410fb7662440e75493df110de2283f75ab2418"
dependencies = [
"anyhow",
"futures-util",
@ -3068,7 +3068,7 @@ dependencies = [
[[package]]
name = "gotrue-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=30c7acce96f1a7b8865c05e70b6e525eaa286b37#30c7acce96f1a7b8865c05e70b6e525eaa286b37"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=99410fb7662440e75493df110de2283f75ab2418#99410fb7662440e75493df110de2283f75ab2418"
dependencies = [
"anyhow",
"app-error",
@ -3500,7 +3500,7 @@ dependencies = [
[[package]]
name = "infra"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=30c7acce96f1a7b8865c05e70b6e525eaa286b37#30c7acce96f1a7b8865c05e70b6e525eaa286b37"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=99410fb7662440e75493df110de2283f75ab2418#99410fb7662440e75493df110de2283f75ab2418"
dependencies = [
"anyhow",
"bytes",
@ -6098,7 +6098,7 @@ dependencies = [
[[package]]
name = "shared-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=30c7acce96f1a7b8865c05e70b6e525eaa286b37#30c7acce96f1a7b8865c05e70b6e525eaa286b37"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=99410fb7662440e75493df110de2283f75ab2418#99410fb7662440e75493df110de2283f75ab2418"
dependencies = [
"anyhow",
"app-error",

View File

@ -53,7 +53,7 @@ collab-user = { version = "0.2" }
# Run the script:
# scripts/tool/update_client_api_rev.sh new_rev_id
# ⚠️⚠️⚠️️
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "30c7acce96f1a7b8865c05e70b6e525eaa286b37" }
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "99410fb7662440e75493df110de2283f75ab2418" }
[dependencies]
serde_json.workspace = true

View File

@ -163,7 +163,7 @@ checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
[[package]]
name = "app-error"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=30c7acce96f1a7b8865c05e70b6e525eaa286b37#30c7acce96f1a7b8865c05e70b6e525eaa286b37"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=99410fb7662440e75493df110de2283f75ab2418#99410fb7662440e75493df110de2283f75ab2418"
dependencies = [
"anyhow",
"bincode",
@ -183,7 +183,7 @@ dependencies = [
[[package]]
name = "appflowy-ai-client"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=30c7acce96f1a7b8865c05e70b6e525eaa286b37#30c7acce96f1a7b8865c05e70b6e525eaa286b37"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=99410fb7662440e75493df110de2283f75ab2418#99410fb7662440e75493df110de2283f75ab2418"
dependencies = [
"anyhow",
"bytes",
@ -800,7 +800,7 @@ dependencies = [
[[package]]
name = "client-api"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=30c7acce96f1a7b8865c05e70b6e525eaa286b37#30c7acce96f1a7b8865c05e70b6e525eaa286b37"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=99410fb7662440e75493df110de2283f75ab2418#99410fb7662440e75493df110de2283f75ab2418"
dependencies = [
"again",
"anyhow",
@ -850,7 +850,7 @@ dependencies = [
[[package]]
name = "client-api-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=30c7acce96f1a7b8865c05e70b6e525eaa286b37#30c7acce96f1a7b8865c05e70b6e525eaa286b37"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=99410fb7662440e75493df110de2283f75ab2418#99410fb7662440e75493df110de2283f75ab2418"
dependencies = [
"collab-entity",
"collab-rt-entity",
@ -862,7 +862,7 @@ dependencies = [
[[package]]
name = "client-websocket"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=30c7acce96f1a7b8865c05e70b6e525eaa286b37#30c7acce96f1a7b8865c05e70b6e525eaa286b37"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=99410fb7662440e75493df110de2283f75ab2418#99410fb7662440e75493df110de2283f75ab2418"
dependencies = [
"futures-channel",
"futures-util",
@ -1115,7 +1115,7 @@ dependencies = [
[[package]]
name = "collab-rt-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=30c7acce96f1a7b8865c05e70b6e525eaa286b37#30c7acce96f1a7b8865c05e70b6e525eaa286b37"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=99410fb7662440e75493df110de2283f75ab2418#99410fb7662440e75493df110de2283f75ab2418"
dependencies = [
"anyhow",
"bincode",
@ -1140,7 +1140,7 @@ dependencies = [
[[package]]
name = "collab-rt-protocol"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=30c7acce96f1a7b8865c05e70b6e525eaa286b37#30c7acce96f1a7b8865c05e70b6e525eaa286b37"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=99410fb7662440e75493df110de2283f75ab2418#99410fb7662440e75493df110de2283f75ab2418"
dependencies = [
"anyhow",
"async-trait",
@ -1411,7 +1411,7 @@ dependencies = [
"cssparser-macros",
"dtoa-short",
"itoa 1.0.10",
"phf 0.8.0",
"phf 0.11.2",
"smallvec",
]
@ -1522,7 +1522,7 @@ checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5"
[[package]]
name = "database-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=30c7acce96f1a7b8865c05e70b6e525eaa286b37#30c7acce96f1a7b8865c05e70b6e525eaa286b37"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=99410fb7662440e75493df110de2283f75ab2418#99410fb7662440e75493df110de2283f75ab2418"
dependencies = [
"anyhow",
"app-error",
@ -3118,7 +3118,7 @@ dependencies = [
[[package]]
name = "gotrue"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=30c7acce96f1a7b8865c05e70b6e525eaa286b37#30c7acce96f1a7b8865c05e70b6e525eaa286b37"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=99410fb7662440e75493df110de2283f75ab2418#99410fb7662440e75493df110de2283f75ab2418"
dependencies = [
"anyhow",
"futures-util",
@ -3135,7 +3135,7 @@ dependencies = [
[[package]]
name = "gotrue-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=30c7acce96f1a7b8865c05e70b6e525eaa286b37#30c7acce96f1a7b8865c05e70b6e525eaa286b37"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=99410fb7662440e75493df110de2283f75ab2418#99410fb7662440e75493df110de2283f75ab2418"
dependencies = [
"anyhow",
"app-error",
@ -3572,7 +3572,7 @@ dependencies = [
[[package]]
name = "infra"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=30c7acce96f1a7b8865c05e70b6e525eaa286b37#30c7acce96f1a7b8865c05e70b6e525eaa286b37"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=99410fb7662440e75493df110de2283f75ab2418#99410fb7662440e75493df110de2283f75ab2418"
dependencies = [
"anyhow",
"bytes",
@ -6162,7 +6162,7 @@ dependencies = [
[[package]]
name = "shared-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=30c7acce96f1a7b8865c05e70b6e525eaa286b37#30c7acce96f1a7b8865c05e70b6e525eaa286b37"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=99410fb7662440e75493df110de2283f75ab2418#99410fb7662440e75493df110de2283f75ab2418"
dependencies = [
"anyhow",
"app-error",

View File

@ -52,7 +52,7 @@ collab-user = { version = "0.2" }
# Run the script:
# scripts/tool/update_client_api_rev.sh new_rev_id
# ⚠️⚠️⚠️️
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "30c7acce96f1a7b8865c05e70b6e525eaa286b37" }
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "99410fb7662440e75493df110de2283f75ab2418" }
[dependencies]
serde_json.workspace = true

View File

@ -1,3 +1 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.76392 5.78125L13.8889 7.5L9.76392 9.21875L7.88892 13L6.01392 9.21875L1.88892 7.5L6.01392 5.78125L7.88892 2L9.76392 5.78125Z" fill="#750D7E"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 16 16" id="Ai-Chip-Spark--Streamline-Core-Remix.svg" height="16" width="16"><desc>Ai Chip Spark Streamline Icon: https://streamlinehq.com</desc><g id="Free Remix/Artificial Intelligence/ai-chip-spark--chip-processor-artificial-intelligence-ai"><path id="Union" fill="#000000" fill-rule="evenodd" d="M5.730194285714285 3.2857142857142856H3.80952c-0.2892914285714286 0 -0.5238057142857143 0.2345142857142857 -0.5238057142857143 0.5238057142857143v8.380994285714285c0 0.2892571428571428 0.2345142857142857 0.5237714285714286 0.5238057142857143 0.5237714285714286h8.380994285714285c0.2892571428571428 0 0.5237714285714286 -0.2345142857142857 0.5237714285714286 -0.5237714285714286V3.80952c0 -0.2892914285714286 -0.2345142857142857 -0.5238057142857143 -0.5237714285714286 -0.5238057142857143H5.730194285714285Zm5.253897142857142 -1.4285714285714284v-1.1428571428571428c0 -0.3944891428571428 -0.3197942857142857 -0.7142857142857142 -0.7142857142857142 -0.7142857142857142 -0.39447999999999994 0 -0.7142857142857142 0.31979657142857143 -0.7142857142857142 0.7142857142857142v1.1428571428571428H6.4444799999999995v-1.1428571428571428c0 -0.3944891428571428 -0.3198057142857143 -0.7142857142857142 -0.7142857142857142 -0.7142857142857142 -0.39449142857142855 0 -0.7142857142857142 0.31979657142857143 -0.7142857142857142 0.7142857142857142v1.1428571428571428H3.80952c-1.078262857142857 0 -1.9523771428571426 0.8741142857142857 -1.9523771428571426 1.9523771428571426v1.2063885714285714h-1.1428571428571428c-0.3944891428571428 0 -0.7142857142857142 0.3197942857142857 -0.7142857142857142 0.7142857142857142 0 0.39447999999999994 0.31979657142857143 0.7142857142857142 0.7142857142857142 0.7142857142857142h1.1428571428571428v3.11104h-1.1428571428571428c-0.3944891428571428 0 -0.7142857142857142 0.3198057142857143 -0.7142857142857142 0.7142857142857142 0 0.39449142857142855 0.31979657142857143 0.7142857142857142 0.7142857142857142 0.7142857142857142h1.1428571428571428v1.206422857142857c0 1.0782857142857143 0.8741142857142857 1.952342857142857 1.9523771428571426 1.952342857142857h1.2063885714285714v1.1428571428571428c0 0.3945142857142857 0.3197942857142857 0.7142857142857142 0.7142857142857142 0.7142857142857142 0.39447999999999994 0 0.7142857142857142 -0.31977142857142854 0.7142857142857142 -0.7142857142857142v-1.1428571428571428h3.11104v1.1428571428571428c0 0.3945142857142857 0.3198057142857143 0.7142857142857142 0.7142857142857142 0.7142857142857142 0.39449142857142855 0 0.7142857142857142 -0.31977142857142854 0.7142857142857142 -0.7142857142857142v-1.1428571428571428h1.206422857142857c1.0782857142857143 0 1.952342857142857 -0.8740571428571429 1.952342857142857 -1.952342857142857V10.984091428571427h1.1428571428571428c0.3945142857142857 0 0.7142857142857142 -0.3197942857142857 0.7142857142857142 -0.7142857142857142 0 -0.39447999999999994 -0.31977142857142854 -0.7142857142857142 -0.7142857142857142 -0.7142857142857142h-1.1428571428571428V6.4444799999999995h1.1428571428571428c0.3945142857142857 0 0.7142857142857142 -0.3198057142857143 0.7142857142857142 -0.7142857142857142 0 -0.39449142857142855 -0.31977142857142854 -0.7142857142857142 -0.7142857142857142 -0.7142857142857142h-1.1428571428571428V3.80952c0 -1.078262857142857 -0.8740571428571429 -1.9523771428571426 -1.952342857142857 -1.9523771428571426H10.984091428571427ZM8.843817142857143 5.2543999999999995c-0.20528 -0.9151542857142857 -1.503062857142857 -0.9085371428571427 -1.70024 0.007394285714285714l-0.01904 0.08846857142857144c-0.20056 0.9316228571428571 -0.9331085714285714 1.6393714285714285 -1.8442742857142855 1.80056 -0.94512 0.16720000000000002 -0.9451085714285714 1.5311542857142857 0 1.6983542857142855 0.9111657142857142 0.16118857142857143 1.6437142857142857 0.8689371428571429 1.8442742857142855 1.8005714285714283l0.01904 0.08845714285714285c0.19717714285714283 0.9159657142857143 1.4949599999999998 0.9225942857142857 1.70024 0.007394285714285714l0.02312 -0.10308571428571428c0.20821714285714282 -0.9282742857142856 0.9417599999999999 -1.630925714285714 1.8515885714285711 -1.7918742857142855 0.9467314285714284 -0.1674857142857143 0.9467314285714284 -1.5337942857142857 0 -1.70128 -0.9098285714285714 -0.16094857142857144 -1.6433714285714285 -0.8636 -1.8515885714285711 -1.7918742857142855l-0.02312 -0.10308571428571428Z" clip-rule="evenodd" stroke-width="1"></path></g></svg>

Before

Width:  |  Height:  |  Size: 257 B

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-0.5 -0.5 16 16" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" id="At-Sign--Streamline-Lucide.svg" height="16" width="16"><desc>At Sign Streamline Icon: https://streamlinehq.com</desc><path d="M5 7.5a2.5 2.5 0 1 0 5 0 2.5 2.5 0 1 0 -5 0" stroke-width="1"></path><path d="M10 5v3.125a1.875 1.875 0 0 0 3.75 0v-0.625a6.25 6.25 0 1 0 -2.5 5" stroke-width="1"></path></svg>

After

Width:  |  Height:  |  Size: 453 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-0.5 -0.5 16 16" fill="none" stroke="#f254bc" stroke-linecap="round" stroke-linejoin="round" id="Feather--Streamline-Lucide.svg" height="16" width="16"><desc>Feather Streamline Icon: https://streamlinehq.com</desc><path d="M7.91875 11.875a1.25 1.25 0 0 0 0.885 -0.3675l3.84625 -3.8575a3.75 3.75 0 0 0 -5.30625 -5.30625L3.49125 6.19625A1.25 1.25 0 0 0 3.125 7.08V11.25a0.625 0.625 0 0 0 0.625 0.625z" stroke-width="1"></path><path d="M10 5 1.25 13.75" stroke-width="1"></path><path d="M10.9375 9.375H5.625" stroke-width="1"></path></svg>

After

Width:  |  Height:  |  Size: 585 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 16 16" id="Lightbulb--Streamline-Flex.svg" height="16" width="16"><desc>Lightbulb Streamline Icon: https://streamlinehq.com</desc><g id="lightbulb--lighting-light-incandescent-bulb-lights"><path id="Vector" stroke="#a44afd" stroke-linecap="round" stroke-linejoin="round" d="M13.284685714285715 5.819314285714285c0.006171428571428572 -0.8796571428571429 -0.2233142857142857 -1.7449142857142856 -0.6645714285714286 -2.505931428571428 -0.44125714285714285 -0.7610171428571428 -1.0781714285714286 -1.3900571428571427 -1.8445942857142856 -1.8218171428571428C10.009074285714284 1.0598102857142857 9.141039999999998 0.8410719999999999 8.261531428571429 0.858062857142857c-0.87952 0.016989714285714285 -1.7384571428571427 0.26909028571428567 -2.487645714285714 0.7301314285714285 -0.7491885714285714 0.46103999999999995 -1.3613142857142855 1.1142057142857142 -1.772822857142857 1.8916914285714284 -0.41152 0.7774971428571428 -0.6074285714285713 1.6509714285714285 -0.5673828571428571 2.5297371428571425 0.04004571428571429 0.8787657142857143 0.3145828571428571 1.7308 0.7950971428571428 2.467645714285714 0.85808 1.3157942857142855 1.366102857142857 1.7994742857142856 1.366102857142857 3.36536 0 0.14514285714285713 0.05769142857142857 0.28445714285714285 0.16037714285714286 0.3872 0.10269714285714285 0.10262857142857143 0.24197714285714284 0.16034285714285715 0.3872 0.16034285714285715H10.570742857142855c0.14522285714285713 0 0.2845028571428571 -0.05771428571428572 0.3871885714285714 -0.16034285714285715 0.10268571428571428 -0.10274285714285714 0.16037714285714286 -0.24205714285714283 0.16037714285714286 -0.3872 0 -1.603622857142857 0.5683199999999999 -2.0796571428571426 1.420662857142857 -3.44352 0.48365714285714284 -0.77384 0.7419428571428571 -1.667245714285714 0.7457142857142857 -2.5797942857142857Z" stroke-width="1"></path><path id="Vector_2" stroke="#a44afd" stroke-linecap="round" stroke-linejoin="round" d="m6.853794285714285 15.142857142857142 3.005405714285714 0" stroke-width="1"></path></g></svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-0.5 -0.5 16 16" fill="none" stroke="#f254bc" stroke-linecap="round" stroke-linejoin="round" id="Pen-Line--Streamline-Lucide.svg" height="16" width="16"><desc>Pen Line Streamline Icon: https://streamlinehq.com</desc><path d="M7.83775 14.08125H14.75675" stroke-width="1"></path><path d="M11.297250000000002 1.3964375000000002C12.1849375 0.5087499999999999 13.70075 0.914875 14.025625 2.1275C14.1764375 2.6903125 14.015562500000001 3.29075 13.603562499999999 3.70275L3.993875 13.312437500000001L0.91875 14.08125L1.6875624999999999 11.006125Z" stroke-width="1"></path></svg>

After

Width:  |  Height:  |  Size: 620 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-0.5 -0.5 16 16" fill="none" stroke="#00c8ff" stroke-linecap="round" stroke-linejoin="round" id="Message-Circle-Question--Streamline-Lucide.svg" height="16" width="16"><desc>Message Circle Question Streamline Icon: https://streamlinehq.com</desc><path d="M4.9781875 12.750812499999999C9.291625 14.9635 14.382874999999999 11.677 14.1424375 6.835062499999999C13.901937499999999 1.993125 8.510124999999999 -0.7728124999999999 4.437125 1.8564375C1.718 3.6116875 0.772 7.1421875 2.2491875 10.0218125L0.84975 14.15025Z" stroke-width="1"></path><path d="M5.810874999999999 5.0536875000000006C6.347125 3.5293124999999996 8.3325 3.1570625000000003 9.3845 4.3836875C9.711625 4.765 9.8910625 5.2510625 9.8903125 5.7534375C9.8903125 7.152937499999999 7.791125 7.852625 7.791125 7.852625" stroke-width="1"></path><path d="M7.847062500000001 10.6515625H7.8540624999999995" stroke-width="1"></path></svg>

After

Width:  |  Height:  |  Size: 938 B

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="-0.5 -0.5 16 16" height="16" width="16" id="School--Streamline-Rounded----Material-Symbols.svg">
<path fill="#ffba00" d="M3.437625 10.906312499999999C3.2917812499999997 10.82296875 3.1745937499999997 10.70903125 3.0860593749999996 10.564437499999999C2.9975187500000002 10.419968749999999 2.953246875 10.257875 2.953246875 10.0781875V6.8906875L1.3907468749999998 6.0313125C1.3074125 5.97921875 1.2449124999999999 5.9191875000000005 1.203246875 5.851156250000001C1.16158125 5.78325 1.140746875 5.707718750000001 1.140746875 5.62459375C1.140746875 5.5415625 1.16158125 5.46621875 1.203246875 5.3985C1.2449124999999999 5.330781249999999 1.3074125 5.27090625 1.3907468749999998 5.218812499999999L7.031375 2.1250625000000003C7.10428125 2.08339375 7.1783125000000005 2.05475 7.253406249999999 2.039125C7.3284062500000005 2.0235 7.4054375 2.0156875 7.4845 2.0156875C7.5635625 2.0156875 7.64059375 2.0235 7.71559375 2.039125C7.7906875 2.05475 7.864718750000001 2.08339375 7.937625000000001 2.1250625000000003L14.125125 5.4844375C14.208468750000002 5.5365312499999995 14.27096875 5.598875 14.312625 5.671468750000001C14.35428125 5.7439687500000005 14.375125 5.82225 14.375125 5.9063125V10.1094375C14.375125 10.242249999999999 14.32990625 10.353562499999999 14.2395 10.44334375C14.1491875 10.53325 14.037218750000001 10.5781875 13.9035625 10.5781875C13.77003125 10.5781875 13.65896875 10.53325 13.5704375 10.44334375C13.48190625 10.353562499999999 13.437625 10.242249999999999 13.437625 10.1094375V6.1719375L12.01575 6.8906875V10.0781875C12.01575 10.257875 11.97146875 10.419968749999999 11.882937499999999 10.564437499999999C11.79440625 10.70903125 11.67721875 10.82296875 11.531374999999999 10.906312499999999L7.937625000000001 12.8750625C7.864718750000001 12.916718750000001 7.7906875 12.945374999999999 7.71559375 12.961C7.64059375 12.976624999999999 7.5635625 12.984437499999999 7.4845 12.984437499999999C7.4054375 12.984437499999999 7.3284062500000005 12.976624999999999 7.253406249999999 12.961C7.1783125000000005 12.945374999999999 7.10428125 12.916718750000001 7.031375 12.8750625L3.437625 10.906312499999999ZM7.4845 8.3125625L12.406375 5.6250625L7.4845 2.9844375000000003L2.593871875 5.6250625L7.4845 8.3125625ZM7.4845 12.0625625L11.07825 10.0781875V7.4531875L7.937625000000001 9.1406875C7.864718750000001 9.182343750000001 7.7917812500000005 9.211 7.718875 9.226625C7.64596875 9.24225 7.56784375 9.2500625 7.4845 9.2500625C7.4011562500000005 9.2500625 7.32565625 9.24225 7.257937500000001 9.226625C7.1902187500000005 9.211 7.11990625 9.182343750000001 7.047 9.1406875L3.89075 7.4219375V10.0781875L7.4845 12.0625625Z" stroke-width="1"></path>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-0.5 -0.5 16 16" fill="currentColor" id="File-Md-Light--Streamline-Phosphor.svg" height="16" width="16"><desc>File Md Light Streamline Icon: https://streamlinehq.com</desc><path d="m13.858470703125 4.312119140625001 -4.03529296875 -4.03529296875c-0.081005859375 -0.08110546875 -0.19090429687500002 -0.1267265625 -0.30553125000000003 -0.126826171875H2.599998046875c-0.55715625 0 -1.0088203125 0.4516640625 -1.0088203125 1.0088203125v5.188236328125c0 0.332830078125 0.36029296875 0.54084375 0.64852734375 0.374431640625 0.13376953125 -0.077232421875 0.21617578125 -0.219966796875 0.21617578125 -0.374431640625V1.158826171875c0 -0.079599609375 0.0645234375 -0.14412304687500002 0.1441171875 -0.14412304687500002h6.485296875v3.6029414062500003c0 0.23878710937500003 0.19357031249999998 0.432357421875 0.4323515625 0.432357421875h3.6029414062500003v9.367646484375001c0 0.33282421875 0.36029296875 0.540837890625 0.6485332031250001 0.37442578125000003 0.13376953125 -0.077232421875 0.21617578125 -0.2199609375 0.21617578125 -0.37442578125000003V4.61764453125c-0.00010546875 -0.114626953125 -0.045720703125 -0.22452539062500002 -0.126826171875 -0.305525390625Zm-3.90847265625 -2.686353515625L12.50953125 4.18529296875h-2.559533203125Zm-1.0088203125 7.171294921875h-1.152943359375c-0.23878710937500003 -0.00001171875 -0.4323515625 0.19355859375 -0.4323515625 0.4323515625v4.03529296875c0 0.23878124999999997 0.19357031249999998 0.4323515625 0.4323515625 0.4323515625h1.152943359375c1.886009765625 0 3.064763671875 -2.0416640625 2.1217617187499997 -3.675 -0.4376484375 -0.758033203125 -1.2464648437499999 -1.22499609375 -2.1217617187499997 -1.22499609375Zm0 4.03529296875H8.220585937500001v-3.17058984375h0.720591796875c1.220361328125 0.000744140625 1.982279296875 1.32228515625 1.3714570312499998 2.37877734375 -0.28313671875 0.48972070312500005 -0.8057753906249999 0.791466796875 -1.3714570312499998 0.7918125Zm-3.02647265625 -3.6029414062500003v4.03529296875c0 0.33282421875 -0.36029296875 0.54084375 -0.64852734375 0.374431640625 -0.13376953125 -0.077232421875 -0.21617578125 -0.219966796875 -0.21617578125 -0.374431640625v-2.666173828125L3.819234375 12.359648437499999c-0.172072265625 0.24651562500000002 -0.53698828125 0.24651562500000002 -0.709060546875 0l-1.23076171875 -1.7611171875v2.666173828125c0 0.33282421875 -0.36029296875 0.54084375 -0.64852734375 0.374431640625 -0.13377539062500002 -0.077232421875 -0.21618164062499998 -0.219966796875 -0.21618164062499998 -0.374431640625v-4.03529296875c-0.00032226562499999996 -0.33282421875 0.359771484375 -0.541189453125 0.648169921875 -0.37505859375 0.055013671874999996 0.031693359375 0.10237500000000001 0.075111328125 0.13871484374999998 0.12717773437500002l1.663119140625 2.377939453125 1.66311328125 -2.377939453125c0.1905 -0.27291796875 0.604998046875 -0.237275390625 0.746103515625 0.06416015625 0.026923828125 0.05750390625 0.040845703125 0.12022851562499999 0.04078125 0.183720703125Z" stroke-width="1"></path></svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-0.5 -0.5 16 16" fill="currentColor" id="File-Pdf-Light--Streamline-Phosphor.svg" height="16" width="16"><desc>File Pdf Light Streamline Icon: https://streamlinehq.com</desc><path d="M14.537232421875 10.00212890625c0 0.259095703125 -0.21005273437500002 0.46913671875 -0.4691484375 0.4691484375h-2.03298046875v1.56383203125h1.407451171875c0.3611484375 0 0.5868691406250001 0.39095507812499997 0.406294921875 0.70372265625 -0.083806640625 0.145154296875 -0.2386875 0.23457421875 -0.406294921875 0.23457421875h-1.407451171875v1.4074453125c0 0.3611484375 -0.39095507812499997 0.5868691406250001 -0.70372265625 0.406294921875 -0.145154296875 -0.083806640625 -0.23457421875 -0.238681640625 -0.23457421875 -0.406294921875V10.00212890625c0 -0.2591015625 0.210046875 -0.4691484375 0.4691484375 -0.4691484375h2.5021289062500003c0.259107421875 0 0.4691484375 0.210046875 0.4691484375 0.4691484375ZM4.215955078125 11.565955078125c0 1.12278515625 -0.9101953125000001 2.03298046875 -2.032974609375 2.03298046875H1.401064453125v0.781916015625c0 0.3611484375 -0.39095507812499997 0.5868691406250001 -0.70372265625 0.406294921875 -0.145154296875 -0.083806640625 -0.23457421875 -0.238681640625 -0.23457421875 -0.406294921875V10.00212890625c0 -0.259107421875 0.21004101562500002 -0.4691484375 0.4691484375 -0.4691484375h1.2510644531250001c1.122744140625 0.000046875 2.032974609375 0.9102304687499999 2.032974609375 2.032974609375Zm-0.938296875 0c0 -0.604552734375 -0.49012500000000003 -1.0946484374999998 -1.094677734375 -1.094677734375H1.401064453125v2.189361328125h0.781916015625c0.604552734375 -0.000029296875 1.094677734375 -0.49012500000000003 1.094677734375 -1.094677734375Zm6.568083984375 0.62553515625c0 1.46820703125 -1.190296875 2.6584453125 -2.658509765625 2.658509765625h-1.2510644531250001c-0.259107421875 0.00001171875 -0.4691484375 -0.21003515625 -0.4691484375 -0.4691484375V10.00212890625c0 -0.259107421875 0.210046875 -0.4691484375 0.4691484375 -0.4691484375h1.2510644531250001c1.46825390625 0 2.658509765625 1.1902558593750001 2.658509765625 2.658509765625Zm-0.938296875 0c0 -0.950015625 -0.77019140625 -1.7201718750000001 -1.720212890625 -1.720212890625h-0.781916015625v3.44042578125h0.781916015625c0.950021484375 -0.000041015625 1.720212890625 -0.770197265625 1.720212890625 -1.720212890625ZM0.462767578125 6.874470703125V1.24468359375c0 -0.604576171875 0.4901015625 -1.0946835937500001 1.094677734375 -1.0946835937500001h7.506386718750001c0.12465234375 -0.000099609375 0.24421875 0.04941796875 0.33230859375 0.137619140625l4.378728515625 4.37872265625c0.08772070312500001 0.088025390625 0.13693359375 0.2072578125 0.136833984375 0.331529296875v1.876599609375c0 0.3611484375 -0.3909609375 0.5868691406250001 -0.70372265625 0.406294921875 -0.145154296875 -0.083806640625 -0.23457421875 -0.2386875 -0.23457421875 -0.406294921875V5.46701953125h-3.90957421875c-0.259107421875 0 -0.4691484375 -0.21004101562500002 -0.4691484375 -0.4691484375V1.088296875H1.5574453125c-0.0863671875 0 -0.156380859375 0.070013671875 -0.156380859375 0.15638671875000001v5.629787109375c0 0.3611484375 -0.3909609375 0.5868691406250001 -0.70372265625 0.406294921875 -0.145154296875 -0.083806640625 -0.23457421875 -0.2386875 -0.23457421875 -0.406294921875Zm9.070212890625001 -2.3457480468750003h2.777361328125l-2.777361328125 -2.777361328125Z" stroke-width="1"></path></svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-0.5 -0.5 16 16" fill="currentColor" id="File-Txt-Light--Streamline-Phosphor.svg" height="16" width="16"><desc>File Txt Light Streamline Icon: https://streamlinehq.com</desc><path d="M1.245193359375 7.34302734375c0.25908984375 0.00001171875 0.46911328124999996 -0.210017578125 0.46911328124999996 -0.46911328124999996V1.24458984375c0 -0.086361328125 0.0700078125 -0.15636914062499999 0.15636914062499999 -0.15636914062499999h7.036658203125v3.909251953125c0 0.25908984375 0.210017578125 0.46912499999999996 0.469107421875 0.46911328124999996h3.909251953125v1.407328125c0 0.361125 0.39092578125 0.5868222656250001 0.703669921875 0.406265625 0.145142578125 -0.08380078125 0.23455078125 -0.2386640625 0.23455078125 -0.406265625V4.99747265625c-0.00010546875 -0.12437109375 -0.04960546875 -0.24360937500000002 -0.1376015625 -0.331505859375l-4.378365234375001 -4.3783593750000005c-0.087890625 -0.088001953125 -0.207134765625 -0.13749609375 -0.331505859375 -0.137607421875H1.8706757812500001c-0.604529296875 0 -1.0945898437500001 0.49006640625 -1.0945898437500001 1.0945898437500001v5.62932421875c0 0.25908984375 0.210017578125 0.46912499999999996 0.469107421875 0.46911328124999996Zm8.600361328125 -5.5917949218750005 2.7771328125 2.7771328125h-2.7771328125Zm-0.4003125 8.522167968749999 -1.36901953125 1.917099609375 1.36901953125 1.916314453125c0.21065625000000002 0.293712890625 0.02436328125 0.705322265625 -0.335326171875 0.740900390625 -0.16693359375 0.01651171875 -0.329994140625 -0.05729296875 -0.4277578125 -0.19360546874999998L7.5 12.997371093749999l-1.182158203125 1.6551796875c-0.21065625000000002 0.29370703125 -0.660263671875 0.249240234375 -0.809302734375 -0.08005078125000001 -0.0691640625 -0.152818359375 -0.051550781250000004 -0.3309375 0.046218749999999996 -0.46725l1.36901953125 -1.9147500000000002 -1.36901953125 -1.916314453125c-0.21065625000000002 -0.293712890625 -0.02436328125 -0.705322265625 0.335326171875 -0.740900390625 0.16693359375 -0.01651171875 0.329994140625 0.05729296875 0.4277578125 0.19360546874999998L7.5 11.38362890625l1.182158203125 -1.6551738281250001c0.21065625000000002 -0.293712890625 0.660263671875 -0.24924609375 0.809302734375 0.08004492187500001 0.0691640625 0.152818359375 0.051550781250000004 0.3309375 -0.046218749999999996 0.46725Zm-4.9162734375 -0.272080078125c0 0.259072265625 -0.21003515625 0.46909570312500004 -0.46911328124999996 0.469107421875h-1.0945898437500001v3.909251953125c0 0.361125 -0.39092578125 0.5868222656250001 -0.7036640625 0.406265625 -0.145142578125 -0.08380078125 -0.23455664062499998 -0.2386640625 -0.23455664062499998 -0.406265625v-3.909251953125h-1.0945898437500001c-0.361125 0 -0.5868222656250001 -0.39092578125 -0.40625976562500005 -0.7036640625 0.08380078125 -0.1451484375 0.23865234375000002 -0.23456249999999998 0.40625976562500005 -0.23455664062499998h3.127400390625c0.259078125 0.00001171875 0.46911328124999996 0.21003515625 0.46911328124999996 0.46911328124999996Zm10.007689453125 0c0 0.259072265625 -0.21003515625 0.46909570312500004 -0.46911328124999996 0.469107421875h-1.0945898437500001v3.909251953125c0 0.361125 -0.39092578125 0.5868222656250001 -0.7036640625 0.406265625 -0.145142578125 -0.08380078125 -0.23455664062499998 -0.2386640625 -0.23455664062499998 -0.406265625v-3.909251953125h-1.0945898437500001c-0.361125 0 -0.5868222656250001 -0.39092578125 -0.40625976562500005 -0.7036640625 0.08380078125 -0.1451484375 0.23865234375000002 -0.23456249999999998 0.40625976562500005 -0.23455664062499998h3.127400390625c0.259078125 0.00001171875 0.46911328124999996 0.21003515625 0.46911328124999996 0.46911328124999996Z" stroke-width="1"></path></svg>

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-0.5 -0.5 16 16" fill="currentColor" id="File-X-Light--Streamline-Phosphor.svg" height="16" width="16"><desc>File X Light Streamline Icon: https://streamlinehq.com</desc><path d="m13.570236328125 4.312119140625001 -4.03529296875 -4.03529296875c-0.081005859375 -0.08110546875 -0.19090429687500002 -0.1267265625 -0.30553125000000003 -0.126826171875H2.3117636718749996c-0.55715625 0 -1.0088203125 0.4516640625 -1.0088203125 1.0088203125v12.682353515625001c0 0.557162109375 0.4516640625 1.008826171875 1.0088203125 1.008826171875h10.37647265625c0.55715625 0 1.0088203125 -0.4516640625 1.0088203125 -1.008826171875V4.61764453125c-0.000099609375 -0.114626953125 -0.04571484375 -0.22452539062500002 -0.1268203125 -0.305525390625Zm-3.90847265625 -2.686353515625 2.55952734375 2.55952734375h-2.55952734375Zm3.17058984375 12.215408203125001c0 0.07959375 -0.064529296875 0.1441171875 -0.1441171875 0.14412304687500002H2.3117636718749996c-0.07959375 0 -0.1441171875 -0.0645234375 -0.1441171875 -0.14412304687500002V1.158826171875c0 -0.079599609375 0.0645234375 -0.14412304687500002 0.1441171875 -0.14412304687500002h6.485296875v3.6029414062500003c0 0.23878710937500003 0.19357031249999998 0.432357421875 0.4323515625 0.432357421875h3.6029414062500003Zm-3.29741015625 -6.646705078125c0.1685859375 0.168802734375 0.1685859375 0.442259765625 0 0.6110625000000001l-1.423166015625 1.4238808593749999 1.423166015625 1.4238808593749999c0.24349218749999998 0.2268984375 0.150046875 0.632302734375 -0.16819921875 0.7297265625 -0.15816796875 0.048416015625 -0.33009375 0.0023496093749999997 -0.44286328125 -0.1186640625L7.5 9.841189453125l-1.4238808593749999 1.423166015625c-0.243498046875 0.2268984375 -0.6413027343750001 0.1051171875 -0.7160566406250001 -0.21920507812499998 -0.03226171875 -0.139951171875 0.007083984375 -0.28678125 0.10499414062500001 -0.391857421875l1.423166015625 -1.4238808593749999 -1.423166015625 -1.4238808593749999c-0.24349218749999998 -0.226904296875 -0.150046875 -0.632302734375 0.16819921875 -0.7297265625 0.15816796875 -0.048416015625 0.33009375 -0.0023496093749999997 0.44286328125 0.1186640625L7.5 8.617634765624999l1.4238808593749999 -1.423166015625c0.168802734375 -0.168591796875 0.442259765625 -0.168591796875 0.6110625000000001 0Z" stroke-width="1"></path></svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -178,6 +178,8 @@
"referenceSources": "{} sources found",
"clickToMention": "Click to mention a page",
"uploadFile": "Upload PDFs, md or txt files to chat with",
"questionTitle": "Ideas",
"questionDetail": "Hi {}! How can I help you today?",
"indexingFile": "Indexing {}"
},
"trash": {
@ -303,7 +305,8 @@
"purchaseStorageSpace": "Purchase Storage Space",
"purchaseAIResponse": "Purchase ",
"askOwnerToUpgradeToLocalAI": "Ask workspace owner to enable AI On-device",
"upgradeToAILocal": "AI On-device on your device"
"upgradeToAILocal": "Run local models on your device for ultimate privacy",
"upgradeToAILocalDesc": "Chat with PDFs, improve your writing, and auto-fill tables using local AI"
},
"notifications": {
"export": {

View File

@ -163,7 +163,7 @@ checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
[[package]]
name = "app-error"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=30c7acce96f1a7b8865c05e70b6e525eaa286b37#30c7acce96f1a7b8865c05e70b6e525eaa286b37"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=99410fb7662440e75493df110de2283f75ab2418#99410fb7662440e75493df110de2283f75ab2418"
dependencies = [
"anyhow",
"bincode",
@ -183,7 +183,7 @@ dependencies = [
[[package]]
name = "appflowy-ai-client"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=30c7acce96f1a7b8865c05e70b6e525eaa286b37#30c7acce96f1a7b8865c05e70b6e525eaa286b37"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=99410fb7662440e75493df110de2283f75ab2418#99410fb7662440e75493df110de2283f75ab2418"
dependencies = [
"anyhow",
"bytes",
@ -718,7 +718,7 @@ dependencies = [
[[package]]
name = "client-api"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=30c7acce96f1a7b8865c05e70b6e525eaa286b37#30c7acce96f1a7b8865c05e70b6e525eaa286b37"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=99410fb7662440e75493df110de2283f75ab2418#99410fb7662440e75493df110de2283f75ab2418"
dependencies = [
"again",
"anyhow",
@ -768,7 +768,7 @@ dependencies = [
[[package]]
name = "client-api-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=30c7acce96f1a7b8865c05e70b6e525eaa286b37#30c7acce96f1a7b8865c05e70b6e525eaa286b37"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=99410fb7662440e75493df110de2283f75ab2418#99410fb7662440e75493df110de2283f75ab2418"
dependencies = [
"collab-entity",
"collab-rt-entity",
@ -780,7 +780,7 @@ dependencies = [
[[package]]
name = "client-websocket"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=30c7acce96f1a7b8865c05e70b6e525eaa286b37#30c7acce96f1a7b8865c05e70b6e525eaa286b37"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=99410fb7662440e75493df110de2283f75ab2418#99410fb7662440e75493df110de2283f75ab2418"
dependencies = [
"futures-channel",
"futures-util",
@ -993,7 +993,7 @@ dependencies = [
[[package]]
name = "collab-rt-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=30c7acce96f1a7b8865c05e70b6e525eaa286b37#30c7acce96f1a7b8865c05e70b6e525eaa286b37"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=99410fb7662440e75493df110de2283f75ab2418#99410fb7662440e75493df110de2283f75ab2418"
dependencies = [
"anyhow",
"bincode",
@ -1018,7 +1018,7 @@ dependencies = [
[[package]]
name = "collab-rt-protocol"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=30c7acce96f1a7b8865c05e70b6e525eaa286b37#30c7acce96f1a7b8865c05e70b6e525eaa286b37"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=99410fb7662440e75493df110de2283f75ab2418#99410fb7662440e75493df110de2283f75ab2418"
dependencies = [
"anyhow",
"async-trait",
@ -1256,7 +1256,7 @@ dependencies = [
"cssparser-macros",
"dtoa-short",
"itoa",
"phf 0.11.2",
"phf 0.8.0",
"smallvec",
]
@ -1356,7 +1356,7 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308"
[[package]]
name = "database-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=30c7acce96f1a7b8865c05e70b6e525eaa286b37#30c7acce96f1a7b8865c05e70b6e525eaa286b37"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=99410fb7662440e75493df110de2283f75ab2418#99410fb7662440e75493df110de2283f75ab2418"
dependencies = [
"anyhow",
"app-error",
@ -2730,7 +2730,7 @@ dependencies = [
[[package]]
name = "gotrue"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=30c7acce96f1a7b8865c05e70b6e525eaa286b37#30c7acce96f1a7b8865c05e70b6e525eaa286b37"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=99410fb7662440e75493df110de2283f75ab2418#99410fb7662440e75493df110de2283f75ab2418"
dependencies = [
"anyhow",
"futures-util",
@ -2747,7 +2747,7 @@ dependencies = [
[[package]]
name = "gotrue-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=30c7acce96f1a7b8865c05e70b6e525eaa286b37#30c7acce96f1a7b8865c05e70b6e525eaa286b37"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=99410fb7662440e75493df110de2283f75ab2418#99410fb7662440e75493df110de2283f75ab2418"
dependencies = [
"anyhow",
"app-error",
@ -3112,7 +3112,7 @@ dependencies = [
[[package]]
name = "infra"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=30c7acce96f1a7b8865c05e70b6e525eaa286b37#30c7acce96f1a7b8865c05e70b6e525eaa286b37"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=99410fb7662440e75493df110de2283f75ab2418#99410fb7662440e75493df110de2283f75ab2418"
dependencies = [
"anyhow",
"bytes",
@ -4068,7 +4068,7 @@ version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12"
dependencies = [
"phf_macros 0.8.0",
"phf_macros",
"phf_shared 0.8.0",
"proc-macro-hack",
]
@ -4088,7 +4088,6 @@ version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc"
dependencies = [
"phf_macros 0.11.2",
"phf_shared 0.11.2",
]
@ -4156,19 +4155,6 @@ dependencies = [
"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]]
name = "phf_shared"
version = "0.8.0"
@ -5321,7 +5307,7 @@ dependencies = [
[[package]]
name = "shared-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=30c7acce96f1a7b8865c05e70b6e525eaa286b37#30c7acce96f1a7b8865c05e70b6e525eaa286b37"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=99410fb7662440e75493df110de2283f75ab2418#99410fb7662440e75493df110de2283f75ab2418"
dependencies = [
"anyhow",
"app-error",

View File

@ -99,8 +99,8 @@ zip = "2.1.3"
# Run the script.add_workspace_members:
# scripts/tool/update_client_api_rev.sh new_rev_id
# ⚠️⚠️⚠️️
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "30c7acce96f1a7b8865c05e70b6e525eaa286b37" }
client-api-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "30c7acce96f1a7b8865c05e70b6e525eaa286b37" }
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "99410fb7662440e75493df110de2283f75ab2418" }
client-api-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "99410fb7662440e75493df110de2283f75ab2418" }
[profile.dev]
opt-level = 0

View File

@ -4,8 +4,8 @@ pub use client_api::entity::ai_dto::{
RelatedQuestion, RepeatedRelatedQuestion, StringOrMessage,
};
pub use client_api::entity::{
ChatAuthorType, ChatMessage, ChatMessageMetadata, ChatMessageType, ChatMetadataData,
MessageCursor, QAChatMessage, QuestionStreamValue, RepeatedChatMessage,
ChatAuthorType, ChatMessage, ChatMessageMetadata, ChatMessageType, ChatMetadataContentType,
ChatMetadataData, MessageCursor, QAChatMessage, QuestionStreamValue, RepeatedChatMessage,
};
use client_api::error::AppResponseError;
use flowy_error::FlowyError;

View File

@ -91,6 +91,10 @@ impl AIManager {
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> {
if let Some((_, chat)) = self.chats.remove(chat_id) {
chat.close();
@ -244,9 +248,7 @@ impl AIManager {
Ok(())
}
pub fn local_ai_purchased(&self) {
// TODO(nathan): enable local ai
}
pub fn local_ai_purchased(&self) {}
}
fn save_chat(conn: DBConnection, chat_id: &str) -> FlowyResult<()> {

View File

@ -4,10 +4,7 @@ use crate::entities::{
};
use crate::middleware::chat_service_mw::AICloudServiceMiddleware;
use crate::notification::{make_notification, ChatNotification};
use crate::persistence::{
insert_chat_messages, read_chat_metadata, select_chat_messages, update_chat, ChatMessageTable,
ChatTableChangeset,
};
use crate::persistence::{insert_chat_messages, select_chat_messages, ChatMessageTable};
use allo_isolate::Isolate;
use flowy_ai_pub::cloud::{
ChatCloudService, ChatMessage, ChatMessageMetadata, ChatMessageType, MessageCursor,
@ -491,24 +488,6 @@ impl Chat {
)
.await?;
let file_name = file_path
.file_name()
.unwrap_or_default()
.to_str()
.unwrap_or_default();
let mut conn = self.user_service.sqlite_connection(self.uid)?;
conn.immediate_transaction(|conn| {
let mut metadata = read_chat_metadata(conn, &self.chat_id)?;
metadata.add_file(
file_name.to_string(),
file_path.to_str().unwrap_or_default().to_string(),
);
let changeset = ChatTableChangeset::from_metadata(metadata);
update_chat(conn, changeset)?;
Ok::<(), FlowyError>(())
})?;
trace!(
"[Chat] created index file record: chat_id={}, file_path={:?}",
self.chat_id,

View File

@ -79,9 +79,21 @@ pub struct ChatMessageMetaPB {
pub data: String,
#[pb(index = 4)]
pub data_type: ChatMessageMetaTypePB,
#[pb(index = 5)]
pub source: String,
}
#[derive(Debug, Default, Clone, ProtoBuf_Enum, PartialEq, Eq, Copy)]
pub enum ChatMessageMetaTypePB {
#[default]
UnknownMetaType = 0,
Txt = 1,
Markdown = 2,
PDF = 3,
}
#[derive(Default, ProtoBuf, Validate, Clone, Debug)]
pub struct StopStreamPB {
#[pb(index = 1)]

View File

@ -7,7 +7,9 @@ use crate::entities::*;
use crate::local_ai::local_llm_chat::LLMModelInfo;
use crate::notification::{make_notification, ChatNotification, APPFLOWY_AI_NOTIFICATION_KEY};
use allo_isolate::Isolate;
use flowy_ai_pub::cloud::{ChatMessageMetadata, ChatMessageType, ChatMetadataData};
use flowy_ai_pub::cloud::{
ChatMessageMetadata, ChatMessageType, ChatMetadataContentType, ChatMetadataData,
};
use flowy_error::{ErrorCode, FlowyError, FlowyResult};
use lib_dispatch::prelude::{data_result_ok, AFPluginData, AFPluginState, DataResult};
use lib_infra::isolate_stream::IsolateSink;
@ -35,16 +37,36 @@ pub(crate) async fn stream_chat_message_handler(
ChatMessageTypePB::System => ChatMessageType::System,
ChatMessageTypePB::User => ChatMessageType::User,
};
let is_using_local_ai = ai_manager.is_using_local_ai();
let metadata = data
.metadata
.into_iter()
.map(|metadata| ChatMessageMetadata {
data: ChatMetadataData::new_text(metadata.data),
id: metadata.id,
name: metadata.name.clone(),
source: metadata.source,
extract: None,
.map(|metadata| {
let (content_type, content_len) = if is_using_local_ai {
(ChatMetadataContentType::Unknown, 0)
} else {
match metadata.data_type {
ChatMessageMetaTypePB::Txt => (ChatMetadataContentType::Text, metadata.data.len()),
ChatMessageMetaTypePB::Markdown => {
(ChatMetadataContentType::Markdown, metadata.data.len())
},
ChatMessageMetaTypePB::PDF => (ChatMetadataContentType::PDF, 0),
ChatMessageMetaTypePB::UnknownMetaType => (ChatMetadataContentType::Unknown, 0),
}
};
ChatMessageMetadata {
data: ChatMetadataData {
content: metadata.data,
content_type,
size: content_len as i64,
},
id: metadata.id,
name: metadata.name.clone(),
source: metadata.source,
extract: None,
}
})
.collect::<Vec<_>>();

View File

@ -76,6 +76,7 @@ pub fn insert_chat(mut conn: DBConnection, new_chat: &ChatTable) -> QueryResult<
.execute(&mut *conn)
}
#[allow(dead_code)]
pub fn update_chat(
conn: &mut SqliteConnection,
changeset: ChatTableChangeset,
@ -93,6 +94,7 @@ pub fn read_chat(mut conn: DBConnection, chat_id_val: &str) -> QueryResult<ChatT
Ok(row)
}
#[allow(dead_code)]
pub fn read_chat_metadata(
conn: &mut SqliteConnection,
chat_id_val: &str,