diff --git a/frontend/appflowy_flutter/ios/Podfile.lock b/frontend/appflowy_flutter/ios/Podfile.lock index c54ae23ed6..72df3b0bd7 100644 --- a/frontend/appflowy_flutter/ios/Podfile.lock +++ b/frontend/appflowy_flutter/ios/Podfile.lock @@ -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 diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_ai_message_bloc.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_ai_message_bloc.dart index bf45326961..77e68803f9 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_ai_message_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_ai_message_bloc.dart @@ -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 { @@ -16,10 +18,12 @@ class ChatAIMessageBloc extends Bloc { 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 { 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 { ), ); }, - receiveMetadata: (List metadata) { + receiveSources: (List 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 data, + const factory ChatAIMessageEvent.receiveSources( + List sources, ) = _ReceiveMetadata; } @@ -147,16 +151,18 @@ class ChatAIMessageState with _$ChatAIMessageState { AnswerStream? stream, required String text, required MessageState messageState, - required List metadata, + required List sources, }) = _ChatAIMessageState; factory ChatAIMessageState.initial( - dynamic text, List metadata,) { + dynamic text, + List sources, + ) { return ChatAIMessageState( text: text is String ? text : "", stream: text is AnswerStream ? text : null, messageState: const MessageState.ready(), - metadata: metadata, + sources: sources, ); } } diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_bloc.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_bloc.dart index 5c85256de3..da8d9cd2fe 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_bloc.dart @@ -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 { ChatBloc({ @@ -412,7 +413,7 @@ class ChatBloc extends Bloc { author: User(id: "streamId:${nanoid()}"), metadata: { "$AnswerStream": stream, - "question": questionMessageId, + messageQuestionIdKey: questionMessageId, "chatId": chatId, }, id: streamMessageId, @@ -435,7 +436,7 @@ class ChatBloc extends Bloc { 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 metadata)? _onMetadata; + void Function(List 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 metadata)? onMetadata, + void Function(List metadata)? onMetadata, }) { _onData = onData; _onStart = onStart; @@ -661,55 +662,22 @@ class AnswerStream { } } -List chatMessageMetadataFromString(String? s) { - if (s == null || s.isEmpty || s == "null") { - return []; - } - - final List metadata = []; - try { - final metadataJson = jsonDecode(s); - if (metadataJson == null) { - Log.warn("metadata is null"); - return []; - } - - if (metadataJson is Map) { - if (metadataJson.isNotEmpty) { - metadata.add(ChatMessageMetadata.fromJson(metadataJson)); - } - } else if (metadataJson is List) { - metadata.addAll( - metadataJson.map( - (e) => ChatMessageMetadata.fromJson(e as Map), - ), - ); - } 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 json) => - _$ChatMessageMetadataFromJson(json); + factory ChatMessageRefSource.fromJson(Map json) => + _$ChatMessageRefSourceFromJson(json); final String id; final String name; final String source; - Map toJson() => _$ChatMessageMetadataToJson(this); + Map toJson() => _$ChatMessageRefSourceToJson(this); } @freezed diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_file_bloc.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_file_bloc.dart index 032ae0b5e5..4d05e27fd5 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_file_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_file_bloc.dart @@ -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; + class ChatFileBloc extends Bloc { - ChatFileBloc({ - required String chatId, - }) : listener = LocalLLMListener(), + ChatFileBloc() + : listener = LocalLLMListener(), super(const ChatFileState()) { listener.start( stateCallback: (pluginState) { @@ -49,38 +55,15 @@ class ChatFileBloc extends Bloc { }, newFile: (String filePath, String fileName) async { final files = List.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 { ), ); }, + deleteFile: (file) { + final files = List.from(state.uploadFiles); + files.remove(file); + emit( + state.copyWith( + uploadFiles: files, + ), + ); + }, clear: () { emit( state.copyWith( @@ -116,14 +108,24 @@ class ChatFileBloc extends Bloc { ), ); }, - updateUploadState: (UploadFileIndicator indicator) { - emit(state.copyWith(uploadFileIndicator: indicator)); - }, ); }, ); } + ChatInputFileMetadata consumeMetaData() { + final metadata = state.uploadFiles.fold( + {}, + (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 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 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); + } + } } diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_input_action_bloc.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_input_action_bloc.dart index 5787a06ef4..466f82ca0b 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_input_action_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_input_action_bloc.dart @@ -81,7 +81,7 @@ class ChatInputActionBloc ), ); }, - addPage: (ChatInputActionPage page) { + addPage: (ChatInputMention page) { if (!state.selectedPages.any((p) => p.pageId == page.pageId)) { final List pages = _filterPages( state.views, @@ -97,7 +97,7 @@ class ChatInputActionBloc } }, removePage: (String text) { - final List selectedPages = + final List selectedPages = List.from(state.selectedPages); selectedPages.retainWhere((t) => !text.contains(t.title)); @@ -128,7 +128,7 @@ class ChatInputActionBloc List _filterPages( List views, - List selectedPages, + List selectedPages, String filter, ) { final pages = views @@ -152,7 +152,7 @@ List _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 views, - @Default([]) List pages, - @Default([]) List selectedPages, + @Default([]) List pages, + @Default([]) List selectedPages, @Default("") String filter, ChatInputKeyboardEvent? keyboardKey, @Default(ChatActionMenuIndicator.loading()) diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_input_action_control.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_input_action_control.dart index 51362c052f..b945b9c2d7 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_input_action_control.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_input_action_control.dart @@ -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; +/// Key: the key is the pageId +typedef ChatInputMentionMetadata = Map; class ChatInputActionControl extends ChatActionHandler { ChatInputActionControl({ @@ -35,9 +36,9 @@ class ChatInputActionControl extends ChatActionHandler { List get tags => _commandBloc.state.selectedPages.map((e) => e.title).toList(); - ChatInputMetadata consumeMetaData() { + ChatInputMentionMetadata consumeMetaData() { final metadata = _commandBloc.state.selectedPages.fold( - {}, + {}, (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}"; diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_input_bloc.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_input_bloc.dart index 811c61ff57..a0cedd2900 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_input_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_input_bloc.dart @@ -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; +} diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_input_file_bloc.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_input_file_bloc.dart new file mode 100644 index 0000000000..ff67ab2019 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_input_file_bloc.dart @@ -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 { + ChatInputFileBloc({ + required String chatId, + required this.file, + }) : super(const ChatInputFileState()) { + on( + (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; +} diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_message_service.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_message_service.dart index 6c627e56c5..71fa3a18d2 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_message_service.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_message_service.dart @@ -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 fileListFromMessageMetadata( + Map? map, +) { + final List metadata = []; + if (map != null) { + for (final entry in map.entries) { + if (entry.value is ChatFile) { + metadata.add(entry.value); + } + } + } + + return metadata; +} + +List chatFilesFromMetadataString(String? s) { + if (s == null || s.isEmpty || s == "null") { + return []; + } + + final metadataJson = jsonDecode(s); + if (metadataJson is Map) { + return _parseChatFile(metadataJson); + } else if (metadataJson is List) { + return metadataJson + .map((e) => e as Map) + .map(chatFileFromMap) + .where((file) => file != null) + .cast() + .toList(); + } else { + Log.error("Invalid metadata: $metadataJson"); + return []; + } +} + +List _parseChatFile(Map map) { + final file = chatFileFromMap(map); + return file != null ? [file] : []; +} + +ChatFile? chatFileFromMap(Map? 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 messageRefSourceFromString(String? s) { + if (s == null || s.isEmpty || s == "null") { + return []; + } + + final List metadata = []; + try { + final metadataJson = jsonDecode(s); + if (metadataJson == null) { + Log.warn("metadata is null"); + return []; + } + + if (metadataJson is Map) { + if (metadataJson.isNotEmpty) { + metadata.add(ChatMessageRefSource.fromJson(metadataJson)); + } + } else if (metadataJson is List) { + metadata.addAll( + metadataJson.map( + (e) => ChatMessageRefSource.fromJson(e as Map), + ), + ); + } else { + Log.error("Invalid metadata: $metadataJson"); + } + } catch (e) { + Log.error("Failed to parse metadata: $e"); + } + + return metadata; +} Future> metadataPBFromMetadata( Map? map, @@ -24,6 +116,7 @@ Future> metadataPBFromMetadata( id: view.id, name: view.name, data: pb.text, + dataType: ChatMessageMetaTypePB.Txt, source: "appflowy document", ), ); @@ -32,6 +125,16 @@ Future> 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, + ), + ); } } } diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_side_pannel_bloc.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_side_pannel_bloc.dart index b79252d6b6..fe0439f918 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_side_pannel_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_side_pannel_bloc.dart @@ -19,7 +19,7 @@ class ChatSidePannelBloc on( (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, diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_user_message_bloc.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_user_message_bloc.dart index aa40d65078..a436936344 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_user_message_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_user_message_bloc.dart @@ -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 { 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( (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 files, }) = _ChatUserMessageState; factory ChatUserMessageState.initial( - Message message, - ChatMember? member, + TextMessage message, + List files, ) => - ChatUserMessageState(message: message, member: member); + ChatUserMessageState(message: message, files: files); } diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart index 2fb0869886..f7882eb351 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart @@ -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( - listenWhen: (previous, current) => - previous.uploadFileIndicator != current.uploadFileIndicator, - listener: (context, state) { - _handleIndexIndicator(state.uploadFileIndicator, context); - }, - child: BlocBuilder( - 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() - .add(ChatFileEvent.newFile(file.path, file.name)); - } - }, - description: '', - ); + child: BlocBuilder( + builder: (context, state) { + return DropTarget( + onDragDone: (DropDoneDetails detail) async { + if (state.supportChatWithFile) { + for (final file in detail.files) { + context + .read() + .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() .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().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), diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_input/chat_at_button.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_input/chat_at_button.dart index 45a39fcb72..53741f4431 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_input/chat_at_button.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_input/chat_at_button.dart @@ -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, ), diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_input/chat_input.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_input/chat_input.dart index 7bf7549990..4bea505e79 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_input/chat_input.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_input/chat_input.dart @@ -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 { @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 { 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 { 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().state.uploadFiles.isNotEmpty) + Padding( + padding: EdgeInsets.only( + top: 12, + bottom: 12, + left: textPadding.left + sendButtonSize, + right: textPadding.right, + ), + child: BlocBuilder( + builder: (context, state) { + return ChatInputFile( + chatId: widget.chatId, + files: state.uploadFiles, + onDeleted: (file) => context.read().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 { void _handleSendPressed() { final trimmedText = _textController.text.trim(); if (trimmedText != '') { + // consume metadata + final ChatInputMentionMetadata mentionPageMetadata = + _inputActionControl.consumeMetaData(); + final ChatInputFileMetadata fileMetadata = + context.read().consumeMetaData(); + + // combine metadata + final Map 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 { } 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 { return Padding( padding: buttonPadding, child: SizedBox.square( - dimension: 26, + dimension: sendButtonSize, child: ChatInputSendButton( onSendPressed: () { if (!_sendButtonEnabled) { @@ -317,7 +329,7 @@ class _ChatInputState extends State { return Padding( padding: buttonPadding, child: SizedBox.square( - dimension: 26, + dimension: attachButtonSize, child: ChatInputAttachment( onTap: () async { final path = await getIt().pickFiles( @@ -348,7 +360,7 @@ class _ChatInputState extends State { return Padding( padding: buttonPadding, child: SizedBox.square( - dimension: 26, + dimension: attachButtonSize, child: ChatInputAtButton( onTap: () { _textController.text += '@'; diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_input/chat_attachment.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_input/chat_input_attachment.dart similarity index 94% rename from frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_input/chat_attachment.dart rename to frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_input/chat_input_attachment.dart index 13cbbadfb1..b4eee5d8ef 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_input/chat_attachment.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_input/chat_input_attachment.dart @@ -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, ), diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_input/chat_input_file.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_input/chat_input_file.dart new file mode 100644 index 0000000000..87bef8044b --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_input/chat_input_file.dart @@ -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 files; + final String chatId; + + final Function(ChatFile) onDeleted; + + @override + Widget build(BuildContext context) { + final List 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( + 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, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_input/chat_send_button.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_input/chat_send_button.dart index 33c9e47b02..2de77e9362 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_input/chat_send_button.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_input/chat_send_button.dart @@ -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, ); diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_input/layout_define.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_input/layout_define.dart new file mode 100644 index 0000000000..74a66d0130 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_input/layout_define.dart @@ -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; diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_input_action_menu.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_input_action_menu.dart index af181c6e46..faf586e3b9 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_input_action_menu.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_input_action_menu.dart @@ -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 pages; + final List pages; final bool isLoading; @override @@ -257,7 +257,7 @@ class _ActionListState extends State { 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, diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_welcome_page.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_welcome_page.dart index 6576d13446..f1ec5d2a7d 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_welcome_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_welcome_page.dart @@ -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 items = [ - LocaleKeys.chat_question1.tr(), - LocaleKeys.chat_question2.tr(), - LocaleKeys.chat_question3.tr(), - LocaleKeys.chat_question4.tr(), + final List 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, ), ], diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_metadata.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_metadata.dart index f807d84c96..5cf9cee2f5 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_metadata.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_metadata.dart @@ -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 metadata; - final Function(ChatMessageMetadata metadata) onSelectedMetadata; + final List 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, diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_text_message.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_text_message.dart index abe12d7a0a..582f587049 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_text_message.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_text_message.dart @@ -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, ), ], diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/user_text_message.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/user_text_message.dart index 0c507ace16..e9bf1928ac 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/user_text_message.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/user_text_message.dart @@ -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( + 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 files; + + @override + Widget build(BuildContext context) { + final List 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, + ), + ), + ], + ), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_on_boarding_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_on_boarding_bloc.dart index 66cfa11bf0..3a56e93a32 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_on_boarding_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_on_boarding_bloc.dart @@ -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 { LocalAIOnBoardingBloc(this.userProfile) : super(const LocalAIOnBoardingState()) { + _userService = UserBackendService(userId: userProfile.id); + _successListenable = getIt(); + _successListenable.addListener(_onPaymentSuccessful); _dispatch(); } + Future _onPaymentSuccessful() async { + if (isClosed) { + return; + } + + add( + LocalAIOnBoardingEvent.paymentSuccessful( + _successListenable.subscribedPlan, + ), + ); + } + final UserProfilePB userProfile; + late final IUserBackendService _userService; + late final SubscriptionSuccessListenable _successListenable; void _dispatch() { on((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 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; } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/settings_ai_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/settings_ai_view.dart index 79f1de6da0..c5e160fb03 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/settings_ai_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/settings_ai_view.dart @@ -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().add( - const SettingsDialogEvent.setSelectedPage( - SettingsPage.plan, + context.read().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( + builder: (context, state) { + if (state.isLoading) { + return const CircularProgressIndicator.adaptive(); + } else { + return Toggle( + value: false, + onChanged: (_) => widget.onTap(), + ); + } + }, + ), + ], ); } } diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.lock b/frontend/appflowy_tauri/src-tauri/Cargo.lock index 2f5b138d61..9e99c50cd7 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.lock +++ b/frontend/appflowy_tauri/src-tauri/Cargo.lock @@ -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", diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.toml b/frontend/appflowy_tauri/src-tauri/Cargo.toml index 103ea91b32..d9279126c8 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.toml +++ b/frontend/appflowy_tauri/src-tauri/Cargo.toml @@ -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 diff --git a/frontend/appflowy_web_app/src-tauri/Cargo.lock b/frontend/appflowy_web_app/src-tauri/Cargo.lock index 9f8bf03b05..b77a35c7a1 100644 --- a/frontend/appflowy_web_app/src-tauri/Cargo.lock +++ b/frontend/appflowy_web_app/src-tauri/Cargo.lock @@ -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", diff --git a/frontend/appflowy_web_app/src-tauri/Cargo.toml b/frontend/appflowy_web_app/src-tauri/Cargo.toml index 7815cbbe03..b157b83731 100644 --- a/frontend/appflowy_web_app/src-tauri/Cargo.toml +++ b/frontend/appflowy_web_app/src-tauri/Cargo.toml @@ -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 diff --git a/frontend/resources/flowy_icons/16x/ai_star.svg b/frontend/resources/flowy_icons/16x/ai_star.svg index b98634fda1..336e160f6f 100644 --- a/frontend/resources/flowy_icons/16x/ai_star.svg +++ b/frontend/resources/flowy_icons/16x/ai_star.svg @@ -1,3 +1 @@ - - - +Ai Chip Spark Streamline Icon: https://streamlinehq.com \ No newline at end of file diff --git a/frontend/resources/flowy_icons/16x/chat_at.svg b/frontend/resources/flowy_icons/16x/chat_at.svg new file mode 100644 index 0000000000..2d4c8d507b --- /dev/null +++ b/frontend/resources/flowy_icons/16x/chat_at.svg @@ -0,0 +1 @@ +At Sign Streamline Icon: https://streamlinehq.com \ No newline at end of file diff --git a/frontend/resources/flowy_icons/16x/chat_feather.svg b/frontend/resources/flowy_icons/16x/chat_feather.svg new file mode 100644 index 0000000000..7a960957c0 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/chat_feather.svg @@ -0,0 +1 @@ +Feather Streamline Icon: https://streamlinehq.com \ No newline at end of file diff --git a/frontend/resources/flowy_icons/16x/chat_lightbulb.svg b/frontend/resources/flowy_icons/16x/chat_lightbulb.svg new file mode 100644 index 0000000000..dc5d1806e4 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/chat_lightbulb.svg @@ -0,0 +1 @@ +Lightbulb Streamline Icon: https://streamlinehq.com \ No newline at end of file diff --git a/frontend/resources/flowy_icons/16x/chat_pen.svg b/frontend/resources/flowy_icons/16x/chat_pen.svg new file mode 100644 index 0000000000..d26493b164 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/chat_pen.svg @@ -0,0 +1 @@ +Pen Line Streamline Icon: https://streamlinehq.com \ No newline at end of file diff --git a/frontend/resources/flowy_icons/16x/chat_question.svg b/frontend/resources/flowy_icons/16x/chat_question.svg new file mode 100644 index 0000000000..db16cb1f9d --- /dev/null +++ b/frontend/resources/flowy_icons/16x/chat_question.svg @@ -0,0 +1 @@ +Message Circle Question Streamline Icon: https://streamlinehq.com \ No newline at end of file diff --git a/frontend/resources/flowy_icons/16x/chat_scholar.svg b/frontend/resources/flowy_icons/16x/chat_scholar.svg new file mode 100644 index 0000000000..49fb435836 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/chat_scholar.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/frontend/resources/flowy_icons/16x/file_md.svg b/frontend/resources/flowy_icons/16x/file_md.svg new file mode 100644 index 0000000000..0414453c10 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/file_md.svg @@ -0,0 +1 @@ +File Md Light Streamline Icon: https://streamlinehq.com \ No newline at end of file diff --git a/frontend/resources/flowy_icons/16x/file_pdf.svg b/frontend/resources/flowy_icons/16x/file_pdf.svg new file mode 100644 index 0000000000..04684dc74a --- /dev/null +++ b/frontend/resources/flowy_icons/16x/file_pdf.svg @@ -0,0 +1 @@ +File Pdf Light Streamline Icon: https://streamlinehq.com \ No newline at end of file diff --git a/frontend/resources/flowy_icons/16x/file_txt.svg b/frontend/resources/flowy_icons/16x/file_txt.svg new file mode 100644 index 0000000000..27c5aec523 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/file_txt.svg @@ -0,0 +1 @@ +File Txt Light Streamline Icon: https://streamlinehq.com \ No newline at end of file diff --git a/frontend/resources/flowy_icons/16x/file_unknown.svg b/frontend/resources/flowy_icons/16x/file_unknown.svg new file mode 100644 index 0000000000..cc4225f5f0 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/file_unknown.svg @@ -0,0 +1 @@ +File X Light Streamline Icon: https://streamlinehq.com \ No newline at end of file diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index fd4dcc9c9b..a38096f9c8 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -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": { diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index 9d993c5e22..253255f99d 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -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", diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index 8fd538c6fb..828b04d84d 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -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 diff --git a/frontend/rust-lib/flowy-ai-pub/src/cloud.rs b/frontend/rust-lib/flowy-ai-pub/src/cloud.rs index 2793cd18a6..520ea640e6 100644 --- a/frontend/rust-lib/flowy-ai-pub/src/cloud.rs +++ b/frontend/rust-lib/flowy-ai-pub/src/cloud.rs @@ -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; diff --git a/frontend/rust-lib/flowy-ai/src/ai_manager.rs b/frontend/rust-lib/flowy-ai/src/ai_manager.rs index 424b141e4d..243a18d76d 100644 --- a/frontend/rust-lib/flowy-ai/src/ai_manager.rs +++ b/frontend/rust-lib/flowy-ai/src/ai_manager.rs @@ -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<()> { diff --git a/frontend/rust-lib/flowy-ai/src/chat.rs b/frontend/rust-lib/flowy-ai/src/chat.rs index 11116f451f..90e7aae401 100644 --- a/frontend/rust-lib/flowy-ai/src/chat.rs +++ b/frontend/rust-lib/flowy-ai/src/chat.rs @@ -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, diff --git a/frontend/rust-lib/flowy-ai/src/entities.rs b/frontend/rust-lib/flowy-ai/src/entities.rs index 5ff3cd5bd3..3693773177 100644 --- a/frontend/rust-lib/flowy-ai/src/entities.rs +++ b/frontend/rust-lib/flowy-ai/src/entities.rs @@ -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)] diff --git a/frontend/rust-lib/flowy-ai/src/event_handler.rs b/frontend/rust-lib/flowy-ai/src/event_handler.rs index dd8e984ee4..3b7f46dcc6 100644 --- a/frontend/rust-lib/flowy-ai/src/event_handler.rs +++ b/frontend/rust-lib/flowy-ai/src/event_handler.rs @@ -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::>(); diff --git a/frontend/rust-lib/flowy-ai/src/persistence/chat_sql.rs b/frontend/rust-lib/flowy-ai/src/persistence/chat_sql.rs index 6086c8e466..e962f2c880 100644 --- a/frontend/rust-lib/flowy-ai/src/persistence/chat_sql.rs +++ b/frontend/rust-lib/flowy-ai/src/persistence/chat_sql.rs @@ -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