mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: Implement menu on chat (#5879)
* chore: implement menu * chore: gather metadata * chore: update client api * chore: stream metadata * chore: save metadata * chore: update client api * chore: adjust UI * chore: fmt
This commit is contained in:
parent
0abf916796
commit
d378c456d4
@ -13,9 +13,13 @@ part 'chat_ai_message_bloc.freezed.dart';
|
|||||||
class ChatAIMessageBloc extends Bloc<ChatAIMessageEvent, ChatAIMessageState> {
|
class ChatAIMessageBloc extends Bloc<ChatAIMessageEvent, ChatAIMessageState> {
|
||||||
ChatAIMessageBloc({
|
ChatAIMessageBloc({
|
||||||
dynamic message,
|
dynamic message,
|
||||||
|
String? metadata,
|
||||||
required this.chatId,
|
required this.chatId,
|
||||||
required this.questionId,
|
required this.questionId,
|
||||||
}) : super(ChatAIMessageState.initial(message)) {
|
}) : super(ChatAIMessageState.initial(
|
||||||
|
message,
|
||||||
|
chatMessageMetadataFromString(metadata),
|
||||||
|
),) {
|
||||||
if (state.stream != null) {
|
if (state.stream != null) {
|
||||||
state.stream!.listen(
|
state.stream!.listen(
|
||||||
onData: (text) {
|
onData: (text) {
|
||||||
@ -33,6 +37,11 @@ class ChatAIMessageBloc extends Bloc<ChatAIMessageEvent, ChatAIMessageState> {
|
|||||||
add(const ChatAIMessageEvent.onAIResponseLimit());
|
add(const ChatAIMessageEvent.onAIResponseLimit());
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
onMetadata: (metadata) {
|
||||||
|
if (!isClosed) {
|
||||||
|
add(ChatAIMessageEvent.receiveMetadata(metadata));
|
||||||
|
}
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
if (state.stream!.error != null) {
|
if (state.stream!.error != null) {
|
||||||
@ -103,6 +112,13 @@ class ChatAIMessageBloc extends Bloc<ChatAIMessageEvent, ChatAIMessageState> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
receiveMetadata: (List<ChatMessageMetadata> metadata) {
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
metadata: metadata,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -120,6 +136,9 @@ class ChatAIMessageEvent with _$ChatAIMessageEvent {
|
|||||||
const factory ChatAIMessageEvent.retry() = _Retry;
|
const factory ChatAIMessageEvent.retry() = _Retry;
|
||||||
const factory ChatAIMessageEvent.retryResult(String text) = _RetryResult;
|
const factory ChatAIMessageEvent.retryResult(String text) = _RetryResult;
|
||||||
const factory ChatAIMessageEvent.onAIResponseLimit() = _OnAIResponseLimit;
|
const factory ChatAIMessageEvent.onAIResponseLimit() = _OnAIResponseLimit;
|
||||||
|
const factory ChatAIMessageEvent.receiveMetadata(
|
||||||
|
List<ChatMessageMetadata> data,
|
||||||
|
) = _ReceiveMetadata;
|
||||||
}
|
}
|
||||||
|
|
||||||
@freezed
|
@freezed
|
||||||
@ -128,13 +147,16 @@ class ChatAIMessageState with _$ChatAIMessageState {
|
|||||||
AnswerStream? stream,
|
AnswerStream? stream,
|
||||||
required String text,
|
required String text,
|
||||||
required MessageState messageState,
|
required MessageState messageState,
|
||||||
|
required List<ChatMessageMetadata> metadata,
|
||||||
}) = _ChatAIMessageState;
|
}) = _ChatAIMessageState;
|
||||||
|
|
||||||
factory ChatAIMessageState.initial(dynamic text) {
|
factory ChatAIMessageState.initial(
|
||||||
|
dynamic text, List<ChatMessageMetadata> metadata,) {
|
||||||
return ChatAIMessageState(
|
return ChatAIMessageState(
|
||||||
text: text is String ? text : "",
|
text: text is String ? text : "",
|
||||||
stream: text is AnswerStream ? text : null,
|
stream: text is AnswerStream ? text : null,
|
||||||
messageState: const MessageState.ready(),
|
messageState: const MessageState.ready(),
|
||||||
|
metadata: metadata,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:collection';
|
import 'dart:collection';
|
||||||
|
import 'dart:convert';
|
||||||
import 'dart:ffi';
|
import 'dart:ffi';
|
||||||
import 'dart:isolate';
|
import 'dart:isolate';
|
||||||
|
|
||||||
@ -19,7 +20,9 @@ import 'package:freezed_annotation/freezed_annotation.dart';
|
|||||||
import 'package:nanoid/nanoid.dart';
|
import 'package:nanoid/nanoid.dart';
|
||||||
|
|
||||||
import 'chat_message_listener.dart';
|
import 'chat_message_listener.dart';
|
||||||
|
import 'chat_message_service.dart';
|
||||||
|
|
||||||
|
part 'chat_bloc.g.dart';
|
||||||
part 'chat_bloc.freezed.dart';
|
part 'chat_bloc.freezed.dart';
|
||||||
|
|
||||||
const sendMessageErrorKey = "sendMessageError";
|
const sendMessageErrorKey = "sendMessageError";
|
||||||
@ -153,8 +156,8 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
sendMessage: (String message) {
|
sendMessage: (String message, Map<String, dynamic>? metadata) async {
|
||||||
_startStreamingMessage(message, emit);
|
unawaited(_startStreamingMessage(message, metadata, emit));
|
||||||
final allMessages = _perminentMessages();
|
final allMessages = _perminentMessages();
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
@ -327,6 +330,7 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
|
|||||||
|
|
||||||
Future<void> _startStreamingMessage(
|
Future<void> _startStreamingMessage(
|
||||||
String message,
|
String message,
|
||||||
|
Map<String, dynamic>? metadata,
|
||||||
Emitter<ChatState> emit,
|
Emitter<ChatState> emit,
|
||||||
) async {
|
) async {
|
||||||
if (state.answerStream != null) {
|
if (state.answerStream != null) {
|
||||||
@ -341,6 +345,7 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
|
|||||||
message: message,
|
message: message,
|
||||||
messageType: ChatMessageTypePB.User,
|
messageType: ChatMessageTypePB.User,
|
||||||
textStreamPort: Int64(answerStream.nativePort),
|
textStreamPort: Int64(answerStream.nativePort),
|
||||||
|
metadata: await metadataPBFromMetadata(metadata),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Stream message to the server
|
// Stream message to the server
|
||||||
@ -410,6 +415,9 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
|
|||||||
id: messageId,
|
id: messageId,
|
||||||
text: message.content,
|
text: message.content,
|
||||||
createdAt: message.createdAt.toInt() * 1000,
|
createdAt: message.createdAt.toInt() * 1000,
|
||||||
|
metadata: {
|
||||||
|
"metadata": message.metadata,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -417,7 +425,10 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
|
|||||||
@freezed
|
@freezed
|
||||||
class ChatEvent with _$ChatEvent {
|
class ChatEvent with _$ChatEvent {
|
||||||
const factory ChatEvent.initialLoad() = _InitialLoadMessage;
|
const factory ChatEvent.initialLoad() = _InitialLoadMessage;
|
||||||
const factory ChatEvent.sendMessage(String message) = _SendMessage;
|
const factory ChatEvent.sendMessage({
|
||||||
|
required String message,
|
||||||
|
Map<String, dynamic>? metadata,
|
||||||
|
}) = _SendMessage;
|
||||||
const factory ChatEvent.startLoadingPrevMessage() = _StartLoadPrevMessage;
|
const factory ChatEvent.startLoadingPrevMessage() = _StartLoadPrevMessage;
|
||||||
const factory ChatEvent.didLoadPreviousMessages(
|
const factory ChatEvent.didLoadPreviousMessages(
|
||||||
List<Message> messages,
|
List<Message> messages,
|
||||||
@ -542,6 +553,11 @@ class AnswerStream {
|
|||||||
if (_onError != null) {
|
if (_onError != null) {
|
||||||
_onError!(_error!);
|
_onError!(_error!);
|
||||||
}
|
}
|
||||||
|
} else if (event.startsWith("metadata:")) {
|
||||||
|
if (_onMetadata != null) {
|
||||||
|
final s = event.substring(9);
|
||||||
|
_onMetadata!(chatMessageMetadataFromString(s));
|
||||||
|
}
|
||||||
} else if (event == "AI_RESPONSE_LIMIT") {
|
} else if (event == "AI_RESPONSE_LIMIT") {
|
||||||
if (_onAIResponseLimit != null) {
|
if (_onAIResponseLimit != null) {
|
||||||
_onAIResponseLimit!();
|
_onAIResponseLimit!();
|
||||||
@ -574,6 +590,7 @@ class AnswerStream {
|
|||||||
void Function()? _onEnd;
|
void Function()? _onEnd;
|
||||||
void Function(String error)? _onError;
|
void Function(String error)? _onError;
|
||||||
void Function()? _onAIResponseLimit;
|
void Function()? _onAIResponseLimit;
|
||||||
|
void Function(List<ChatMessageMetadata> metadata)? _onMetadata;
|
||||||
|
|
||||||
int get nativePort => _port.sendPort.nativePort;
|
int get nativePort => _port.sendPort.nativePort;
|
||||||
bool get hasStarted => _hasStarted;
|
bool get hasStarted => _hasStarted;
|
||||||
@ -592,15 +609,66 @@ class AnswerStream {
|
|||||||
void Function()? onEnd,
|
void Function()? onEnd,
|
||||||
void Function(String error)? onError,
|
void Function(String error)? onError,
|
||||||
void Function()? onAIResponseLimit,
|
void Function()? onAIResponseLimit,
|
||||||
|
void Function(List<ChatMessageMetadata> metadata)? onMetadata,
|
||||||
}) {
|
}) {
|
||||||
_onData = onData;
|
_onData = onData;
|
||||||
_onStart = onStart;
|
_onStart = onStart;
|
||||||
_onEnd = onEnd;
|
_onEnd = onEnd;
|
||||||
_onError = onError;
|
_onError = onError;
|
||||||
_onAIResponseLimit = onAIResponseLimit;
|
_onAIResponseLimit = onAIResponseLimit;
|
||||||
|
_onMetadata = onMetadata;
|
||||||
|
|
||||||
if (_onStart != null) {
|
if (_onStart != null) {
|
||||||
_onStart!();
|
_onStart!();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<ChatMessageMetadata> chatMessageMetadataFromString(String? s) {
|
||||||
|
if (s == null || s.isEmpty || s == "null") {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
final List<ChatMessageMetadata> metadata = [];
|
||||||
|
try {
|
||||||
|
final metadataJson = jsonDecode(s);
|
||||||
|
if (metadataJson == null) {
|
||||||
|
Log.warn("metadata is null");
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (metadataJson is Map<String, dynamic>) {
|
||||||
|
metadata.add(ChatMessageMetadata.fromJson(metadataJson));
|
||||||
|
} else if (metadataJson is List) {
|
||||||
|
metadata.addAll(
|
||||||
|
metadataJson.map(
|
||||||
|
(e) => ChatMessageMetadata.fromJson(e as Map<String, dynamic>),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
Log.error("Invalid metadata: $metadataJson");
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
Log.error("Failed to parse metadata: $e");
|
||||||
|
}
|
||||||
|
|
||||||
|
return metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
class ChatMessageMetadata {
|
||||||
|
ChatMessageMetadata({
|
||||||
|
required this.id,
|
||||||
|
required this.name,
|
||||||
|
required this.source,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory ChatMessageMetadata.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$ChatMessageMetadataFromJson(json);
|
||||||
|
|
||||||
|
final String id;
|
||||||
|
final String name;
|
||||||
|
final String source;
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => _$ChatMessageMetadataToJson(this);
|
||||||
|
}
|
||||||
|
@ -0,0 +1,161 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||||
|
import 'package:appflowy/workspace/application/view/view_service.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||||
|
import 'package:bloc/bloc.dart';
|
||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
|
||||||
|
import 'chat_input_action_control.dart';
|
||||||
|
part 'chat_input_action_bloc.freezed.dart';
|
||||||
|
|
||||||
|
class ChatInputActionBloc
|
||||||
|
extends Bloc<ChatInputActionEvent, ChatInputActionState> {
|
||||||
|
ChatInputActionBloc({required this.chatId})
|
||||||
|
: super(const ChatInputActionState()) {
|
||||||
|
on<ChatInputActionEvent>(_handleEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
final String chatId;
|
||||||
|
|
||||||
|
Future<void> _handleEvent(
|
||||||
|
ChatInputActionEvent event,
|
||||||
|
Emitter<ChatInputActionState> emit,
|
||||||
|
) async {
|
||||||
|
await event.when(
|
||||||
|
started: () async {
|
||||||
|
unawaited(
|
||||||
|
ViewBackendService.getAllViews().then(
|
||||||
|
(result) {
|
||||||
|
final views = result
|
||||||
|
.toNullable()
|
||||||
|
?.items
|
||||||
|
.where((v) => v.layout.isDocumentView)
|
||||||
|
.toList() ??
|
||||||
|
[];
|
||||||
|
if (!isClosed) {
|
||||||
|
add(ChatInputActionEvent.refreshViews(views));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
refreshViews: (List<ViewPB> views) {
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
views: views,
|
||||||
|
pages: views.map((v) => ViewActionPage(view: v)).toList(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
filter: (String filter) {
|
||||||
|
final List<ViewActionPage> pages = [];
|
||||||
|
if (filter.isEmpty) {
|
||||||
|
pages.addAll(state.views.map((v) => ViewActionPage(view: v)));
|
||||||
|
} else {
|
||||||
|
pages.addAll(
|
||||||
|
state.views
|
||||||
|
.where(
|
||||||
|
(v) => v.name.toLowerCase().contains(
|
||||||
|
filter.toLowerCase(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.map(
|
||||||
|
(v) => ViewActionPage(view: v),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
pages.retainWhere((view) {
|
||||||
|
return !state.selectedPages.contains(view);
|
||||||
|
});
|
||||||
|
emit(state.copyWith(pages: pages));
|
||||||
|
},
|
||||||
|
handleKeyEvent: (PhysicalKeyboardKey physicalKey) {
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
keyboardKey: ChatInputKeyboardEvent(physicalKey: physicalKey),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
addPage: (ChatInputActionPage page) {
|
||||||
|
if (!state.selectedPages.any((p) => p.pageId == page.pageId)) {
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
selectedPages: [...state.selectedPages, page],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
removePage: (String text) {
|
||||||
|
final List<ChatInputActionPage> selectedPages =
|
||||||
|
List.from(state.selectedPages);
|
||||||
|
selectedPages.retainWhere((t) => !text.contains(t.title));
|
||||||
|
|
||||||
|
final allPages =
|
||||||
|
state.views.map((v) => ViewActionPage(view: v)).toList();
|
||||||
|
allPages.retainWhere((view) => !selectedPages.contains(view));
|
||||||
|
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
selectedPages: selectedPages,
|
||||||
|
pages: allPages,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ViewActionPage extends ChatInputActionPage {
|
||||||
|
ViewActionPage({required this.view});
|
||||||
|
|
||||||
|
final ViewPB view;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pageId => view.id;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get title => view.name;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [pageId];
|
||||||
|
|
||||||
|
@override
|
||||||
|
dynamic get page => view;
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class ChatInputActionEvent with _$ChatInputActionEvent {
|
||||||
|
const factory ChatInputActionEvent.started() = _Started;
|
||||||
|
const factory ChatInputActionEvent.refreshViews(List<ViewPB> views) =
|
||||||
|
_RefreshViews;
|
||||||
|
const factory ChatInputActionEvent.filter(String filter) = _Filter;
|
||||||
|
const factory ChatInputActionEvent.handleKeyEvent(
|
||||||
|
PhysicalKeyboardKey keyboardKey,
|
||||||
|
) = _HandleKeyEvent;
|
||||||
|
const factory ChatInputActionEvent.addPage(ChatInputActionPage page) =
|
||||||
|
_AddPage;
|
||||||
|
const factory ChatInputActionEvent.removePage(String text) = _RemovePage;
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class ChatInputActionState with _$ChatInputActionState {
|
||||||
|
const factory ChatInputActionState({
|
||||||
|
@Default([]) List<ViewPB> views,
|
||||||
|
@Default([]) List<ChatInputActionPage> pages,
|
||||||
|
@Default([]) List<ChatInputActionPage> selectedPages,
|
||||||
|
ChatInputKeyboardEvent? keyboardKey,
|
||||||
|
}) = _ChatInputActionState;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ChatInputKeyboardEvent extends Equatable {
|
||||||
|
ChatInputKeyboardEvent({required this.physicalKey});
|
||||||
|
|
||||||
|
final PhysicalKeyboardKey physicalKey;
|
||||||
|
final int timestamp = DateTime.now().millisecondsSinceEpoch;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [timestamp];
|
||||||
|
}
|
@ -0,0 +1,170 @@
|
|||||||
|
import 'package:appflowy/plugins/ai_chat/application/chat_input_action_bloc.dart';
|
||||||
|
import 'package:appflowy/plugins/ai_chat/presentation/chat_input_action_menu.dart';
|
||||||
|
import 'package:appflowy_backend/log.dart';
|
||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
abstract class ChatInputActionPage extends Equatable {
|
||||||
|
String get title;
|
||||||
|
String get pageId;
|
||||||
|
dynamic get page;
|
||||||
|
}
|
||||||
|
|
||||||
|
typedef ChatInputMetadata = Map<String, ChatInputActionPage>;
|
||||||
|
|
||||||
|
class ChatInputActionControl extends ChatActionHandler {
|
||||||
|
ChatInputActionControl({
|
||||||
|
required this.textController,
|
||||||
|
required this.textFieldFocusNode,
|
||||||
|
required this.chatId,
|
||||||
|
}) : _commandBloc = ChatInputActionBloc(chatId: chatId);
|
||||||
|
|
||||||
|
final TextEditingController textController;
|
||||||
|
final ChatInputActionBloc _commandBloc;
|
||||||
|
final FocusNode textFieldFocusNode;
|
||||||
|
final String chatId;
|
||||||
|
|
||||||
|
// Private attributes
|
||||||
|
bool _isShowActionMenu = false;
|
||||||
|
String _atText = "";
|
||||||
|
String _prevText = "";
|
||||||
|
bool _didLoadViews = false;
|
||||||
|
|
||||||
|
// Getter
|
||||||
|
List<String> get tags =>
|
||||||
|
_commandBloc.state.selectedPages.map((e) => e.title).toList();
|
||||||
|
|
||||||
|
ChatInputMetadata get metaData => _commandBloc.state.selectedPages.fold(
|
||||||
|
<String, ChatInputActionPage>{},
|
||||||
|
(map, page) => map..putIfAbsent(page.pageId, () => page),
|
||||||
|
);
|
||||||
|
|
||||||
|
void handleKeyEvent(KeyEvent event) {
|
||||||
|
// ignore: deprecated_member_use
|
||||||
|
if (event is KeyDownEvent || event is RawKeyDownEvent) {
|
||||||
|
commandBloc.add(ChatInputActionEvent.handleKeyEvent(event.physicalKey));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool canHandleKeyEvent(KeyEvent event) {
|
||||||
|
return _isShowActionMenu &&
|
||||||
|
<PhysicalKeyboardKey>{
|
||||||
|
PhysicalKeyboardKey.arrowDown,
|
||||||
|
PhysicalKeyboardKey.arrowUp,
|
||||||
|
PhysicalKeyboardKey.enter,
|
||||||
|
PhysicalKeyboardKey.escape,
|
||||||
|
}.contains(event.physicalKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
void dispose() {
|
||||||
|
commandBloc.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onSelected(ChatInputActionPage page) {
|
||||||
|
_atText = "";
|
||||||
|
_isShowActionMenu = false;
|
||||||
|
|
||||||
|
_commandBloc.add(ChatInputActionEvent.addPage(page));
|
||||||
|
textController.text =
|
||||||
|
"${textController.text.replaceAll(_atText, '')}${page.title}";
|
||||||
|
_prevText = textController.text;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onExit() {
|
||||||
|
_atText = "";
|
||||||
|
_isShowActionMenu = false;
|
||||||
|
_didLoadViews = false;
|
||||||
|
commandBloc.add(const ChatInputActionEvent.filter(""));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onEnter() {
|
||||||
|
if (!_didLoadViews) {
|
||||||
|
_didLoadViews = true;
|
||||||
|
commandBloc.add(const ChatInputActionEvent.started());
|
||||||
|
}
|
||||||
|
_isShowActionMenu = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
double actionMenuOffsetX() {
|
||||||
|
final TextPosition textPosition = textController.selection.extent;
|
||||||
|
if (textFieldFocusNode.context == null) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
final RenderBox renderBox =
|
||||||
|
textFieldFocusNode.context?.findRenderObject() as RenderBox;
|
||||||
|
|
||||||
|
final TextPainter textPainter = TextPainter(
|
||||||
|
text: TextSpan(text: textController.text),
|
||||||
|
textDirection: TextDirection.ltr,
|
||||||
|
);
|
||||||
|
textPainter.layout(
|
||||||
|
minWidth: renderBox.size.width,
|
||||||
|
maxWidth: renderBox.size.width,
|
||||||
|
);
|
||||||
|
|
||||||
|
final Offset caretOffset =
|
||||||
|
textPainter.getOffsetForCaret(textPosition, Rect.zero);
|
||||||
|
final List<TextBox> boxes = textPainter.getBoxesForSelection(
|
||||||
|
TextSelection(
|
||||||
|
baseOffset: textPosition.offset,
|
||||||
|
extentOffset: textPosition.offset,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (boxes.isNotEmpty) {
|
||||||
|
return boxes.last.right;
|
||||||
|
}
|
||||||
|
return caretOffset.dx;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool onTextChanged(String text) {
|
||||||
|
final String inputText = text;
|
||||||
|
if (_prevText.length > inputText.length) {
|
||||||
|
final deleteStartIndex = textController.selection.baseOffset;
|
||||||
|
final deleteEndIndex =
|
||||||
|
_prevText.length - inputText.length + deleteStartIndex;
|
||||||
|
final deletedText = _prevText.substring(deleteStartIndex, deleteEndIndex);
|
||||||
|
_commandBloc.add(ChatInputActionEvent.removePage(deletedText));
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the action menu is shown, filter the views
|
||||||
|
if (_isShowActionMenu) {
|
||||||
|
// before filter the views, remove the first character '@' if it exists
|
||||||
|
if (inputText.startsWith("@")) {
|
||||||
|
final filter = inputText.substring(1);
|
||||||
|
commandBloc.add(ChatInputActionEvent.filter(filter));
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the text change from "xxx @"" to "xxx", which means user delete the @, we should hide the action menu
|
||||||
|
if (_atText.isNotEmpty && !inputText.contains(_atText)) {
|
||||||
|
commandBloc.add(
|
||||||
|
const ChatInputActionEvent.handleKeyEvent(PhysicalKeyboardKey.escape),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
final isTypingNewAt =
|
||||||
|
text.endsWith("@") && _prevText.length < text.length;
|
||||||
|
if (isTypingNewAt) {
|
||||||
|
_atText = text;
|
||||||
|
_prevText = text;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_prevText = text;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onFilter(String filter) {
|
||||||
|
Log.info("filter: $filter");
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
ChatInputActionBloc get commandBloc => _commandBloc;
|
||||||
|
}
|
@ -8,19 +8,20 @@ import 'package:bloc/bloc.dart';
|
|||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
part 'chat_input_bloc.freezed.dart';
|
part 'chat_input_bloc.freezed.dart';
|
||||||
|
|
||||||
class ChatInputBloc extends Bloc<ChatInputEvent, ChatInputState> {
|
class ChatInputStateBloc
|
||||||
ChatInputBloc()
|
extends Bloc<ChatInputStateEvent, ChatInputStateState> {
|
||||||
|
ChatInputStateBloc()
|
||||||
: listener = LocalLLMListener(),
|
: listener = LocalLLMListener(),
|
||||||
super(const ChatInputState(aiType: _AppFlowyAI())) {
|
super(const ChatInputStateState(aiType: _AppFlowyAI())) {
|
||||||
listener.start(
|
listener.start(
|
||||||
stateCallback: (pluginState) {
|
stateCallback: (pluginState) {
|
||||||
if (!isClosed) {
|
if (!isClosed) {
|
||||||
add(ChatInputEvent.updatePluginState(pluginState));
|
add(ChatInputStateEvent.updatePluginState(pluginState));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
on<ChatInputEvent>(_handleEvent);
|
on<ChatInputStateEvent>(_handleEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
final LocalLLMListener listener;
|
final LocalLLMListener listener;
|
||||||
@ -32,8 +33,8 @@ class ChatInputBloc extends Bloc<ChatInputEvent, ChatInputState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _handleEvent(
|
Future<void> _handleEvent(
|
||||||
ChatInputEvent event,
|
ChatInputStateEvent event,
|
||||||
Emitter<ChatInputState> emit,
|
Emitter<ChatInputStateState> emit,
|
||||||
) async {
|
) async {
|
||||||
await event.when(
|
await event.when(
|
||||||
started: () async {
|
started: () async {
|
||||||
@ -42,7 +43,7 @@ class ChatInputBloc extends Bloc<ChatInputEvent, ChatInputState> {
|
|||||||
(pluginState) {
|
(pluginState) {
|
||||||
if (!isClosed) {
|
if (!isClosed) {
|
||||||
add(
|
add(
|
||||||
ChatInputEvent.updatePluginState(pluginState),
|
ChatInputStateEvent.updatePluginState(pluginState),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -53,9 +54,9 @@ class ChatInputBloc extends Bloc<ChatInputEvent, ChatInputState> {
|
|||||||
},
|
},
|
||||||
updatePluginState: (pluginState) {
|
updatePluginState: (pluginState) {
|
||||||
if (pluginState.state == RunningStatePB.Running) {
|
if (pluginState.state == RunningStatePB.Running) {
|
||||||
emit(const ChatInputState(aiType: _LocalAI()));
|
emit(const ChatInputStateState(aiType: _LocalAI()));
|
||||||
} else {
|
} else {
|
||||||
emit(const ChatInputState(aiType: _AppFlowyAI()));
|
emit(const ChatInputStateState(aiType: _AppFlowyAI()));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -63,16 +64,16 @@ class ChatInputBloc extends Bloc<ChatInputEvent, ChatInputState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@freezed
|
@freezed
|
||||||
class ChatInputEvent with _$ChatInputEvent {
|
class ChatInputStateEvent with _$ChatInputStateEvent {
|
||||||
const factory ChatInputEvent.started() = _Started;
|
const factory ChatInputStateEvent.started() = _Started;
|
||||||
const factory ChatInputEvent.updatePluginState(
|
const factory ChatInputStateEvent.updatePluginState(
|
||||||
LocalAIPluginStatePB pluginState,
|
LocalAIPluginStatePB pluginState,
|
||||||
) = _UpdatePluginState;
|
) = _UpdatePluginState;
|
||||||
}
|
}
|
||||||
|
|
||||||
@freezed
|
@freezed
|
||||||
class ChatInputState with _$ChatInputState {
|
class ChatInputStateState with _$ChatInputStateState {
|
||||||
const factory ChatInputState({required AIType aiType}) = _ChatInputState;
|
const factory ChatInputStateState({required AIType aiType}) = _ChatInputState;
|
||||||
}
|
}
|
||||||
|
|
||||||
@freezed
|
@freezed
|
||||||
|
@ -0,0 +1,36 @@
|
|||||||
|
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';
|
||||||
|
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';
|
||||||
|
|
||||||
|
Future<List<ChatMessageMetaPB>> metadataPBFromMetadata(
|
||||||
|
Map<String, dynamic>? map,) async {
|
||||||
|
final List<ChatMessageMetaPB> metadata = [];
|
||||||
|
if (map != null) {
|
||||||
|
for (final entry in map.entries) {
|
||||||
|
if (entry.value is ViewActionPage) {
|
||||||
|
if (entry.value.page is ViewPB) {
|
||||||
|
final view = entry.value.page as ViewPB;
|
||||||
|
if (view.layout.isDocumentView) {
|
||||||
|
final payload = OpenDocumentPayloadPB(documentId: view.id);
|
||||||
|
final result = await DocumentEventGetDocumentText(payload).send();
|
||||||
|
result.fold((pb) {
|
||||||
|
metadata.add(ChatMessageMetaPB(
|
||||||
|
id: view.id,
|
||||||
|
name: view.name,
|
||||||
|
text: pb.text,
|
||||||
|
),);
|
||||||
|
}, (err) {
|
||||||
|
Log.error('Failed to get document text: $err');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return metadata;
|
||||||
|
}
|
@ -0,0 +1,85 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:appflowy/plugins/ai_chat/application/chat_bloc.dart';
|
||||||
|
import 'package:appflowy/workspace/application/view/view_service.dart';
|
||||||
|
import 'package:appflowy_backend/log.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
|
||||||
|
part 'chat_side_pannel_bloc.freezed.dart';
|
||||||
|
|
||||||
|
const double kDefaultSidePannelWidth = 500;
|
||||||
|
|
||||||
|
class ChatSidePannelBloc
|
||||||
|
extends Bloc<ChatSidePannelEvent, ChatSidePannelState> {
|
||||||
|
ChatSidePannelBloc({
|
||||||
|
required this.chatId,
|
||||||
|
}) : super(const ChatSidePannelState()) {
|
||||||
|
on<ChatSidePannelEvent>(
|
||||||
|
(event, emit) async {
|
||||||
|
await event.when(
|
||||||
|
selectedMetadata: (ChatMessageMetadata metadata) async {
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
metadata: metadata,
|
||||||
|
indicator: const ChatSidePannelIndicator.loading(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
unawaited(
|
||||||
|
ViewBackendService.getView(metadata.id).then(
|
||||||
|
(result) {
|
||||||
|
result.fold((view) {
|
||||||
|
if (!isClosed) {
|
||||||
|
add(ChatSidePannelEvent.open(view));
|
||||||
|
}
|
||||||
|
}, (err) {
|
||||||
|
Log.error("Failed to get view: $err");
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
close: () {
|
||||||
|
emit(state.copyWith(metadata: null, isShowPannel: false));
|
||||||
|
},
|
||||||
|
open: (ViewPB view) {
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
indicator: ChatSidePannelIndicator.ready(view),
|
||||||
|
isShowPannel: true,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final String chatId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class ChatSidePannelEvent with _$ChatSidePannelEvent {
|
||||||
|
const factory ChatSidePannelEvent.selectedMetadata(
|
||||||
|
ChatMessageMetadata metadata,
|
||||||
|
) = _SelectedMetadata;
|
||||||
|
const factory ChatSidePannelEvent.close() = _Close;
|
||||||
|
const factory ChatSidePannelEvent.open(ViewPB view) = _Open;
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class ChatSidePannelState with _$ChatSidePannelState {
|
||||||
|
const factory ChatSidePannelState({
|
||||||
|
ChatMessageMetadata? metadata,
|
||||||
|
@Default(ChatSidePannelIndicator.loading())
|
||||||
|
ChatSidePannelIndicator indicator,
|
||||||
|
@Default(false) bool isShowPannel,
|
||||||
|
}) = _ChatSidePannelState;
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class ChatSidePannelIndicator with _$ChatSidePannelIndicator {
|
||||||
|
const factory ChatSidePannelIndicator.ready(ViewPB view) = _Ready;
|
||||||
|
const factory ChatSidePannelIndicator.loading() = _Loading;
|
||||||
|
}
|
@ -6,6 +6,7 @@ import 'package:appflowy/workspace/application/view_info/view_info_bloc.dart';
|
|||||||
import 'package:appflowy/workspace/presentation/home/home_stack.dart';
|
import 'package:appflowy/workspace/presentation/home/home_stack.dart';
|
||||||
import 'package:appflowy/workspace/presentation/widgets/tab_bar_item.dart';
|
import 'package:appflowy/workspace/presentation/widgets/tab_bar_item.dart';
|
||||||
import 'package:appflowy/workspace/presentation/widgets/view_title_bar.dart';
|
import 'package:appflowy/workspace/presentation/widgets/view_title_bar.dart';
|
||||||
|
import 'package:appflowy_backend/log.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
@ -104,6 +105,11 @@ class AIChatPagePluginWidgetBuilder extends PluginWidgetBuilder
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (context.userProfile == null) {
|
||||||
|
Log.error("User profile is null when opening AI Chat plugin");
|
||||||
|
return const SizedBox();
|
||||||
|
}
|
||||||
|
|
||||||
return BlocProvider<ViewInfoBloc>.value(
|
return BlocProvider<ViewInfoBloc>.value(
|
||||||
value: bloc,
|
value: bloc,
|
||||||
child: AIChatPage(
|
child: AIChatPage(
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:appflowy/plugins/ai_chat/application/chat_file_bloc.dart';
|
import 'package:appflowy/plugins/ai_chat/application/chat_file_bloc.dart';
|
||||||
import 'package:appflowy/plugins/ai_chat/application/chat_input_bloc.dart';
|
import 'package:appflowy/plugins/ai_chat/application/chat_input_bloc.dart';
|
||||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart';
|
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart';
|
||||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||||
import 'package:desktop_drop/desktop_drop.dart';
|
import 'package:desktop_drop/desktop_drop.dart';
|
||||||
|
import 'package:flowy_infra/platform_extension.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
@ -22,9 +25,12 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
|||||||
import 'package:flutter_chat_types/flutter_chat_types.dart';
|
import 'package:flutter_chat_types/flutter_chat_types.dart';
|
||||||
import 'package:flutter_chat_types/flutter_chat_types.dart' as types;
|
import 'package:flutter_chat_types/flutter_chat_types.dart' as types;
|
||||||
import 'package:flutter_chat_ui/flutter_chat_ui.dart' show Chat;
|
import 'package:flutter_chat_ui/flutter_chat_ui.dart' show Chat;
|
||||||
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
|
||||||
|
import 'application/chat_side_pannel_bloc.dart';
|
||||||
import 'presentation/chat_input/chat_input.dart';
|
import 'presentation/chat_input/chat_input.dart';
|
||||||
import 'presentation/chat_popmenu.dart';
|
import 'presentation/chat_popmenu.dart';
|
||||||
|
import 'presentation/chat_side_pannel.dart';
|
||||||
import 'presentation/chat_theme.dart';
|
import 'presentation/chat_theme.dart';
|
||||||
import 'presentation/chat_user_invalid_message.dart';
|
import 'presentation/chat_user_invalid_message.dart';
|
||||||
import 'presentation/chat_welcome_page.dart';
|
import 'presentation/chat_welcome_page.dart';
|
||||||
@ -72,19 +78,29 @@ class AIChatPage extends StatelessWidget {
|
|||||||
if (userProfile.authenticator == AuthenticatorPB.AppFlowyCloud) {
|
if (userProfile.authenticator == AuthenticatorPB.AppFlowyCloud) {
|
||||||
return MultiBlocProvider(
|
return MultiBlocProvider(
|
||||||
providers: [
|
providers: [
|
||||||
BlocProvider(
|
/// [ChatBloc] is used to handle chat messages including send/receive message
|
||||||
create: (_) => ChatFileBloc(chatId: view.id.toString())
|
///
|
||||||
..add(const ChatFileEvent.initial()),
|
|
||||||
),
|
|
||||||
BlocProvider(
|
BlocProvider(
|
||||||
create: (_) => ChatBloc(
|
create: (_) => ChatBloc(
|
||||||
view: view,
|
view: view,
|
||||||
userProfile: userProfile,
|
userProfile: userProfile,
|
||||||
)..add(const ChatEvent.initialLoad()),
|
)..add(const ChatEvent.initialLoad()),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
/// [ChatFileBloc] is used to handle file indexing as a chat context
|
||||||
|
///
|
||||||
BlocProvider(
|
BlocProvider(
|
||||||
create: (_) => ChatInputBloc()..add(const ChatInputEvent.started()),
|
create: (_) => ChatFileBloc(chatId: view.id.toString())
|
||||||
|
..add(const ChatFileEvent.initial()),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
/// [ChatInputStateBloc] is used to handle chat input text field state
|
||||||
|
///
|
||||||
|
BlocProvider(
|
||||||
|
create: (_) =>
|
||||||
|
ChatInputStateBloc()..add(const ChatInputStateEvent.started()),
|
||||||
|
),
|
||||||
|
BlocProvider(create: (_) => ChatSidePannelBloc(chatId: view.id)),
|
||||||
],
|
],
|
||||||
child: BlocListener<ChatFileBloc, ChatFileState>(
|
child: BlocListener<ChatFileBloc, ChatFileState>(
|
||||||
listenWhen: (previous, current) =>
|
listenWhen: (previous, current) =>
|
||||||
@ -187,7 +203,71 @@ class _ChatContentPageState extends State<_ChatContentPage> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (widget.userProfile.authenticator == AuthenticatorPB.AppFlowyCloud) {
|
if (widget.userProfile.authenticator == AuthenticatorPB.AppFlowyCloud) {
|
||||||
return buildChatWidget();
|
if (PlatformExtension.isDesktop) {
|
||||||
|
return BlocSelector<ChatSidePannelBloc, ChatSidePannelState, bool>(
|
||||||
|
selector: (state) => state.isShowPannel,
|
||||||
|
builder: (context, isShowPannel) {
|
||||||
|
return LayoutBuilder(
|
||||||
|
builder: (BuildContext context, BoxConstraints constraints) {
|
||||||
|
final double chatOffsetX = isShowPannel
|
||||||
|
? 60
|
||||||
|
: (constraints.maxWidth > 784
|
||||||
|
? (constraints.maxWidth - 784) / 2.0
|
||||||
|
: 60);
|
||||||
|
|
||||||
|
final double width = isShowPannel
|
||||||
|
? (constraints.maxWidth - chatOffsetX * 2) * 0.46
|
||||||
|
: min(constraints.maxWidth - chatOffsetX * 2, 784);
|
||||||
|
|
||||||
|
final double sidePannelOffsetX = chatOffsetX + width;
|
||||||
|
|
||||||
|
return Stack(
|
||||||
|
alignment: AlignmentDirectional.centerStart,
|
||||||
|
children: [
|
||||||
|
buildChatWidget()
|
||||||
|
.constrained(width: width)
|
||||||
|
.positioned(
|
||||||
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
|
left: chatOffsetX,
|
||||||
|
animate: true,
|
||||||
|
)
|
||||||
|
.animate(
|
||||||
|
const Duration(milliseconds: 200),
|
||||||
|
Curves.easeOut,
|
||||||
|
),
|
||||||
|
if (isShowPannel)
|
||||||
|
buildChatSidePannel()
|
||||||
|
.positioned(
|
||||||
|
left: sidePannelOffsetX,
|
||||||
|
right: 0,
|
||||||
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
|
animate: true,
|
||||||
|
)
|
||||||
|
.animate(
|
||||||
|
const Duration(milliseconds: 200),
|
||||||
|
Curves.easeOut,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Flexible(
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(maxWidth: 784),
|
||||||
|
child: buildChatWidget(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Center(
|
return Center(
|
||||||
@ -198,14 +278,25 @@ class _ChatContentPageState extends State<_ChatContentPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget buildChatSidePannel() {
|
||||||
|
if (PlatformExtension.isDesktop) {
|
||||||
|
return BlocBuilder<ChatSidePannelBloc, ChatSidePannelState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
if (state.metadata != null) {
|
||||||
|
return const ChatSidePannel();
|
||||||
|
} else {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// TODO(lucas): implement mobile chat side panel
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Widget buildChatWidget() {
|
Widget buildChatWidget() {
|
||||||
return Row(
|
return BlocBuilder<ChatBloc, ChatState>(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Flexible(
|
|
||||||
child: ConstrainedBox(
|
|
||||||
constraints: const BoxConstraints(maxWidth: 784),
|
|
||||||
child: BlocBuilder<ChatBloc, ChatState>(
|
|
||||||
builder: (blocContext, state) => Chat(
|
builder: (blocContext, state) => Chat(
|
||||||
messages: state.messages,
|
messages: state.messages,
|
||||||
onSendPressed: (_) {
|
onSendPressed: (_) {
|
||||||
@ -217,8 +308,7 @@ class _ChatContentPageState extends State<_ChatContentPage> {
|
|||||||
theme: buildTheme(context),
|
theme: buildTheme(context),
|
||||||
onEndReached: () async {
|
onEndReached: () async {
|
||||||
if (state.hasMorePrevMessage &&
|
if (state.hasMorePrevMessage &&
|
||||||
state.loadingPreviousStatus !=
|
state.loadingPreviousStatus != const LoadingState.loading()) {
|
||||||
const LoadingState.loading()) {
|
|
||||||
blocContext
|
blocContext
|
||||||
.read<ChatBloc>()
|
.read<ChatBloc>()
|
||||||
.add(const ChatEvent.startLoadingPrevMessage());
|
.add(const ChatEvent.startLoadingPrevMessage());
|
||||||
@ -232,7 +322,7 @@ class _ChatContentPageState extends State<_ChatContentPage> {
|
|||||||
child: ChatWelcomePage(
|
child: ChatWelcomePage(
|
||||||
onSelectedQuestion: (question) => blocContext
|
onSelectedQuestion: (question) => blocContext
|
||||||
.read<ChatBloc>()
|
.read<ChatBloc>()
|
||||||
.add(ChatEvent.sendMessage(question)),
|
.add(ChatEvent.sendMessage(message: question)),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: const Center(
|
: const Center(
|
||||||
@ -261,10 +351,6 @@ class _ChatContentPageState extends State<_ChatContentPage> {
|
|||||||
return _buildAIBubble(message, blocContext, state, child);
|
return _buildAIBubble(message, blocContext, state, child);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -279,6 +365,7 @@ class _ChatContentPageState extends State<_ChatContentPage> {
|
|||||||
} else {
|
} else {
|
||||||
final stream = message.metadata?["$AnswerStream"];
|
final stream = message.metadata?["$AnswerStream"];
|
||||||
final questionId = message.metadata?["question"];
|
final questionId = message.metadata?["question"];
|
||||||
|
final metadata = message.metadata?["metadata"] as String?;
|
||||||
return ChatAITextMessageWidget(
|
return ChatAITextMessageWidget(
|
||||||
user: message.author,
|
user: message.author,
|
||||||
messageUserId: message.id,
|
messageUserId: message.id,
|
||||||
@ -286,6 +373,12 @@ class _ChatContentPageState extends State<_ChatContentPage> {
|
|||||||
key: ValueKey(message.id),
|
key: ValueKey(message.id),
|
||||||
questionId: questionId,
|
questionId: questionId,
|
||||||
chatId: widget.view.id,
|
chatId: widget.view.id,
|
||||||
|
metadata: metadata,
|
||||||
|
onSelectedMetadata: (ChatMessageMetadata metadata) {
|
||||||
|
context.read<ChatSidePannelBloc>().add(
|
||||||
|
ChatSidePannelEvent.selectedMetadata(metadata),
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -309,7 +402,9 @@ class _ChatContentPageState extends State<_ChatContentPage> {
|
|||||||
if (messageType == OnetimeShotType.relatedQuestion) {
|
if (messageType == OnetimeShotType.relatedQuestion) {
|
||||||
return RelatedQuestionList(
|
return RelatedQuestionList(
|
||||||
onQuestionSelected: (question) {
|
onQuestionSelected: (question) {
|
||||||
blocContext.read<ChatBloc>().add(ChatEvent.sendMessage(question));
|
blocContext
|
||||||
|
.read<ChatBloc>()
|
||||||
|
.add(ChatEvent.sendMessage(message: question));
|
||||||
blocContext
|
blocContext
|
||||||
.read<ChatBloc>()
|
.read<ChatBloc>()
|
||||||
.add(const ChatEvent.clearReleatedQuestion());
|
.add(const ChatEvent.clearReleatedQuestion());
|
||||||
@ -391,8 +486,9 @@ class _ChatContentPageState extends State<_ChatContentPage> {
|
|||||||
return ClipRect(
|
return ClipRect(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: AIChatUILayout.safeAreaInsets(context),
|
padding: AIChatUILayout.safeAreaInsets(context),
|
||||||
child: BlocBuilder<ChatInputBloc, ChatInputState>(
|
child: BlocBuilder<ChatInputStateBloc, ChatInputStateState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
|
// Show different hint text based on the AI type
|
||||||
final hintText = state.aiType.when(
|
final hintText = state.aiType.when(
|
||||||
appflowyAI: () => LocaleKeys.chat_inputMessageHint.tr(),
|
appflowyAI: () => LocaleKeys.chat_inputMessageHint.tr(),
|
||||||
localAI: () => LocaleKeys.chat_inputLocalAIMessageHint.tr(),
|
localAI: () => LocaleKeys.chat_inputLocalAIMessageHint.tr(),
|
||||||
@ -405,8 +501,14 @@ class _ChatContentPageState extends State<_ChatContentPage> {
|
|||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
return ChatInput(
|
return ChatInput(
|
||||||
chatId: widget.view.id,
|
chatId: widget.view.id,
|
||||||
onSendPressed: (message) =>
|
onSendPressed: (message) {
|
||||||
onSendPressed(context, message.text),
|
context.read<ChatBloc>().add(
|
||||||
|
ChatEvent.sendMessage(
|
||||||
|
message: message.text,
|
||||||
|
metadata: message.metadata,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
isStreaming: state != const LoadingState.finish(),
|
isStreaming: state != const LoadingState.finish(),
|
||||||
onStopStreaming: () {
|
onStopStreaming: () {
|
||||||
context
|
context
|
||||||
@ -432,8 +534,9 @@ class _ChatContentPageState extends State<_ChatContentPage> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
AFDefaultChatTheme buildTheme(BuildContext context) {
|
AFDefaultChatTheme buildTheme(BuildContext context) {
|
||||||
return AFDefaultChatTheme(
|
return AFDefaultChatTheme(
|
||||||
backgroundColor: AFThemeExtension.of(context).background,
|
backgroundColor: AFThemeExtension.of(context).background,
|
||||||
primaryColor: Theme.of(context).colorScheme.primary,
|
primaryColor: Theme.of(context).colorScheme.primary,
|
||||||
@ -477,9 +580,4 @@ class _ChatContentPageState extends State<_ChatContentPage> {
|
|||||||
),
|
),
|
||||||
inputElevation: 2,
|
inputElevation: 2,
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
void onSendPressed(BuildContext context, String message) {
|
|
||||||
context.read<ChatBloc>().add(ChatEvent.sendMessage(message));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,237 +0,0 @@
|
|||||||
import 'package:flowy_infra_ui/style_widget/button.dart';
|
|
||||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
|
|
||||||
abstract class ChatActionMenuItem {
|
|
||||||
String get title;
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract class ChatActionHandler {
|
|
||||||
List<ChatActionMenuItem> get items;
|
|
||||||
void onEnter();
|
|
||||||
void onSelected(ChatActionMenuItem item);
|
|
||||||
void onExit();
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract class ChatAnchor {
|
|
||||||
GlobalKey get anchorKey;
|
|
||||||
LayerLink get layerLink;
|
|
||||||
}
|
|
||||||
|
|
||||||
const int _itemHeight = 34;
|
|
||||||
const int _itemVerticalPadding = 4;
|
|
||||||
|
|
||||||
class ChatActionsMenu {
|
|
||||||
ChatActionsMenu({
|
|
||||||
required this.anchor,
|
|
||||||
required this.context,
|
|
||||||
required this.handler,
|
|
||||||
required this.style,
|
|
||||||
});
|
|
||||||
|
|
||||||
final BuildContext context;
|
|
||||||
final ChatAnchor anchor;
|
|
||||||
final ChatActionsMenuStyle style;
|
|
||||||
final ChatActionHandler handler;
|
|
||||||
|
|
||||||
OverlayEntry? _overlayEntry;
|
|
||||||
|
|
||||||
void dismiss() {
|
|
||||||
_overlayEntry?.remove();
|
|
||||||
_overlayEntry = null;
|
|
||||||
handler.onExit();
|
|
||||||
}
|
|
||||||
|
|
||||||
void show() {
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) => _show());
|
|
||||||
}
|
|
||||||
|
|
||||||
void _show() {
|
|
||||||
if (_overlayEntry != null) {
|
|
||||||
dismiss();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (anchor.anchorKey.currentContext == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
handler.onEnter();
|
|
||||||
|
|
||||||
final height = handler.items.length * (_itemHeight + _itemVerticalPadding);
|
|
||||||
_overlayEntry = OverlayEntry(
|
|
||||||
builder: (context) => Stack(
|
|
||||||
children: [
|
|
||||||
CompositedTransformFollower(
|
|
||||||
link: anchor.layerLink,
|
|
||||||
showWhenUnlinked: false,
|
|
||||||
offset: Offset(0, -height - 4),
|
|
||||||
child: Material(
|
|
||||||
elevation: 4.0,
|
|
||||||
child: ConstrainedBox(
|
|
||||||
constraints: const BoxConstraints(
|
|
||||||
minWidth: 200,
|
|
||||||
maxWidth: 200,
|
|
||||||
maxHeight: 200,
|
|
||||||
),
|
|
||||||
child: DecoratedBox(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color:
|
|
||||||
Theme.of(context).colorScheme.surfaceContainerHighest,
|
|
||||||
borderRadius: BorderRadius.circular(6.0),
|
|
||||||
),
|
|
||||||
child: ActionList(
|
|
||||||
handler: handler,
|
|
||||||
onDismiss: () => dismiss(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
Overlay.of(context).insert(_overlayEntry!);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ActionItem extends StatelessWidget {
|
|
||||||
const _ActionItem({
|
|
||||||
required this.item,
|
|
||||||
required this.onTap,
|
|
||||||
required this.isSelected,
|
|
||||||
});
|
|
||||||
|
|
||||||
final ChatActionMenuItem item;
|
|
||||||
final VoidCallback? onTap;
|
|
||||||
final bool isSelected;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Container(
|
|
||||||
height: _itemHeight.toDouble(),
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: _itemVerticalPadding / 2.0),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: isSelected
|
|
||||||
? Theme.of(context).colorScheme.primary.withOpacity(0.1)
|
|
||||||
: Colors.transparent,
|
|
||||||
borderRadius: BorderRadius.circular(4.0),
|
|
||||||
),
|
|
||||||
child: FlowyButton(
|
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 6),
|
|
||||||
iconPadding: 10.0,
|
|
||||||
text: FlowyText.regular(
|
|
||||||
item.title,
|
|
||||||
),
|
|
||||||
onTap: onTap,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ActionList extends StatefulWidget {
|
|
||||||
const ActionList({super.key, required this.handler, required this.onDismiss});
|
|
||||||
|
|
||||||
final ChatActionHandler handler;
|
|
||||||
final VoidCallback? onDismiss;
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<ActionList> createState() => _ActionListState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ActionListState extends State<ActionList> {
|
|
||||||
final FocusScopeNode _focusNode =
|
|
||||||
FocusScopeNode(debugLabel: 'ChatActionsMenu');
|
|
||||||
int _selectedIndex = 0;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
||||||
_focusNode.requestFocus();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_focusNode.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _handleKeyPress(event) {
|
|
||||||
setState(() {
|
|
||||||
// ignore: deprecated_member_use
|
|
||||||
if (event is KeyDownEvent || event is RawKeyDownEvent) {
|
|
||||||
if (event.logicalKey == LogicalKeyboardKey.arrowDown) {
|
|
||||||
_selectedIndex = (_selectedIndex + 1) % widget.handler.items.length;
|
|
||||||
} else if (event.logicalKey == LogicalKeyboardKey.arrowUp) {
|
|
||||||
_selectedIndex = (_selectedIndex - 1 + widget.handler.items.length) %
|
|
||||||
widget.handler.items.length;
|
|
||||||
} else if (event.logicalKey == LogicalKeyboardKey.enter) {
|
|
||||||
widget.handler.onSelected(widget.handler.items[_selectedIndex]);
|
|
||||||
widget.onDismiss?.call();
|
|
||||||
} else if (event.logicalKey == LogicalKeyboardKey.escape) {
|
|
||||||
widget.onDismiss?.call();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return FocusScope(
|
|
||||||
node: _focusNode,
|
|
||||||
onKey: (node, event) {
|
|
||||||
_handleKeyPress(event);
|
|
||||||
return KeyEventResult.handled;
|
|
||||||
},
|
|
||||||
child: ListView(
|
|
||||||
shrinkWrap: true,
|
|
||||||
padding: const EdgeInsets.all(8),
|
|
||||||
children: widget.handler.items.asMap().entries.map((entry) {
|
|
||||||
final index = entry.key;
|
|
||||||
final ChatActionMenuItem item = entry.value;
|
|
||||||
return _ActionItem(
|
|
||||||
item: item,
|
|
||||||
onTap: () {
|
|
||||||
widget.handler.onSelected(item);
|
|
||||||
widget.onDismiss?.call();
|
|
||||||
},
|
|
||||||
isSelected: _selectedIndex == index,
|
|
||||||
);
|
|
||||||
}).toList(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ChatActionsMenuStyle {
|
|
||||||
ChatActionsMenuStyle({
|
|
||||||
required this.backgroundColor,
|
|
||||||
required this.groupTextColor,
|
|
||||||
required this.menuItemTextColor,
|
|
||||||
required this.menuItemSelectedColor,
|
|
||||||
required this.menuItemSelectedTextColor,
|
|
||||||
});
|
|
||||||
|
|
||||||
const ChatActionsMenuStyle.light()
|
|
||||||
: backgroundColor = Colors.white,
|
|
||||||
groupTextColor = const Color(0xFF555555),
|
|
||||||
menuItemTextColor = const Color(0xFF333333),
|
|
||||||
menuItemSelectedColor = const Color(0xFFE0F8FF),
|
|
||||||
menuItemSelectedTextColor = const Color.fromARGB(255, 56, 91, 247);
|
|
||||||
|
|
||||||
const ChatActionsMenuStyle.dark()
|
|
||||||
: backgroundColor = const Color(0xFF282E3A),
|
|
||||||
groupTextColor = const Color(0xFFBBC3CD),
|
|
||||||
menuItemTextColor = const Color(0xFFBBC3CD),
|
|
||||||
menuItemSelectedColor = const Color(0xFF00BCF0),
|
|
||||||
menuItemSelectedTextColor = const Color(0xFF131720);
|
|
||||||
|
|
||||||
final Color backgroundColor;
|
|
||||||
final Color groupTextColor;
|
|
||||||
final Color menuItemTextColor;
|
|
||||||
final Color menuItemSelectedColor;
|
|
||||||
final Color menuItemSelectedTextColor;
|
|
||||||
}
|
|
@ -0,0 +1,29 @@
|
|||||||
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||||
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flowy_infra/theme_extension.dart';
|
||||||
|
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
|
||||||
|
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class ChatInputAtButton extends StatelessWidget {
|
||||||
|
const ChatInputAtButton({required this.onTap, super.key});
|
||||||
|
|
||||||
|
final VoidCallback onTap;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return FlowyTooltip(
|
||||||
|
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),
|
||||||
|
),
|
||||||
|
onPressed: onTap,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,76 +0,0 @@
|
|||||||
import 'package:appflowy/plugins/ai_chat/presentation/chat_inline_action_menu.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
class ChatTextFieldInterceptor {
|
|
||||||
String previosText = "";
|
|
||||||
|
|
||||||
ChatActionHandler? onTextChanged(
|
|
||||||
String text,
|
|
||||||
TextEditingController textController,
|
|
||||||
FocusNode textFieldFocusNode,
|
|
||||||
) {
|
|
||||||
if (previosText == "/" && text == "/ ") {
|
|
||||||
final handler = IndexActionHandler(
|
|
||||||
textController: textController,
|
|
||||||
textFieldFocusNode: textFieldFocusNode,
|
|
||||||
) as ChatActionHandler;
|
|
||||||
return handler;
|
|
||||||
}
|
|
||||||
previosText = text;
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class FixGrammarMenuItem extends ChatActionMenuItem {
|
|
||||||
@override
|
|
||||||
String get title => "Fix Grammar";
|
|
||||||
}
|
|
||||||
|
|
||||||
class ImproveWritingMenuItem extends ChatActionMenuItem {
|
|
||||||
@override
|
|
||||||
String get title => "Improve Writing";
|
|
||||||
}
|
|
||||||
|
|
||||||
class ChatWithFileMenuItem extends ChatActionMenuItem {
|
|
||||||
@override
|
|
||||||
String get title => "Chat With PDF";
|
|
||||||
}
|
|
||||||
|
|
||||||
class IndexActionHandler extends ChatActionHandler {
|
|
||||||
IndexActionHandler({
|
|
||||||
required this.textController,
|
|
||||||
required this.textFieldFocusNode,
|
|
||||||
});
|
|
||||||
|
|
||||||
final TextEditingController textController;
|
|
||||||
final FocusNode textFieldFocusNode;
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<ChatActionMenuItem> get items => [
|
|
||||||
ChatWithFileMenuItem(),
|
|
||||||
FixGrammarMenuItem(),
|
|
||||||
ImproveWritingMenuItem(),
|
|
||||||
];
|
|
||||||
|
|
||||||
@override
|
|
||||||
void onSelected(ChatActionMenuItem item) {
|
|
||||||
textController.clear();
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback(
|
|
||||||
(_) => textFieldFocusNode.requestFocus(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void onExit() {
|
|
||||||
if (!textFieldFocusNode.hasFocus) {
|
|
||||||
textFieldFocusNode.requestFocus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void onEnter() {
|
|
||||||
if (textFieldFocusNode.hasFocus) {
|
|
||||||
textFieldFocusNode.unfocus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,12 +1,18 @@
|
|||||||
import 'package:appflowy/plugins/ai_chat/presentation/chat_inline_action_menu.dart';
|
import 'package:appflowy/plugins/ai_chat/presentation/chat_input_action_menu.dart';
|
||||||
|
import 'package:appflowy/plugins/ai_chat/application/chat_input_action_control.dart';
|
||||||
|
import 'package:extended_text_field/extended_text_field.dart';
|
||||||
|
import 'package:flowy_infra/platform_extension.dart';
|
||||||
import 'package:flowy_infra/theme_extension.dart';
|
import 'package:flowy_infra/theme_extension.dart';
|
||||||
|
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_chat_types/flutter_chat_types.dart' as types;
|
import 'package:flutter_chat_types/flutter_chat_types.dart' as types;
|
||||||
import 'package:flutter_chat_ui/flutter_chat_ui.dart';
|
import 'package:flutter_chat_ui/flutter_chat_ui.dart';
|
||||||
|
|
||||||
import 'chat_accessory_button.dart';
|
import 'chat_at_button.dart';
|
||||||
|
import 'chat_send_button.dart';
|
||||||
|
import 'chat_input_span.dart';
|
||||||
|
|
||||||
class ChatInput extends StatefulWidget {
|
class ChatInput extends StatefulWidget {
|
||||||
/// Creates [ChatInput] widget.
|
/// Creates [ChatInput] widget.
|
||||||
@ -39,41 +45,41 @@ class ChatInput extends StatefulWidget {
|
|||||||
class _ChatInputState extends State<ChatInput> {
|
class _ChatInputState extends State<ChatInput> {
|
||||||
final GlobalKey _textFieldKey = GlobalKey();
|
final GlobalKey _textFieldKey = GlobalKey();
|
||||||
final LayerLink _layerLink = LayerLink();
|
final LayerLink _layerLink = LayerLink();
|
||||||
// final ChatTextFieldInterceptor _textFieldInterceptor =
|
late ChatInputActionControl _inputActionControl;
|
||||||
// ChatTextFieldInterceptor();
|
late FocusNode _inputFocusNode;
|
||||||
|
late TextEditingController _textController;
|
||||||
|
bool _sendButtonEnabled = false;
|
||||||
|
|
||||||
late final _inputFocusNode = FocusNode(
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_textController = InputTextFieldController();
|
||||||
|
_inputFocusNode = FocusNode(
|
||||||
onKeyEvent: (node, event) {
|
onKeyEvent: (node, event) {
|
||||||
if (event.physicalKey == PhysicalKeyboardKey.enter &&
|
// TODO(lucas): support mobile
|
||||||
!HardwareKeyboard.instance.physicalKeysPressed.any(
|
if (PlatformExtension.isDesktop) {
|
||||||
(el) => <PhysicalKeyboardKey>{
|
if (_inputActionControl.canHandleKeyEvent(event)) {
|
||||||
PhysicalKeyboardKey.shiftLeft,
|
_inputActionControl.handleKeyEvent(event);
|
||||||
PhysicalKeyboardKey.shiftRight,
|
|
||||||
}.contains(el),
|
|
||||||
)) {
|
|
||||||
if (kIsWeb && _textController.value.isComposingRangeValid) {
|
|
||||||
return KeyEventResult.ignored;
|
|
||||||
}
|
|
||||||
if (event is KeyDownEvent) {
|
|
||||||
if (!widget.isStreaming) {
|
|
||||||
_handleSendPressed();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return KeyEventResult.handled;
|
return KeyEventResult.handled;
|
||||||
|
} else {
|
||||||
|
return _handleEnterKeyWithoutShift(
|
||||||
|
event,
|
||||||
|
_textController,
|
||||||
|
widget.isStreaming,
|
||||||
|
_handleSendPressed,
|
||||||
|
);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
return KeyEventResult.ignored;
|
return KeyEventResult.ignored;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
late TextEditingController _textController;
|
|
||||||
|
|
||||||
bool _sendButtonVisible = false;
|
_inputActionControl = ChatInputActionControl(
|
||||||
|
chatId: widget.chatId,
|
||||||
@override
|
textController: _textController,
|
||||||
void initState() {
|
textFieldFocusNode: _inputFocusNode,
|
||||||
super.initState();
|
);
|
||||||
|
|
||||||
_textController = InputTextFieldController();
|
|
||||||
_handleSendButtonVisibilityModeChange();
|
_handleSendButtonVisibilityModeChange();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -81,13 +87,14 @@ class _ChatInputState extends State<ChatInput> {
|
|||||||
void dispose() {
|
void dispose() {
|
||||||
_inputFocusNode.dispose();
|
_inputFocusNode.dispose();
|
||||||
_textController.dispose();
|
_textController.dispose();
|
||||||
|
_inputActionControl.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
const textPadding = EdgeInsets.symmetric(horizontal: 16, vertical: 6);
|
const textPadding = EdgeInsets.symmetric(horizontal: 16);
|
||||||
const buttonPadding = EdgeInsets.symmetric(horizontal: 16, vertical: 6);
|
const buttonPadding = EdgeInsets.symmetric(horizontal: 2);
|
||||||
const inputPadding = EdgeInsets.all(6);
|
const inputPadding = EdgeInsets.all(6);
|
||||||
|
|
||||||
return Focus(
|
return Focus(
|
||||||
@ -108,7 +115,11 @@ class _ChatInputState extends State<ChatInput> {
|
|||||||
padding: buttonPadding,
|
padding: buttonPadding,
|
||||||
),
|
),
|
||||||
Expanded(child: _inputTextField(textPadding)),
|
Expanded(child: _inputTextField(textPadding)),
|
||||||
|
|
||||||
|
// TODO(lucas): support mobile
|
||||||
|
if (PlatformExtension.isDesktop) _atButton(buttonPadding),
|
||||||
_sendButton(buttonPadding),
|
_sendButton(buttonPadding),
|
||||||
|
const HSpace(14),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -118,7 +129,7 @@ class _ChatInputState extends State<ChatInput> {
|
|||||||
|
|
||||||
void _handleSendButtonVisibilityModeChange() {
|
void _handleSendButtonVisibilityModeChange() {
|
||||||
_textController.removeListener(_handleTextControllerChange);
|
_textController.removeListener(_handleTextControllerChange);
|
||||||
_sendButtonVisible =
|
_sendButtonEnabled =
|
||||||
_textController.text.trim() != '' || widget.isStreaming;
|
_textController.text.trim() != '' || widget.isStreaming;
|
||||||
_textController.addListener(_handleTextControllerChange);
|
_textController.addListener(_handleTextControllerChange);
|
||||||
}
|
}
|
||||||
@ -126,9 +137,11 @@ class _ChatInputState extends State<ChatInput> {
|
|||||||
void _handleSendPressed() {
|
void _handleSendPressed() {
|
||||||
final trimmedText = _textController.text.trim();
|
final trimmedText = _textController.text.trim();
|
||||||
if (trimmedText != '') {
|
if (trimmedText != '') {
|
||||||
final partialText = types.PartialText(text: trimmedText);
|
final partialText = types.PartialText(
|
||||||
|
text: trimmedText,
|
||||||
|
metadata: _inputActionControl.metaData,
|
||||||
|
);
|
||||||
widget.onSendPressed(partialText);
|
widget.onSendPressed(partialText);
|
||||||
|
|
||||||
_textController.clear();
|
_textController.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -138,7 +151,7 @@ class _ChatInputState extends State<ChatInput> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setState(() {
|
setState(() {
|
||||||
_sendButtonVisible = _textController.text.trim() != '';
|
_sendButtonEnabled = _textController.text.trim() != '';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -147,8 +160,10 @@ class _ChatInputState extends State<ChatInput> {
|
|||||||
link: _layerLink,
|
link: _layerLink,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: textPadding,
|
padding: textPadding,
|
||||||
child: TextField(
|
child: ExtendedTextField(
|
||||||
key: _textFieldKey,
|
key: _textFieldKey,
|
||||||
|
specialTextSpanBuilder:
|
||||||
|
ChatInputTextSpanBuilder(inputActionControl: _inputActionControl),
|
||||||
controller: _textController,
|
controller: _textController,
|
||||||
focusNode: _inputFocusNode,
|
focusNode: _inputFocusNode,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
@ -160,49 +175,49 @@ class _ChatInputState extends State<ChatInput> {
|
|||||||
),
|
),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: AFThemeExtension.of(context).textColor,
|
color: AFThemeExtension.of(context).textColor,
|
||||||
|
fontSize: 15,
|
||||||
),
|
),
|
||||||
keyboardType: TextInputType.multiline,
|
keyboardType: TextInputType.multiline,
|
||||||
textCapitalization: TextCapitalization.sentences,
|
textCapitalization: TextCapitalization.sentences,
|
||||||
maxLines: 10,
|
|
||||||
minLines: 1,
|
minLines: 1,
|
||||||
// onChanged: (text) {
|
maxLines: 10,
|
||||||
// final handler = _textFieldInterceptor.onTextChanged(
|
onChanged: (text) {
|
||||||
// text,
|
_handleOnTextChange(context, text);
|
||||||
// _textController,
|
},
|
||||||
// _inputFocusNode,
|
|
||||||
// );
|
|
||||||
// // If the handler is not null, it means that the text has been
|
|
||||||
// // recognized as a command.
|
|
||||||
// if (handler != null) {
|
|
||||||
// ChatActionsMenu(
|
|
||||||
// anchor: ChatInputAnchor(
|
|
||||||
// anchorKey: _textFieldKey,
|
|
||||||
// layerLink: _layerLink,
|
|
||||||
// ),
|
|
||||||
// handler: handler,
|
|
||||||
// context: context,
|
|
||||||
// style: Theme.of(context).brightness == Brightness.dark
|
|
||||||
// ? const ChatActionsMenuStyle.dark()
|
|
||||||
// : const ChatActionsMenuStyle.light(),
|
|
||||||
// ).show();
|
|
||||||
// }
|
|
||||||
// },
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
ConstrainedBox _sendButton(EdgeInsets buttonPadding) {
|
void _handleOnTextChange(BuildContext context, String text) {
|
||||||
return ConstrainedBox(
|
if (PlatformExtension.isDesktop) {
|
||||||
constraints: BoxConstraints(
|
if (_inputActionControl.onTextChanged(text)) {
|
||||||
minHeight: buttonPadding.bottom + buttonPadding.top + 24,
|
ChatActionsMenu(
|
||||||
|
anchor: ChatInputAnchor(
|
||||||
|
anchorKey: _textFieldKey,
|
||||||
|
layerLink: _layerLink,
|
||||||
),
|
),
|
||||||
child: Visibility(
|
handler: _inputActionControl,
|
||||||
visible: _sendButtonVisible,
|
context: context,
|
||||||
child: Padding(
|
style: Theme.of(context).brightness == Brightness.dark
|
||||||
|
? const ChatActionsMenuStyle.dark()
|
||||||
|
: const ChatActionsMenuStyle.light(),
|
||||||
|
).show();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// TODO(lucas): support mobile
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _sendButton(EdgeInsets buttonPadding) {
|
||||||
|
return Padding(
|
||||||
padding: buttonPadding,
|
padding: buttonPadding,
|
||||||
child: ChatInputAccessoryButton(
|
child: ChatInputSendButton(
|
||||||
onSendPressed: () {
|
onSendPressed: () {
|
||||||
|
if (!_sendButtonEnabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!widget.isStreaming) {
|
if (!widget.isStreaming) {
|
||||||
widget.onStopStreaming();
|
widget.onStopStreaming();
|
||||||
_handleSendPressed();
|
_handleSendPressed();
|
||||||
@ -210,8 +225,20 @@ class _ChatInputState extends State<ChatInput> {
|
|||||||
},
|
},
|
||||||
onStopStreaming: () => widget.onStopStreaming(),
|
onStopStreaming: () => widget.onStopStreaming(),
|
||||||
isStreaming: widget.isStreaming,
|
isStreaming: widget.isStreaming,
|
||||||
|
enabled: _sendButtonEnabled,
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _atButton(EdgeInsets buttonPadding) {
|
||||||
|
return Padding(
|
||||||
|
padding: buttonPadding,
|
||||||
|
child: ChatInputAtButton(
|
||||||
|
onTap: () {
|
||||||
|
_textController.text += '@';
|
||||||
|
_inputFocusNode.requestFocus();
|
||||||
|
_handleOnTextChange(context, _textController.text);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -238,3 +265,38 @@ class ChatInputAnchor extends ChatAnchor {
|
|||||||
@override
|
@override
|
||||||
final LayerLink layerLink;
|
final LayerLink layerLink;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Handles the key press event for the Enter key without Shift.
|
||||||
|
///
|
||||||
|
/// This function checks if the Enter key is pressed without either of the Shift keys.
|
||||||
|
/// If the conditions are met, it performs the action of sending a message if the
|
||||||
|
/// text controller is not in a composing range and if the event is a key down event.
|
||||||
|
///
|
||||||
|
/// - Returns: A `KeyEventResult` indicating whether the key event was handled or ignored.
|
||||||
|
KeyEventResult _handleEnterKeyWithoutShift(
|
||||||
|
KeyEvent event,
|
||||||
|
TextEditingController textController,
|
||||||
|
bool isStreaming,
|
||||||
|
void Function() handleSendPressed,
|
||||||
|
) {
|
||||||
|
if (event.physicalKey == PhysicalKeyboardKey.enter &&
|
||||||
|
!HardwareKeyboard.instance.physicalKeysPressed.any(
|
||||||
|
(el) => <PhysicalKeyboardKey>{
|
||||||
|
PhysicalKeyboardKey.shiftLeft,
|
||||||
|
PhysicalKeyboardKey.shiftRight,
|
||||||
|
}.contains(el),
|
||||||
|
)) {
|
||||||
|
if (textController.value.isComposingRangeValid) {
|
||||||
|
return KeyEventResult.ignored;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event is KeyDownEvent) {
|
||||||
|
if (!isStreaming) {
|
||||||
|
handleSendPressed();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return KeyEventResult.handled;
|
||||||
|
} else {
|
||||||
|
return KeyEventResult.ignored;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -0,0 +1,74 @@
|
|||||||
|
import 'package:extended_text_library/extended_text_library.dart';
|
||||||
|
import 'package:flutter/gestures.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../../application/chat_input_action_control.dart';
|
||||||
|
|
||||||
|
class ChatInputTextSpanBuilder extends SpecialTextSpanBuilder {
|
||||||
|
ChatInputTextSpanBuilder({
|
||||||
|
required this.inputActionControl,
|
||||||
|
});
|
||||||
|
|
||||||
|
final ChatInputActionControl inputActionControl;
|
||||||
|
|
||||||
|
@override
|
||||||
|
SpecialText? createSpecialText(
|
||||||
|
String flag, {
|
||||||
|
TextStyle? textStyle,
|
||||||
|
SpecialTextGestureTapCallback? onTap,
|
||||||
|
int? index,
|
||||||
|
}) {
|
||||||
|
if (flag == '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
//index is end index of start flag, so text start index should be index-(flag.length-1)
|
||||||
|
if (isStart(flag, AtText.flag)) {
|
||||||
|
return AtText(
|
||||||
|
inputActionControl,
|
||||||
|
textStyle,
|
||||||
|
onTap,
|
||||||
|
start: index! - (AtText.flag.length - 1),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AtText extends SpecialText {
|
||||||
|
AtText(
|
||||||
|
this.inputActionControl,
|
||||||
|
TextStyle? textStyle,
|
||||||
|
SpecialTextGestureTapCallback? onTap, {
|
||||||
|
this.start,
|
||||||
|
}) : super(flag, '', textStyle, onTap: onTap);
|
||||||
|
static const String flag = '@';
|
||||||
|
final int? start;
|
||||||
|
final ChatInputActionControl inputActionControl;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool isEnd(String value) {
|
||||||
|
return inputActionControl.tags.contains(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
InlineSpan finishText() {
|
||||||
|
final TextStyle? textStyle =
|
||||||
|
this.textStyle?.copyWith(color: Colors.blue, fontSize: 15.0);
|
||||||
|
|
||||||
|
final String atText = toString();
|
||||||
|
|
||||||
|
return SpecialTextSpan(
|
||||||
|
text: atText,
|
||||||
|
actualText: atText,
|
||||||
|
start: start!,
|
||||||
|
style: textStyle,
|
||||||
|
recognizer: (TapGestureRecognizer()
|
||||||
|
..onTap = () {
|
||||||
|
if (onTap != null) {
|
||||||
|
onTap!(atText);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -3,26 +3,27 @@ import 'package:flowy_infra/theme_extension.dart';
|
|||||||
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
|
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class ChatInputAccessoryButton extends StatelessWidget {
|
class ChatInputSendButton extends StatelessWidget {
|
||||||
const ChatInputAccessoryButton({
|
const ChatInputSendButton({
|
||||||
required this.onSendPressed,
|
required this.onSendPressed,
|
||||||
required this.onStopStreaming,
|
required this.onStopStreaming,
|
||||||
required this.isStreaming,
|
required this.isStreaming,
|
||||||
|
required this.enabled,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
final void Function() onSendPressed;
|
final void Function() onSendPressed;
|
||||||
final void Function() onStopStreaming;
|
final void Function() onStopStreaming;
|
||||||
final bool isStreaming;
|
final bool isStreaming;
|
||||||
|
final bool enabled;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (isStreaming) {
|
if (isStreaming) {
|
||||||
return FlowyIconButton(
|
return FlowyIconButton(
|
||||||
width: 36,
|
|
||||||
icon: FlowySvg(
|
icon: FlowySvg(
|
||||||
FlowySvgs.ai_stream_stop_s,
|
FlowySvgs.ai_stream_stop_s,
|
||||||
size: const Size.square(28),
|
size: const Size.square(20),
|
||||||
color: Theme.of(context).colorScheme.primary,
|
color: Theme.of(context).colorScheme.primary,
|
||||||
),
|
),
|
||||||
onPressed: onStopStreaming,
|
onPressed: onStopStreaming,
|
||||||
@ -32,14 +33,13 @@ class ChatInputAccessoryButton extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return FlowyIconButton(
|
return FlowyIconButton(
|
||||||
width: 36,
|
|
||||||
fillColor: AFThemeExtension.of(context).lightGreyHover,
|
fillColor: AFThemeExtension.of(context).lightGreyHover,
|
||||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||||
radius: BorderRadius.circular(18),
|
radius: BorderRadius.circular(18),
|
||||||
icon: FlowySvg(
|
icon: FlowySvg(
|
||||||
FlowySvgs.send_s,
|
FlowySvgs.send_s,
|
||||||
size: const Size.square(24),
|
size: const Size.square(20),
|
||||||
color: Theme.of(context).colorScheme.primary,
|
color: enabled ? Theme.of(context).colorScheme.primary : null,
|
||||||
),
|
),
|
||||||
onPressed: onSendPressed,
|
onPressed: onSendPressed,
|
||||||
);
|
);
|
@ -0,0 +1,299 @@
|
|||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:appflowy/generated/locale_keys.g.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:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flowy_infra_ui/style_widget/button.dart';
|
||||||
|
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:scroll_to_index/scroll_to_index.dart';
|
||||||
|
|
||||||
|
abstract class ChatActionHandler {
|
||||||
|
void onEnter();
|
||||||
|
void onSelected(ChatInputActionPage page);
|
||||||
|
void onExit();
|
||||||
|
ChatInputActionBloc get commandBloc;
|
||||||
|
void onFilter(String filter);
|
||||||
|
double actionMenuOffsetX();
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class ChatAnchor {
|
||||||
|
GlobalKey get anchorKey;
|
||||||
|
LayerLink get layerLink;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int _itemHeight = 34;
|
||||||
|
const int _itemVerticalPadding = 4;
|
||||||
|
const int _noPageHeight = 20;
|
||||||
|
|
||||||
|
class ChatActionsMenu {
|
||||||
|
ChatActionsMenu({
|
||||||
|
required this.anchor,
|
||||||
|
required this.context,
|
||||||
|
required this.handler,
|
||||||
|
required this.style,
|
||||||
|
});
|
||||||
|
|
||||||
|
final BuildContext context;
|
||||||
|
final ChatAnchor anchor;
|
||||||
|
final ChatActionsMenuStyle style;
|
||||||
|
final ChatActionHandler handler;
|
||||||
|
|
||||||
|
OverlayEntry? _overlayEntry;
|
||||||
|
|
||||||
|
void dismiss() {
|
||||||
|
_overlayEntry?.remove();
|
||||||
|
_overlayEntry = null;
|
||||||
|
handler.onExit();
|
||||||
|
}
|
||||||
|
|
||||||
|
void show() {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) => _show());
|
||||||
|
}
|
||||||
|
|
||||||
|
void _show() {
|
||||||
|
if (_overlayEntry != null) {
|
||||||
|
dismiss();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (anchor.anchorKey.currentContext == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
handler.onEnter();
|
||||||
|
const double maxHeight = 300;
|
||||||
|
|
||||||
|
_overlayEntry = OverlayEntry(
|
||||||
|
builder: (context) => BlocProvider.value(
|
||||||
|
value: handler.commandBloc,
|
||||||
|
child: BlocBuilder<ChatInputActionBloc, ChatInputActionState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
final height = min(
|
||||||
|
max(
|
||||||
|
state.pages.length * (_itemHeight + _itemVerticalPadding),
|
||||||
|
_noPageHeight,
|
||||||
|
),
|
||||||
|
maxHeight,
|
||||||
|
);
|
||||||
|
|
||||||
|
return Stack(
|
||||||
|
children: [
|
||||||
|
CompositedTransformFollower(
|
||||||
|
link: anchor.layerLink,
|
||||||
|
showWhenUnlinked: false,
|
||||||
|
offset: Offset(handler.actionMenuOffsetX(), -height - 4),
|
||||||
|
child: Material(
|
||||||
|
elevation: 4.0,
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(
|
||||||
|
minWidth: 200,
|
||||||
|
maxWidth: 200,
|
||||||
|
maxHeight: maxHeight,
|
||||||
|
),
|
||||||
|
child: DecoratedBox(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.surfaceContainerHighest,
|
||||||
|
borderRadius: BorderRadius.circular(6.0),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 2,
|
||||||
|
vertical: 2,
|
||||||
|
),
|
||||||
|
child: ActionList(
|
||||||
|
handler: handler,
|
||||||
|
onDismiss: () => dismiss(),
|
||||||
|
pages: state.pages,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
Overlay.of(context).insert(_overlayEntry!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ActionItem extends StatelessWidget {
|
||||||
|
const _ActionItem({
|
||||||
|
required this.item,
|
||||||
|
required this.onTap,
|
||||||
|
required this.isSelected,
|
||||||
|
});
|
||||||
|
|
||||||
|
final ChatInputActionPage item;
|
||||||
|
final VoidCallback? onTap;
|
||||||
|
final bool isSelected;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
height: _itemHeight.toDouble(),
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: _itemVerticalPadding / 2.0),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: isSelected
|
||||||
|
? Theme.of(context).colorScheme.primary.withOpacity(0.1)
|
||||||
|
: Colors.transparent,
|
||||||
|
borderRadius: BorderRadius.circular(4.0),
|
||||||
|
),
|
||||||
|
child: FlowyButton(
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 6),
|
||||||
|
iconPadding: 10.0,
|
||||||
|
text: FlowyText.regular(
|
||||||
|
item.title,
|
||||||
|
),
|
||||||
|
onTap: onTap,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ActionList extends StatefulWidget {
|
||||||
|
const ActionList({
|
||||||
|
super.key,
|
||||||
|
required this.handler,
|
||||||
|
required this.onDismiss,
|
||||||
|
required this.pages,
|
||||||
|
});
|
||||||
|
|
||||||
|
final ChatActionHandler handler;
|
||||||
|
final VoidCallback? onDismiss;
|
||||||
|
final List<ChatInputActionPage> pages;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ActionList> createState() => _ActionListState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ActionListState extends State<ActionList> {
|
||||||
|
int _selectedIndex = 0;
|
||||||
|
final _scrollController = AutoScrollController();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_scrollController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
KeyEventResult _handleKeyPress(logicalKey) {
|
||||||
|
bool isHandle = false;
|
||||||
|
setState(() {
|
||||||
|
if (logicalKey == PhysicalKeyboardKey.arrowDown) {
|
||||||
|
_selectedIndex = (_selectedIndex + 1) % widget.pages.length;
|
||||||
|
_scrollToSelectedIndex();
|
||||||
|
isHandle = true;
|
||||||
|
} else if (logicalKey == PhysicalKeyboardKey.arrowUp) {
|
||||||
|
_selectedIndex =
|
||||||
|
(_selectedIndex - 1 + widget.pages.length) % widget.pages.length;
|
||||||
|
_scrollToSelectedIndex();
|
||||||
|
isHandle = true;
|
||||||
|
} else if (logicalKey == PhysicalKeyboardKey.enter) {
|
||||||
|
widget.handler.onSelected(widget.pages[_selectedIndex]);
|
||||||
|
widget.onDismiss?.call();
|
||||||
|
isHandle = true;
|
||||||
|
} else if (logicalKey == PhysicalKeyboardKey.escape) {
|
||||||
|
widget.onDismiss?.call();
|
||||||
|
isHandle = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return isHandle ? KeyEventResult.handled : KeyEventResult.ignored;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocListener<ChatInputActionBloc, ChatInputActionState>(
|
||||||
|
listenWhen: (previous, current) =>
|
||||||
|
previous.keyboardKey != current.keyboardKey,
|
||||||
|
listener: (context, state) {
|
||||||
|
if (state.keyboardKey != null) {
|
||||||
|
_handleKeyPress(state.keyboardKey!.physicalKey);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: ListView(
|
||||||
|
shrinkWrap: true,
|
||||||
|
controller: _scrollController,
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
children: _buildPages(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Widget> _buildPages() {
|
||||||
|
if (widget.pages.isEmpty) {
|
||||||
|
return [
|
||||||
|
SizedBox(
|
||||||
|
height: _noPageHeight.toDouble(),
|
||||||
|
child:
|
||||||
|
Center(child: FlowyText(LocaleKeys.chat_inputActionNoPages.tr())),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return widget.pages.asMap().entries.map((entry) {
|
||||||
|
final index = entry.key;
|
||||||
|
final ChatInputActionPage item = entry.value;
|
||||||
|
return AutoScrollTag(
|
||||||
|
key: ValueKey(item.pageId),
|
||||||
|
index: index,
|
||||||
|
controller: _scrollController,
|
||||||
|
child: _ActionItem(
|
||||||
|
item: item,
|
||||||
|
onTap: () {
|
||||||
|
widget.handler.onSelected(item);
|
||||||
|
widget.onDismiss?.call();
|
||||||
|
},
|
||||||
|
isSelected: _selectedIndex == index,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _scrollToSelectedIndex() {
|
||||||
|
_scrollController.scrollToIndex(
|
||||||
|
_selectedIndex,
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
preferPosition: AutoScrollPosition.begin,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ChatActionsMenuStyle {
|
||||||
|
ChatActionsMenuStyle({
|
||||||
|
required this.backgroundColor,
|
||||||
|
required this.groupTextColor,
|
||||||
|
required this.menuItemTextColor,
|
||||||
|
required this.menuItemSelectedColor,
|
||||||
|
required this.menuItemSelectedTextColor,
|
||||||
|
});
|
||||||
|
|
||||||
|
const ChatActionsMenuStyle.light()
|
||||||
|
: backgroundColor = Colors.white,
|
||||||
|
groupTextColor = const Color(0xFF555555),
|
||||||
|
menuItemTextColor = const Color(0xFF333333),
|
||||||
|
menuItemSelectedColor = const Color(0xFFE0F8FF),
|
||||||
|
menuItemSelectedTextColor = const Color.fromARGB(255, 56, 91, 247);
|
||||||
|
|
||||||
|
const ChatActionsMenuStyle.dark()
|
||||||
|
: backgroundColor = const Color(0xFF282E3A),
|
||||||
|
groupTextColor = const Color(0xFFBBC3CD),
|
||||||
|
menuItemTextColor = const Color(0xFFBBC3CD),
|
||||||
|
menuItemSelectedColor = const Color(0xFF00BCF0),
|
||||||
|
menuItemSelectedTextColor = const Color(0xFF131720);
|
||||||
|
|
||||||
|
final Color backgroundColor;
|
||||||
|
final Color groupTextColor;
|
||||||
|
final Color menuItemTextColor;
|
||||||
|
final Color menuItemSelectedColor;
|
||||||
|
final Color menuItemSelectedTextColor;
|
||||||
|
}
|
@ -0,0 +1,53 @@
|
|||||||
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||||
|
import 'package:appflowy/plugins/ai_chat/application/chat_side_pannel_bloc.dart';
|
||||||
|
import 'package:appflowy/startup/plugin/plugin.dart';
|
||||||
|
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||||
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
|
class ChatSidePannel extends StatelessWidget {
|
||||||
|
const ChatSidePannel({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocBuilder<ChatSidePannelBloc, ChatSidePannelState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
return state.indicator.when(
|
||||||
|
loading: () {
|
||||||
|
return const CircularProgressIndicator.adaptive();
|
||||||
|
},
|
||||||
|
ready: (view) {
|
||||||
|
final plugin = view.plugin();
|
||||||
|
plugin.init();
|
||||||
|
|
||||||
|
final pluginContext = PluginContext();
|
||||||
|
final child = plugin.widgetBuilder
|
||||||
|
.buildWidget(context: pluginContext, shrinkWrap: false);
|
||||||
|
return Container(
|
||||||
|
color: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: FlowyIconButton(
|
||||||
|
icon: const FlowySvg(FlowySvgs.show_menu_s),
|
||||||
|
onPressed: () {
|
||||||
|
context
|
||||||
|
.read<ChatSidePannelBloc>()
|
||||||
|
.add(const ChatSidePannelEvent.close());
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const VSpace(6),
|
||||||
|
Expanded(child: child),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,62 @@
|
|||||||
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
|
import 'package:appflowy/plugins/ai_chat/application/chat_bloc.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flowy_infra_ui/style_widget/button.dart';
|
||||||
|
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||||
|
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class AIMessageMetadata extends StatelessWidget {
|
||||||
|
const AIMessageMetadata({
|
||||||
|
required this.metadata,
|
||||||
|
required this.onSelectedMetadata,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final List<ChatMessageMetadata> metadata;
|
||||||
|
final Function(ChatMessageMetadata metadata) onSelectedMetadata;
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final title = metadata.length == 1
|
||||||
|
? LocaleKeys.chat_referenceSource.tr(args: [metadata.length.toString()])
|
||||||
|
: LocaleKeys.chat_referenceSources
|
||||||
|
.tr(args: [metadata.length.toString()]);
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
if (metadata.isNotEmpty)
|
||||||
|
Opacity(
|
||||||
|
opacity: 0.5,
|
||||||
|
child: FlowyText(title, fontSize: 12),
|
||||||
|
),
|
||||||
|
const VSpace(6),
|
||||||
|
Wrap(
|
||||||
|
spacing: 8.0,
|
||||||
|
runSpacing: 4.0,
|
||||||
|
children: metadata
|
||||||
|
.map(
|
||||||
|
(m) => SizedBox(
|
||||||
|
height: 24,
|
||||||
|
child: FlowyButton(
|
||||||
|
margin: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 6,
|
||||||
|
vertical: 4,
|
||||||
|
),
|
||||||
|
useIntrinsicWidth: true,
|
||||||
|
radius: BorderRadius.circular(6),
|
||||||
|
text: FlowyText(
|
||||||
|
m.source,
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
onSelectedMetadata(m);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,6 @@
|
|||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy/plugins/ai_chat/application/chat_ai_message_bloc.dart';
|
import 'package:appflowy/plugins/ai_chat/application/chat_ai_message_bloc.dart';
|
||||||
|
import 'package:appflowy/plugins/ai_chat/application/chat_bloc.dart';
|
||||||
import 'package:appflowy/plugins/ai_chat/presentation/chat_loading.dart';
|
import 'package:appflowy/plugins/ai_chat/presentation/chat_loading.dart';
|
||||||
import 'package:appflowy/plugins/ai_chat/presentation/message/ai_markdown_text.dart';
|
import 'package:appflowy/plugins/ai_chat/presentation/message/ai_markdown_text.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
@ -11,6 +12,8 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter_chat_types/flutter_chat_types.dart';
|
import 'package:flutter_chat_types/flutter_chat_types.dart';
|
||||||
|
|
||||||
|
import 'ai_metadata.dart';
|
||||||
|
|
||||||
class ChatAITextMessageWidget extends StatelessWidget {
|
class ChatAITextMessageWidget extends StatelessWidget {
|
||||||
const ChatAITextMessageWidget({
|
const ChatAITextMessageWidget({
|
||||||
super.key,
|
super.key,
|
||||||
@ -19,6 +22,8 @@ class ChatAITextMessageWidget extends StatelessWidget {
|
|||||||
required this.text,
|
required this.text,
|
||||||
required this.questionId,
|
required this.questionId,
|
||||||
required this.chatId,
|
required this.chatId,
|
||||||
|
required this.metadata,
|
||||||
|
required this.onSelectedMetadata,
|
||||||
});
|
});
|
||||||
|
|
||||||
final User user;
|
final User user;
|
||||||
@ -26,12 +31,15 @@ class ChatAITextMessageWidget extends StatelessWidget {
|
|||||||
final dynamic text;
|
final dynamic text;
|
||||||
final Int64? questionId;
|
final Int64? questionId;
|
||||||
final String chatId;
|
final String chatId;
|
||||||
|
final String? metadata;
|
||||||
|
final void Function(ChatMessageMetadata metadata) onSelectedMetadata;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocProvider(
|
return BlocProvider(
|
||||||
create: (context) => ChatAIMessageBloc(
|
create: (context) => ChatAIMessageBloc(
|
||||||
message: text,
|
message: text,
|
||||||
|
metadata: metadata,
|
||||||
chatId: chatId,
|
chatId: chatId,
|
||||||
questionId: questionId,
|
questionId: questionId,
|
||||||
)..add(const ChatAIMessageEvent.initial()),
|
)..add(const ChatAIMessageEvent.initial()),
|
||||||
@ -58,7 +66,16 @@ class ChatAITextMessageWidget extends StatelessWidget {
|
|||||||
if (state.text.isEmpty) {
|
if (state.text.isEmpty) {
|
||||||
return const ChatAILoading();
|
return const ChatAILoading();
|
||||||
} else {
|
} else {
|
||||||
return AIMarkdownText(markdown: state.text);
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
AIMarkdownText(markdown: state.text),
|
||||||
|
AIMessageMetadata(
|
||||||
|
metadata: state.metadata,
|
||||||
|
onSelectedMetadata: onSelectedMetadata,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
loading: () {
|
loading: () {
|
||||||
|
@ -0,0 +1,57 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
|
extension PlatformExtension on Platform {
|
||||||
|
/// Returns true if the operating system is macOS and not running on Web platform.
|
||||||
|
static bool get isMacOS {
|
||||||
|
if (kIsWeb) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return Platform.isMacOS;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if the operating system is Windows and not running on Web platform.
|
||||||
|
static bool get isWindows {
|
||||||
|
if (kIsWeb) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return Platform.isWindows;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if the operating system is Linux and not running on Web platform.
|
||||||
|
static bool get isLinux {
|
||||||
|
if (kIsWeb) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return Platform.isLinux;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool get isDesktopOrWeb {
|
||||||
|
if (kIsWeb) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return isDesktop;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool get isDesktop {
|
||||||
|
if (kIsWeb) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return Platform.isWindows || Platform.isLinux || Platform.isMacOS;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool get isMobile {
|
||||||
|
if (kIsWeb) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return Platform.isAndroid || Platform.isIOS;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool get isNotMobile {
|
||||||
|
if (kIsWeb) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return !isMobile;
|
||||||
|
}
|
||||||
|
}
|
@ -538,6 +538,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.0.1"
|
version: "5.0.1"
|
||||||
|
extended_text_field:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: extended_text_field
|
||||||
|
sha256: "954c7eea1e82728a742f7ddf09b9a51cef087d4f52b716ba88cb3eb78ccd7c6e"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "15.0.0"
|
||||||
|
extended_text_library:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: extended_text_library
|
||||||
|
sha256: "55d09098ec56fab0d9a8a68950ca0bbf2efa1327937f7cec6af6dfa066234829"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "12.0.0"
|
||||||
fake_async:
|
fake_async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1755,7 +1771,7 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "0.1.9"
|
version: "0.1.9"
|
||||||
scroll_to_index:
|
scroll_to_index:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: scroll_to_index
|
name: scroll_to_index
|
||||||
sha256: b707546e7500d9f070d63e5acf74fd437ec7eeeb68d3412ef7b0afada0b4f176
|
sha256: b707546e7500d9f070d63e5acf74fd437ec7eeeb68d3412ef7b0afada0b4f176
|
||||||
|
@ -160,6 +160,9 @@ dependencies:
|
|||||||
flutter_highlight: ^0.7.0
|
flutter_highlight: ^0.7.0
|
||||||
custom_sliding_segmented_control: ^1.8.3
|
custom_sliding_segmented_control: ^1.8.3
|
||||||
toastification: ^2.0.0
|
toastification: ^2.0.0
|
||||||
|
scroll_to_index: ^3.0.1
|
||||||
|
extended_text_field: ^15.0.0
|
||||||
|
extended_text_library: ^12.0.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_lints: ^3.0.1
|
flutter_lints: ^3.0.1
|
||||||
|
28
frontend/appflowy_tauri/src-tauri/Cargo.lock
generated
28
frontend/appflowy_tauri/src-tauri/Cargo.lock
generated
@ -172,7 +172,7 @@ checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "app-error"
|
name = "app-error"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=8f61e6d03469aac73b9ad88e37ac6898a691289d#8f61e6d03469aac73b9ad88e37ac6898a691289d"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bincode",
|
"bincode",
|
||||||
@ -192,11 +192,12 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "appflowy-ai-client"
|
name = "appflowy-ai-client"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=8f61e6d03469aac73b9ad88e37ac6898a691289d#8f61e6d03469aac73b9ad88e37ac6898a691289d"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures",
|
"futures",
|
||||||
|
"pin-project",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_repr",
|
"serde_repr",
|
||||||
@ -825,7 +826,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "client-api"
|
name = "client-api"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=8f61e6d03469aac73b9ad88e37ac6898a691289d#8f61e6d03469aac73b9ad88e37ac6898a691289d"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"again",
|
"again",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
@ -875,7 +876,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "client-api-entity"
|
name = "client-api-entity"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=8f61e6d03469aac73b9ad88e37ac6898a691289d#8f61e6d03469aac73b9ad88e37ac6898a691289d"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"collab-entity",
|
"collab-entity",
|
||||||
"collab-rt-entity",
|
"collab-rt-entity",
|
||||||
@ -887,7 +888,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "client-websocket"
|
name = "client-websocket"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=8f61e6d03469aac73b9ad88e37ac6898a691289d#8f61e6d03469aac73b9ad88e37ac6898a691289d"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
@ -1131,7 +1132,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collab-rt-entity"
|
name = "collab-rt-entity"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=8f61e6d03469aac73b9ad88e37ac6898a691289d#8f61e6d03469aac73b9ad88e37ac6898a691289d"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bincode",
|
"bincode",
|
||||||
@ -1156,7 +1157,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collab-rt-protocol"
|
name = "collab-rt-protocol"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=8f61e6d03469aac73b9ad88e37ac6898a691289d#8f61e6d03469aac73b9ad88e37ac6898a691289d"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
@ -1531,7 +1532,7 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "database-entity"
|
name = "database-entity"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=8f61e6d03469aac73b9ad88e37ac6898a691289d#8f61e6d03469aac73b9ad88e37ac6898a691289d"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"app-error",
|
"app-error",
|
||||||
@ -1974,6 +1975,7 @@ dependencies = [
|
|||||||
"md5",
|
"md5",
|
||||||
"notify",
|
"notify",
|
||||||
"parking_lot 0.12.1",
|
"parking_lot 0.12.1",
|
||||||
|
"pin-project",
|
||||||
"protobuf",
|
"protobuf",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"serde",
|
"serde",
|
||||||
@ -1999,6 +2001,7 @@ dependencies = [
|
|||||||
"flowy-error",
|
"flowy-error",
|
||||||
"futures",
|
"futures",
|
||||||
"lib-infra",
|
"lib-infra",
|
||||||
|
"serde_json",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -2053,6 +2056,7 @@ name = "flowy-core"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"appflowy-local-ai",
|
||||||
"base64 0.21.5",
|
"base64 0.21.5",
|
||||||
"bytes",
|
"bytes",
|
||||||
"client-api",
|
"client-api",
|
||||||
@ -3047,7 +3051,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "gotrue"
|
name = "gotrue"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=8f61e6d03469aac73b9ad88e37ac6898a691289d#8f61e6d03469aac73b9ad88e37ac6898a691289d"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
@ -3064,7 +3068,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "gotrue-entity"
|
name = "gotrue-entity"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=8f61e6d03469aac73b9ad88e37ac6898a691289d#8f61e6d03469aac73b9ad88e37ac6898a691289d"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"app-error",
|
"app-error",
|
||||||
@ -3496,7 +3500,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "infra"
|
name = "infra"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=8f61e6d03469aac73b9ad88e37ac6898a691289d#8f61e6d03469aac73b9ad88e37ac6898a691289d"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bytes",
|
"bytes",
|
||||||
@ -6094,7 +6098,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "shared-entity"
|
name = "shared-entity"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=8f61e6d03469aac73b9ad88e37ac6898a691289d#8f61e6d03469aac73b9ad88e37ac6898a691289d"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"app-error",
|
"app-error",
|
||||||
|
@ -53,7 +53,7 @@ collab-user = { version = "0.2" }
|
|||||||
# Run the script:
|
# Run the script:
|
||||||
# scripts/tool/update_client_api_rev.sh new_rev_id
|
# scripts/tool/update_client_api_rev.sh new_rev_id
|
||||||
# ⚠️⚠️⚠️️
|
# ⚠️⚠️⚠️️
|
||||||
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "8f61e6d03469aac73b9ad88e37ac6898a691289d" }
|
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "a371912c61d79fa946ec78f0cb852fdd7d391356" }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
|
28
frontend/appflowy_web_app/src-tauri/Cargo.lock
generated
28
frontend/appflowy_web_app/src-tauri/Cargo.lock
generated
@ -163,7 +163,7 @@ checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "app-error"
|
name = "app-error"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=8f61e6d03469aac73b9ad88e37ac6898a691289d#8f61e6d03469aac73b9ad88e37ac6898a691289d"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bincode",
|
"bincode",
|
||||||
@ -183,11 +183,12 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "appflowy-ai-client"
|
name = "appflowy-ai-client"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=8f61e6d03469aac73b9ad88e37ac6898a691289d#8f61e6d03469aac73b9ad88e37ac6898a691289d"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures",
|
"futures",
|
||||||
|
"pin-project",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_repr",
|
"serde_repr",
|
||||||
@ -799,7 +800,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "client-api"
|
name = "client-api"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=8f61e6d03469aac73b9ad88e37ac6898a691289d#8f61e6d03469aac73b9ad88e37ac6898a691289d"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"again",
|
"again",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
@ -849,7 +850,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "client-api-entity"
|
name = "client-api-entity"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=8f61e6d03469aac73b9ad88e37ac6898a691289d#8f61e6d03469aac73b9ad88e37ac6898a691289d"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"collab-entity",
|
"collab-entity",
|
||||||
"collab-rt-entity",
|
"collab-rt-entity",
|
||||||
@ -861,7 +862,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "client-websocket"
|
name = "client-websocket"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=8f61e6d03469aac73b9ad88e37ac6898a691289d#8f61e6d03469aac73b9ad88e37ac6898a691289d"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
@ -1114,7 +1115,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collab-rt-entity"
|
name = "collab-rt-entity"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=8f61e6d03469aac73b9ad88e37ac6898a691289d#8f61e6d03469aac73b9ad88e37ac6898a691289d"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bincode",
|
"bincode",
|
||||||
@ -1139,7 +1140,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collab-rt-protocol"
|
name = "collab-rt-protocol"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=8f61e6d03469aac73b9ad88e37ac6898a691289d#8f61e6d03469aac73b9ad88e37ac6898a691289d"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
@ -1521,7 +1522,7 @@ checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "database-entity"
|
name = "database-entity"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=8f61e6d03469aac73b9ad88e37ac6898a691289d#8f61e6d03469aac73b9ad88e37ac6898a691289d"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"app-error",
|
"app-error",
|
||||||
@ -2004,6 +2005,7 @@ dependencies = [
|
|||||||
"md5",
|
"md5",
|
||||||
"notify",
|
"notify",
|
||||||
"parking_lot 0.12.1",
|
"parking_lot 0.12.1",
|
||||||
|
"pin-project",
|
||||||
"protobuf",
|
"protobuf",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"serde",
|
"serde",
|
||||||
@ -2029,6 +2031,7 @@ dependencies = [
|
|||||||
"flowy-error",
|
"flowy-error",
|
||||||
"futures",
|
"futures",
|
||||||
"lib-infra",
|
"lib-infra",
|
||||||
|
"serde_json",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -2083,6 +2086,7 @@ name = "flowy-core"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"appflowy-local-ai",
|
||||||
"base64 0.21.7",
|
"base64 0.21.7",
|
||||||
"bytes",
|
"bytes",
|
||||||
"client-api",
|
"client-api",
|
||||||
@ -3114,7 +3118,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "gotrue"
|
name = "gotrue"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=8f61e6d03469aac73b9ad88e37ac6898a691289d#8f61e6d03469aac73b9ad88e37ac6898a691289d"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
@ -3131,7 +3135,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "gotrue-entity"
|
name = "gotrue-entity"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=8f61e6d03469aac73b9ad88e37ac6898a691289d#8f61e6d03469aac73b9ad88e37ac6898a691289d"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"app-error",
|
"app-error",
|
||||||
@ -3568,7 +3572,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "infra"
|
name = "infra"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=8f61e6d03469aac73b9ad88e37ac6898a691289d#8f61e6d03469aac73b9ad88e37ac6898a691289d"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bytes",
|
"bytes",
|
||||||
@ -6158,7 +6162,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "shared-entity"
|
name = "shared-entity"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=8f61e6d03469aac73b9ad88e37ac6898a691289d#8f61e6d03469aac73b9ad88e37ac6898a691289d"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"app-error",
|
"app-error",
|
||||||
|
@ -52,7 +52,7 @@ collab-user = { version = "0.2" }
|
|||||||
# Run the script:
|
# Run the script:
|
||||||
# scripts/tool/update_client_api_rev.sh new_rev_id
|
# scripts/tool/update_client_api_rev.sh new_rev_id
|
||||||
# ⚠️⚠️⚠️️
|
# ⚠️⚠️⚠️️
|
||||||
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "8f61e6d03469aac73b9ad88e37ac6898a691289d" }
|
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "a371912c61d79fa946ec78f0cb852fdd7d391356" }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
|
@ -173,6 +173,10 @@
|
|||||||
"aiMistakePrompt": "AI can make mistakes. Check important info.",
|
"aiMistakePrompt": "AI can make mistakes. Check important info.",
|
||||||
"chatWithFilePrompt": "Do you want to chat with the file?",
|
"chatWithFilePrompt": "Do you want to chat with the file?",
|
||||||
"indexFileSuccess": "Indexing file successfully",
|
"indexFileSuccess": "Indexing file successfully",
|
||||||
|
"inputActionNoPages": "No page results",
|
||||||
|
"referenceSource": "{} source found",
|
||||||
|
"referenceSources": "{} sources found",
|
||||||
|
"clickToMention": "Click to mention a page",
|
||||||
"indexingFile": "Indexing {}"
|
"indexingFile": "Indexing {}"
|
||||||
},
|
},
|
||||||
"trash": {
|
"trash": {
|
||||||
|
27
frontend/rust-lib/Cargo.lock
generated
27
frontend/rust-lib/Cargo.lock
generated
@ -163,7 +163,7 @@ checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "app-error"
|
name = "app-error"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=8f61e6d03469aac73b9ad88e37ac6898a691289d#8f61e6d03469aac73b9ad88e37ac6898a691289d"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bincode",
|
"bincode",
|
||||||
@ -183,11 +183,12 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "appflowy-ai-client"
|
name = "appflowy-ai-client"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=8f61e6d03469aac73b9ad88e37ac6898a691289d#8f61e6d03469aac73b9ad88e37ac6898a691289d"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures",
|
"futures",
|
||||||
|
"pin-project",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_repr",
|
"serde_repr",
|
||||||
@ -717,7 +718,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "client-api"
|
name = "client-api"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=8f61e6d03469aac73b9ad88e37ac6898a691289d#8f61e6d03469aac73b9ad88e37ac6898a691289d"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"again",
|
"again",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
@ -767,7 +768,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "client-api-entity"
|
name = "client-api-entity"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=8f61e6d03469aac73b9ad88e37ac6898a691289d#8f61e6d03469aac73b9ad88e37ac6898a691289d"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"collab-entity",
|
"collab-entity",
|
||||||
"collab-rt-entity",
|
"collab-rt-entity",
|
||||||
@ -779,7 +780,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "client-websocket"
|
name = "client-websocket"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=8f61e6d03469aac73b9ad88e37ac6898a691289d#8f61e6d03469aac73b9ad88e37ac6898a691289d"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
@ -992,7 +993,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collab-rt-entity"
|
name = "collab-rt-entity"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=8f61e6d03469aac73b9ad88e37ac6898a691289d#8f61e6d03469aac73b9ad88e37ac6898a691289d"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bincode",
|
"bincode",
|
||||||
@ -1017,7 +1018,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collab-rt-protocol"
|
name = "collab-rt-protocol"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=8f61e6d03469aac73b9ad88e37ac6898a691289d#8f61e6d03469aac73b9ad88e37ac6898a691289d"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
@ -1355,7 +1356,7 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "database-entity"
|
name = "database-entity"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=8f61e6d03469aac73b9ad88e37ac6898a691289d#8f61e6d03469aac73b9ad88e37ac6898a691289d"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"app-error",
|
"app-error",
|
||||||
@ -1798,6 +1799,7 @@ dependencies = [
|
|||||||
"md5",
|
"md5",
|
||||||
"notify",
|
"notify",
|
||||||
"parking_lot 0.12.1",
|
"parking_lot 0.12.1",
|
||||||
|
"pin-project",
|
||||||
"protobuf",
|
"protobuf",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"serde",
|
"serde",
|
||||||
@ -1825,6 +1827,7 @@ dependencies = [
|
|||||||
"flowy-error",
|
"flowy-error",
|
||||||
"futures",
|
"futures",
|
||||||
"lib-infra",
|
"lib-infra",
|
||||||
|
"serde_json",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -2727,7 +2730,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "gotrue"
|
name = "gotrue"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=8f61e6d03469aac73b9ad88e37ac6898a691289d#8f61e6d03469aac73b9ad88e37ac6898a691289d"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
@ -2744,7 +2747,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "gotrue-entity"
|
name = "gotrue-entity"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=8f61e6d03469aac73b9ad88e37ac6898a691289d#8f61e6d03469aac73b9ad88e37ac6898a691289d"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"app-error",
|
"app-error",
|
||||||
@ -3109,7 +3112,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "infra"
|
name = "infra"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=8f61e6d03469aac73b9ad88e37ac6898a691289d#8f61e6d03469aac73b9ad88e37ac6898a691289d"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bytes",
|
"bytes",
|
||||||
@ -5304,7 +5307,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "shared-entity"
|
name = "shared-entity"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=8f61e6d03469aac73b9ad88e37ac6898a691289d#8f61e6d03469aac73b9ad88e37ac6898a691289d"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"app-error",
|
"app-error",
|
||||||
|
@ -99,8 +99,8 @@ zip = "2.1.3"
|
|||||||
# Run the script.add_workspace_members:
|
# Run the script.add_workspace_members:
|
||||||
# scripts/tool/update_client_api_rev.sh new_rev_id
|
# scripts/tool/update_client_api_rev.sh new_rev_id
|
||||||
# ⚠️⚠️⚠️️
|
# ⚠️⚠️⚠️️
|
||||||
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "8f61e6d03469aac73b9ad88e37ac6898a691289d" }
|
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "a371912c61d79fa946ec78f0cb852fdd7d391356" }
|
||||||
client-api-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "8f61e6d03469aac73b9ad88e37ac6898a691289d" }
|
client-api-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "a371912c61d79fa946ec78f0cb852fdd7d391356" }
|
||||||
|
|
||||||
[profile.dev]
|
[profile.dev]
|
||||||
opt-level = 0
|
opt-level = 0
|
||||||
|
@ -42,7 +42,7 @@ impl DocumentEventTest {
|
|||||||
.event_test
|
.event_test
|
||||||
.appflowy_core
|
.appflowy_core
|
||||||
.document_manager
|
.document_manager
|
||||||
.get_document(doc_id)
|
.get_opened_document(doc_id)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let guard = doc.lock();
|
let guard = doc.lock();
|
||||||
|
@ -20,11 +20,12 @@ async fn af_cloud_create_chat_message_test() {
|
|||||||
let chat_service = test.server_provider.get_server().unwrap().chat_service();
|
let chat_service = test.server_provider.get_server().unwrap().chat_service();
|
||||||
for i in 0..10 {
|
for i in 0..10 {
|
||||||
let _ = chat_service
|
let _ = chat_service
|
||||||
.save_question(
|
.create_question(
|
||||||
¤t_workspace.id,
|
¤t_workspace.id,
|
||||||
&chat_id,
|
&chat_id,
|
||||||
&format!("hello world {}", i),
|
&format!("hello world {}", i),
|
||||||
ChatMessageType::System,
|
ChatMessageType::System,
|
||||||
|
vec![],
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@ -75,11 +76,12 @@ async fn af_cloud_load_remote_system_message_test() {
|
|||||||
let chat_service = test.server_provider.get_server().unwrap().chat_service();
|
let chat_service = test.server_provider.get_server().unwrap().chat_service();
|
||||||
for i in 0..10 {
|
for i in 0..10 {
|
||||||
let _ = chat_service
|
let _ = chat_service
|
||||||
.save_question(
|
.create_question(
|
||||||
¤t_workspace.id,
|
¤t_workspace.id,
|
||||||
&chat_id,
|
&chat_id,
|
||||||
&format!("hello server {}", i),
|
&format!("hello server {}", i),
|
||||||
ChatMessageType::System,
|
ChatMessageType::System,
|
||||||
|
vec![],
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
@ -11,3 +11,4 @@ flowy-error = { workspace = true }
|
|||||||
client-api = { workspace = true }
|
client-api = { workspace = true }
|
||||||
bytes.workspace = true
|
bytes.workspace = true
|
||||||
futures.workspace = true
|
futures.workspace = true
|
||||||
|
serde_json.workspace = true
|
@ -1,10 +1,11 @@
|
|||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
pub use client_api::entity::ai_dto::{
|
pub use client_api::entity::ai_dto::{
|
||||||
AppFlowyOfflineAI, CompletionType, LLMModel, LocalAIConfig, ModelInfo, RelatedQuestion,
|
AppFlowyOfflineAI, CompletionType, CreateTextChatContext, LLMModel, LocalAIConfig, ModelInfo,
|
||||||
RepeatedRelatedQuestion, StringOrMessage,
|
RelatedQuestion, RepeatedRelatedQuestion, StringOrMessage,
|
||||||
};
|
};
|
||||||
pub use client_api::entity::{
|
pub use client_api::entity::{
|
||||||
ChatAuthorType, ChatMessage, ChatMessageType, MessageCursor, QAChatMessage, RepeatedChatMessage,
|
ChatAuthorType, ChatMessage, ChatMessageMetadata, ChatMessageType, ChatMetadataData,
|
||||||
|
MessageCursor, QAChatMessage, QuestionStreamValue, RepeatedChatMessage,
|
||||||
};
|
};
|
||||||
use client_api::error::AppResponseError;
|
use client_api::error::AppResponseError;
|
||||||
use flowy_error::FlowyError;
|
use flowy_error::FlowyError;
|
||||||
@ -14,7 +15,7 @@ use lib_infra::future::FutureResult;
|
|||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
pub type ChatMessageStream = BoxStream<'static, Result<ChatMessage, AppResponseError>>;
|
pub type ChatMessageStream = BoxStream<'static, Result<ChatMessage, AppResponseError>>;
|
||||||
pub type StreamAnswer = BoxStream<'static, Result<Bytes, FlowyError>>;
|
pub type StreamAnswer = BoxStream<'static, Result<QuestionStreamValue, FlowyError>>;
|
||||||
pub type StreamComplete = BoxStream<'static, Result<Bytes, FlowyError>>;
|
pub type StreamComplete = BoxStream<'static, Result<Bytes, FlowyError>>;
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait ChatCloudService: Send + Sync + 'static {
|
pub trait ChatCloudService: Send + Sync + 'static {
|
||||||
@ -25,30 +26,32 @@ pub trait ChatCloudService: Send + Sync + 'static {
|
|||||||
chat_id: &str,
|
chat_id: &str,
|
||||||
) -> FutureResult<(), FlowyError>;
|
) -> FutureResult<(), FlowyError>;
|
||||||
|
|
||||||
fn save_question(
|
fn create_question(
|
||||||
&self,
|
&self,
|
||||||
workspace_id: &str,
|
workspace_id: &str,
|
||||||
chat_id: &str,
|
chat_id: &str,
|
||||||
message: &str,
|
message: &str,
|
||||||
message_type: ChatMessageType,
|
message_type: ChatMessageType,
|
||||||
|
metadata: Vec<ChatMessageMetadata>,
|
||||||
) -> FutureResult<ChatMessage, FlowyError>;
|
) -> FutureResult<ChatMessage, FlowyError>;
|
||||||
|
|
||||||
fn save_answer(
|
fn create_answer(
|
||||||
&self,
|
&self,
|
||||||
workspace_id: &str,
|
workspace_id: &str,
|
||||||
chat_id: &str,
|
chat_id: &str,
|
||||||
message: &str,
|
message: &str,
|
||||||
question_id: i64,
|
question_id: i64,
|
||||||
|
metadata: Option<serde_json::Value>,
|
||||||
) -> FutureResult<ChatMessage, FlowyError>;
|
) -> FutureResult<ChatMessage, FlowyError>;
|
||||||
|
|
||||||
async fn ask_question(
|
async fn stream_answer(
|
||||||
&self,
|
&self,
|
||||||
workspace_id: &str,
|
workspace_id: &str,
|
||||||
chat_id: &str,
|
chat_id: &str,
|
||||||
message_id: i64,
|
message_id: i64,
|
||||||
) -> Result<StreamAnswer, FlowyError>;
|
) -> Result<StreamAnswer, FlowyError>;
|
||||||
|
|
||||||
async fn generate_answer(
|
async fn get_answer(
|
||||||
&self,
|
&self,
|
||||||
workspace_id: &str,
|
workspace_id: &str,
|
||||||
chat_id: &str,
|
chat_id: &str,
|
||||||
@ -85,4 +88,12 @@ pub trait ChatCloudService: Send + Sync + 'static {
|
|||||||
) -> Result<(), FlowyError>;
|
) -> Result<(), FlowyError>;
|
||||||
|
|
||||||
async fn get_local_ai_config(&self, workspace_id: &str) -> Result<LocalAIConfig, FlowyError>;
|
async fn get_local_ai_config(&self, workspace_id: &str) -> Result<LocalAIConfig, FlowyError>;
|
||||||
|
|
||||||
|
async fn create_chat_context(
|
||||||
|
&self,
|
||||||
|
_workspace_id: &str,
|
||||||
|
_chat_context: CreateTextChatContext,
|
||||||
|
) -> Result<(), FlowyError> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -43,6 +43,7 @@ futures-util = "0.3.30"
|
|||||||
md5 = "0.7.0"
|
md5 = "0.7.0"
|
||||||
zip = { workspace = true, features = ["deflate"] }
|
zip = { workspace = true, features = ["deflate"] }
|
||||||
zip-extensions = "0.8.0"
|
zip-extensions = "0.8.0"
|
||||||
|
pin-project = "1.1.5"
|
||||||
|
|
||||||
[target.'cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))'.dependencies]
|
[target.'cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))'.dependencies]
|
||||||
notify = "6.1.1"
|
notify = "6.1.1"
|
||||||
|
@ -1,17 +1,22 @@
|
|||||||
use crate::chat::Chat;
|
use crate::chat::Chat;
|
||||||
use crate::entities::{ChatMessageListPB, ChatMessagePB, RepeatedRelatedQuestionPB};
|
use crate::entities::{
|
||||||
|
ChatMessageListPB, ChatMessagePB, CreateChatContextPB, RepeatedRelatedQuestionPB,
|
||||||
|
};
|
||||||
use crate::local_ai::local_llm_chat::LocalAIController;
|
use crate::local_ai::local_llm_chat::LocalAIController;
|
||||||
use crate::middleware::chat_service_mw::AICloudServiceMiddleware;
|
use crate::middleware::chat_service_mw::AICloudServiceMiddleware;
|
||||||
use crate::persistence::{insert_chat, ChatTable};
|
use crate::persistence::{insert_chat, ChatTable};
|
||||||
|
|
||||||
use appflowy_plugin::manager::PluginManager;
|
use appflowy_plugin::manager::PluginManager;
|
||||||
use dashmap::DashMap;
|
use dashmap::DashMap;
|
||||||
use flowy_ai_pub::cloud::{ChatCloudService, ChatMessageType};
|
use flowy_ai_pub::cloud::{
|
||||||
|
ChatCloudService, ChatMessageMetadata, ChatMessageType, CreateTextChatContext,
|
||||||
|
};
|
||||||
use flowy_error::{FlowyError, FlowyResult};
|
use flowy_error::{FlowyError, FlowyResult};
|
||||||
use flowy_sqlite::kv::KVStorePreferences;
|
use flowy_sqlite::kv::KVStorePreferences;
|
||||||
use flowy_sqlite::DBConnection;
|
use flowy_sqlite::DBConnection;
|
||||||
|
|
||||||
use lib_infra::util::timestamp;
|
use lib_infra::util::timestamp;
|
||||||
|
use serde_json::json;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tracing::{info, trace};
|
use tracing::{info, trace};
|
||||||
@ -101,6 +106,27 @@ impl AIManager {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn create_chat_context(&self, context: CreateChatContextPB) -> FlowyResult<()> {
|
||||||
|
let workspace_id = self.user_service.workspace_id()?;
|
||||||
|
let context = CreateTextChatContext {
|
||||||
|
chat_id: context.chat_id,
|
||||||
|
content_type: context.content_type,
|
||||||
|
text: context.text,
|
||||||
|
chunk_size: 2000,
|
||||||
|
chunk_overlap: 20,
|
||||||
|
metadata: context
|
||||||
|
.metadata
|
||||||
|
.into_iter()
|
||||||
|
.map(|(k, v)| (k, json!(v)))
|
||||||
|
.collect(),
|
||||||
|
};
|
||||||
|
self
|
||||||
|
.cloud_service_wm
|
||||||
|
.create_chat_context(&workspace_id, context)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn create_chat(&self, uid: &i64, chat_id: &str) -> Result<Arc<Chat>, FlowyError> {
|
pub async fn create_chat(&self, uid: &i64, chat_id: &str) -> Result<Arc<Chat>, FlowyError> {
|
||||||
let workspace_id = self.user_service.workspace_id()?;
|
let workspace_id = self.user_service.workspace_id()?;
|
||||||
self
|
self
|
||||||
@ -125,10 +151,11 @@ impl AIManager {
|
|||||||
message: &str,
|
message: &str,
|
||||||
message_type: ChatMessageType,
|
message_type: ChatMessageType,
|
||||||
text_stream_port: i64,
|
text_stream_port: i64,
|
||||||
|
metadata: Vec<ChatMessageMetadata>,
|
||||||
) -> Result<ChatMessagePB, FlowyError> {
|
) -> Result<ChatMessagePB, FlowyError> {
|
||||||
let chat = self.get_or_create_chat_instance(chat_id).await?;
|
let chat = self.get_or_create_chat_instance(chat_id).await?;
|
||||||
let question = chat
|
let question = chat
|
||||||
.stream_chat_message(message, message_type, text_stream_port)
|
.stream_chat_message(message, message_type, text_stream_port, metadata)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(question)
|
Ok(question)
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,10 @@ use crate::middleware::chat_service_mw::AICloudServiceMiddleware;
|
|||||||
use crate::notification::{make_notification, ChatNotification};
|
use crate::notification::{make_notification, ChatNotification};
|
||||||
use crate::persistence::{insert_chat_messages, select_chat_messages, ChatMessageTable};
|
use crate::persistence::{insert_chat_messages, select_chat_messages, ChatMessageTable};
|
||||||
use allo_isolate::Isolate;
|
use allo_isolate::Isolate;
|
||||||
use flowy_ai_pub::cloud::{ChatCloudService, ChatMessage, ChatMessageType, MessageCursor};
|
use flowy_ai_pub::cloud::{
|
||||||
|
ChatCloudService, ChatMessage, ChatMessageMetadata, ChatMessageType, MessageCursor,
|
||||||
|
QuestionStreamValue,
|
||||||
|
};
|
||||||
use flowy_error::{FlowyError, FlowyResult};
|
use flowy_error::{FlowyError, FlowyResult};
|
||||||
use flowy_sqlite::DBConnection;
|
use flowy_sqlite::DBConnection;
|
||||||
use futures::{SinkExt, StreamExt};
|
use futures::{SinkExt, StreamExt};
|
||||||
@ -31,7 +34,7 @@ pub struct Chat {
|
|||||||
prev_message_state: Arc<RwLock<PrevMessageState>>,
|
prev_message_state: Arc<RwLock<PrevMessageState>>,
|
||||||
latest_message_id: Arc<AtomicI64>,
|
latest_message_id: Arc<AtomicI64>,
|
||||||
stop_stream: Arc<AtomicBool>,
|
stop_stream: Arc<AtomicBool>,
|
||||||
steam_buffer: Arc<Mutex<String>>,
|
stream_buffer: Arc<Mutex<StringBuffer>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Chat {
|
impl Chat {
|
||||||
@ -49,7 +52,7 @@ impl Chat {
|
|||||||
prev_message_state: Arc::new(RwLock::new(PrevMessageState::HasMore)),
|
prev_message_state: Arc::new(RwLock::new(PrevMessageState::HasMore)),
|
||||||
latest_message_id: Default::default(),
|
latest_message_id: Default::default(),
|
||||||
stop_stream: Arc::new(AtomicBool::new(false)),
|
stop_stream: Arc::new(AtomicBool::new(false)),
|
||||||
steam_buffer: Arc::new(Mutex::new("".to_string())),
|
stream_buffer: Arc::new(Mutex::new(StringBuffer::default())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -79,6 +82,7 @@ impl Chat {
|
|||||||
message: &str,
|
message: &str,
|
||||||
message_type: ChatMessageType,
|
message_type: ChatMessageType,
|
||||||
text_stream_port: i64,
|
text_stream_port: i64,
|
||||||
|
metadata: Vec<ChatMessageMetadata>,
|
||||||
) -> Result<ChatMessagePB, FlowyError> {
|
) -> Result<ChatMessagePB, FlowyError> {
|
||||||
if message.len() > 2000 {
|
if message.len() > 2000 {
|
||||||
return Err(FlowyError::text_too_long().with_context("Exceeds maximum message 2000 length"));
|
return Err(FlowyError::text_too_long().with_context("Exceeds maximum message 2000 length"));
|
||||||
@ -87,15 +91,21 @@ impl Chat {
|
|||||||
self
|
self
|
||||||
.stop_stream
|
.stop_stream
|
||||||
.store(false, std::sync::atomic::Ordering::SeqCst);
|
.store(false, std::sync::atomic::Ordering::SeqCst);
|
||||||
self.steam_buffer.lock().await.clear();
|
self.stream_buffer.lock().await.clear();
|
||||||
|
|
||||||
let stream_buffer = self.steam_buffer.clone();
|
let stream_buffer = self.stream_buffer.clone();
|
||||||
let uid = self.user_service.user_id()?;
|
let uid = self.user_service.user_id()?;
|
||||||
let workspace_id = self.user_service.workspace_id()?;
|
let workspace_id = self.user_service.workspace_id()?;
|
||||||
|
|
||||||
let question = self
|
let question = self
|
||||||
.chat_service
|
.chat_service
|
||||||
.save_question(&workspace_id, &self.chat_id, message, message_type)
|
.create_question(
|
||||||
|
&workspace_id,
|
||||||
|
&self.chat_id,
|
||||||
|
message,
|
||||||
|
message_type,
|
||||||
|
metadata,
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(|err| {
|
.map_err(|err| {
|
||||||
error!("Failed to send question: {}", err);
|
error!("Failed to send question: {}", err);
|
||||||
@ -116,7 +126,7 @@ impl Chat {
|
|||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let mut text_sink = IsolateSink::new(Isolate::new(text_stream_port));
|
let mut text_sink = IsolateSink::new(Isolate::new(text_stream_port));
|
||||||
match cloud_service
|
match cloud_service
|
||||||
.ask_question(&workspace_id, &chat_id, question_id)
|
.stream_answer(&workspace_id, &chat_id, question_id)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(mut stream) => {
|
Ok(mut stream) => {
|
||||||
@ -127,9 +137,18 @@ impl Chat {
|
|||||||
trace!("[Chat] stop streaming message");
|
trace!("[Chat] stop streaming message");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
let s = String::from_utf8(message.to_vec()).unwrap_or_default();
|
match message {
|
||||||
stream_buffer.lock().await.push_str(&s);
|
QuestionStreamValue::Answer { value } => {
|
||||||
let _ = text_sink.send(format!("data:{}", s)).await;
|
stream_buffer.lock().await.push_str(&value);
|
||||||
|
let _ = text_sink.send(format!("data:{}", value)).await;
|
||||||
|
},
|
||||||
|
QuestionStreamValue::Metadata { value } => {
|
||||||
|
if let Ok(s) = serde_json::to_string(&value) {
|
||||||
|
stream_buffer.lock().await.set_metadata(value);
|
||||||
|
let _ = text_sink.send(format!("metadata:{}", s)).await;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
},
|
},
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!("[Chat] failed to stream answer: {}", err);
|
error!("[Chat] failed to stream answer: {}", err);
|
||||||
@ -169,14 +188,11 @@ impl Chat {
|
|||||||
if stream_buffer.lock().await.is_empty() {
|
if stream_buffer.lock().await.is_empty() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
let content = stream_buffer.lock().await.take_content();
|
||||||
|
let metadata = stream_buffer.lock().await.take_metadata();
|
||||||
|
|
||||||
let answer = cloud_service
|
let answer = cloud_service
|
||||||
.save_answer(
|
.create_answer(&workspace_id, &chat_id, &content, question_id, metadata)
|
||||||
&workspace_id,
|
|
||||||
&chat_id,
|
|
||||||
&stream_buffer.lock().await,
|
|
||||||
question_id,
|
|
||||||
)
|
|
||||||
.await?;
|
.await?;
|
||||||
Self::save_answer(uid, &chat_id, &user_service, answer)?;
|
Self::save_answer(uid, &chat_id, &user_service, answer)?;
|
||||||
Ok::<(), FlowyError>(())
|
Ok::<(), FlowyError>(())
|
||||||
@ -192,6 +208,7 @@ impl Chat {
|
|||||||
user_service: &Arc<dyn AIUserService>,
|
user_service: &Arc<dyn AIUserService>,
|
||||||
answer: ChatMessage,
|
answer: ChatMessage,
|
||||||
) -> Result<(), FlowyError> {
|
) -> Result<(), FlowyError> {
|
||||||
|
trace!("[Chat] save answer: answer={:?}", answer);
|
||||||
save_chat_message(
|
save_chat_message(
|
||||||
user_service.sqlite_connection(uid)?,
|
user_service.sqlite_connection(uid)?,
|
||||||
chat_id,
|
chat_id,
|
||||||
@ -405,7 +422,7 @@ impl Chat {
|
|||||||
let workspace_id = self.user_service.workspace_id()?;
|
let workspace_id = self.user_service.workspace_id()?;
|
||||||
let answer = self
|
let answer = self
|
||||||
.chat_service
|
.chat_service
|
||||||
.generate_answer(&workspace_id, &self.chat_id, question_message_id)
|
.get_answer(&workspace_id, &self.chat_id, question_message_id)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Self::save_answer(self.uid, &self.chat_id, &self.user_service, answer.clone())?;
|
Self::save_answer(self.uid, &self.chat_id, &self.user_service, answer.clone())?;
|
||||||
@ -436,6 +453,7 @@ impl Chat {
|
|||||||
author_type: record.author_type,
|
author_type: record.author_type,
|
||||||
author_id: record.author_id,
|
author_id: record.author_id,
|
||||||
reply_message_id: record.reply_message_id,
|
reply_message_id: record.reply_message_id,
|
||||||
|
metadata: record.metadata,
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
@ -485,8 +503,42 @@ fn save_chat_message(
|
|||||||
author_type: message.author.author_type as i64,
|
author_type: message.author.author_type as i64,
|
||||||
author_id: message.author.author_id.to_string(),
|
author_id: message.author.author_id.to_string(),
|
||||||
reply_message_id: message.reply_message_id,
|
reply_message_id: message.reply_message_id,
|
||||||
|
metadata: Some(serde_json::to_string(&message.meta_data).unwrap_or_default()),
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
insert_chat_messages(conn, &records)?;
|
insert_chat_messages(conn, &records)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
struct StringBuffer {
|
||||||
|
content: String,
|
||||||
|
metadata: Option<serde_json::Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StringBuffer {
|
||||||
|
fn clear(&mut self) {
|
||||||
|
self.content.clear();
|
||||||
|
self.metadata = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_str(&mut self, value: &str) {
|
||||||
|
self.content.push_str(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_metadata(&mut self, value: serde_json::Value) {
|
||||||
|
self.metadata = Some(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_empty(&self) -> bool {
|
||||||
|
self.content.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn take_metadata(&mut self) -> Option<serde_json::Value> {
|
||||||
|
self.metadata.take()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn take_content(&mut self) -> String {
|
||||||
|
std::mem::take(&mut self.content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
use crate::local_ai::local_llm_chat::LLMModelInfo;
|
use crate::local_ai::local_llm_chat::LLMModelInfo;
|
||||||
use appflowy_plugin::core::plugin::RunningState;
|
use appflowy_plugin::core::plugin::RunningState;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use crate::local_ai::local_llm_resource::PendingResource;
|
use crate::local_ai::local_llm_resource::PendingResource;
|
||||||
use flowy_ai_pub::cloud::{
|
use flowy_ai_pub::cloud::{
|
||||||
@ -38,6 +39,21 @@ pub struct StreamChatPayloadPB {
|
|||||||
|
|
||||||
#[pb(index = 4)]
|
#[pb(index = 4)]
|
||||||
pub text_stream_port: i64,
|
pub text_stream_port: i64,
|
||||||
|
|
||||||
|
#[pb(index = 5)]
|
||||||
|
pub metadata: Vec<ChatMessageMetaPB>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, ProtoBuf, Validate, Clone, Debug)]
|
||||||
|
pub struct ChatMessageMetaPB {
|
||||||
|
#[pb(index = 1)]
|
||||||
|
pub id: String,
|
||||||
|
|
||||||
|
#[pb(index = 2)]
|
||||||
|
pub name: String,
|
||||||
|
|
||||||
|
#[pb(index = 3)]
|
||||||
|
pub text: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, ProtoBuf, Validate, Clone, Debug)]
|
#[derive(Default, ProtoBuf, Validate, Clone, Debug)]
|
||||||
@ -127,6 +143,9 @@ pub struct ChatMessagePB {
|
|||||||
|
|
||||||
#[pb(index = 6, one_of)]
|
#[pb(index = 6, one_of)]
|
||||||
pub reply_message_id: Option<i64>,
|
pub reply_message_id: Option<i64>,
|
||||||
|
|
||||||
|
#[pb(index = 7, one_of)]
|
||||||
|
pub metadata: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default, ProtoBuf)]
|
#[derive(Debug, Clone, Default, ProtoBuf)]
|
||||||
@ -147,6 +166,7 @@ impl From<ChatMessage> for ChatMessagePB {
|
|||||||
author_type: chat_message.author.author_type as i64,
|
author_type: chat_message.author.author_type as i64,
|
||||||
author_id: chat_message.author.author_id.to_string(),
|
author_id: chat_message.author.author_id.to_string(),
|
||||||
reply_message_id: None,
|
reply_message_id: None,
|
||||||
|
metadata: Some(serde_json::to_string(&chat_message.meta_data).unwrap_or_default()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -445,3 +465,21 @@ pub struct OfflineAIPB {
|
|||||||
#[pb(index = 1)]
|
#[pb(index = 1)]
|
||||||
pub link: String,
|
pub link: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Default, ProtoBuf, Validate, Clone, Debug)]
|
||||||
|
pub struct CreateChatContextPB {
|
||||||
|
#[pb(index = 1)]
|
||||||
|
#[validate(custom = "required_not_empty_str")]
|
||||||
|
pub content_type: String,
|
||||||
|
|
||||||
|
#[pb(index = 2)]
|
||||||
|
#[validate(custom = "required_not_empty_str")]
|
||||||
|
pub text: String,
|
||||||
|
|
||||||
|
#[pb(index = 3)]
|
||||||
|
pub metadata: HashMap<String, String>,
|
||||||
|
|
||||||
|
#[pb(index = 4)]
|
||||||
|
#[validate(custom = "required_not_empty_str")]
|
||||||
|
pub chat_id: String,
|
||||||
|
}
|
||||||
|
@ -1,20 +1,18 @@
|
|||||||
use flowy_ai_pub::cloud::ChatMessageType;
|
|
||||||
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use allo_isolate::Isolate;
|
|
||||||
use std::sync::{Arc, Weak};
|
|
||||||
use tokio::sync::oneshot;
|
|
||||||
use validator::Validate;
|
|
||||||
|
|
||||||
use crate::ai_manager::AIManager;
|
use crate::ai_manager::AIManager;
|
||||||
use crate::completion::AICompletion;
|
use crate::completion::AICompletion;
|
||||||
use crate::entities::*;
|
use crate::entities::*;
|
||||||
use crate::local_ai::local_llm_chat::LLMModelInfo;
|
use crate::local_ai::local_llm_chat::LLMModelInfo;
|
||||||
use crate::notification::{make_notification, ChatNotification, APPFLOWY_AI_NOTIFICATION_KEY};
|
use crate::notification::{make_notification, ChatNotification, APPFLOWY_AI_NOTIFICATION_KEY};
|
||||||
|
use allo_isolate::Isolate;
|
||||||
|
use flowy_ai_pub::cloud::{ChatMessageMetadata, ChatMessageType, ChatMetadataData};
|
||||||
use flowy_error::{ErrorCode, FlowyError, FlowyResult};
|
use flowy_error::{ErrorCode, FlowyError, FlowyResult};
|
||||||
use lib_dispatch::prelude::{data_result_ok, AFPluginData, AFPluginState, DataResult};
|
use lib_dispatch::prelude::{data_result_ok, AFPluginData, AFPluginState, DataResult};
|
||||||
use lib_infra::isolate_stream::IsolateSink;
|
use lib_infra::isolate_stream::IsolateSink;
|
||||||
|
use std::sync::{Arc, Weak};
|
||||||
|
use tokio::sync::oneshot;
|
||||||
|
use validator::Validate;
|
||||||
|
|
||||||
fn upgrade_ai_manager(ai_manager: AFPluginState<Weak<AIManager>>) -> FlowyResult<Arc<AIManager>> {
|
fn upgrade_ai_manager(ai_manager: AFPluginState<Weak<AIManager>>) -> FlowyResult<Arc<AIManager>> {
|
||||||
let ai_manager = ai_manager
|
let ai_manager = ai_manager
|
||||||
@ -37,12 +35,24 @@ pub(crate) async fn stream_chat_message_handler(
|
|||||||
ChatMessageTypePB::User => ChatMessageType::User,
|
ChatMessageTypePB::User => ChatMessageType::User,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let metadata = data
|
||||||
|
.metadata
|
||||||
|
.into_iter()
|
||||||
|
.map(|metadata| ChatMessageMetadata {
|
||||||
|
data: ChatMetadataData::new_text(metadata.text),
|
||||||
|
id: metadata.id,
|
||||||
|
name: metadata.name.clone(),
|
||||||
|
source: metadata.name,
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
let question = ai_manager
|
let question = ai_manager
|
||||||
.stream_chat_message(
|
.stream_chat_message(
|
||||||
&data.chat_id,
|
&data.chat_id,
|
||||||
&data.message,
|
&data.message,
|
||||||
message_type,
|
message_type,
|
||||||
data.text_stream_port,
|
data.text_stream_port,
|
||||||
|
metadata,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
data_result_ok(question)
|
data_result_ok(question)
|
||||||
@ -386,3 +396,13 @@ pub(crate) async fn get_offline_app_handler(
|
|||||||
let link = rx.await??;
|
let link = rx.await??;
|
||||||
data_result_ok(OfflineAIPB { link })
|
data_result_ok(OfflineAIPB { link })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(level = "debug", skip_all, err)]
|
||||||
|
pub(crate) async fn create_chat_context_handler(
|
||||||
|
data: AFPluginData<CreateChatContextPB>,
|
||||||
|
_ai_manager: AFPluginState<Weak<AIManager>>,
|
||||||
|
) -> Result<(), FlowyError> {
|
||||||
|
let _data = data.try_into_inner()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
@ -55,6 +55,7 @@ pub fn init(ai_manager: Weak<AIManager>) -> AFPlugin {
|
|||||||
get_model_storage_directory_handler,
|
get_model_storage_directory_handler,
|
||||||
)
|
)
|
||||||
.event(AIEvent::GetOfflineAIAppLink, get_offline_app_handler)
|
.event(AIEvent::GetOfflineAIAppLink, get_offline_app_handler)
|
||||||
|
.event(AIEvent::CreateChatContext, create_chat_context_handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Display, Hash, ProtoBuf_Enum, Flowy_Event)]
|
#[derive(Clone, Copy, PartialEq, Eq, Debug, Display, Hash, ProtoBuf_Enum, Flowy_Event)]
|
||||||
@ -134,4 +135,7 @@ pub enum AIEvent {
|
|||||||
|
|
||||||
#[event(output = "OfflineAIPB")]
|
#[event(output = "OfflineAIPB")]
|
||||||
GetOfflineAIAppLink = 22,
|
GetOfflineAIAppLink = 22,
|
||||||
|
|
||||||
|
#[event(input = "CreateChatContextPB")]
|
||||||
|
CreateChatContext = 23,
|
||||||
}
|
}
|
||||||
|
@ -3,5 +3,6 @@ pub mod local_llm_resource;
|
|||||||
mod model_request;
|
mod model_request;
|
||||||
|
|
||||||
mod path;
|
mod path;
|
||||||
|
pub mod stream_util;
|
||||||
#[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))]
|
#[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))]
|
||||||
pub mod watch;
|
pub mod watch;
|
||||||
|
42
frontend/rust-lib/flowy-ai/src/local_ai/stream_util.rs
Normal file
42
frontend/rust-lib/flowy-ai/src/local_ai/stream_util.rs
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
use appflowy_plugin::error::PluginError;
|
||||||
|
use bytes::Bytes;
|
||||||
|
use flowy_ai_pub::cloud::QuestionStreamValue;
|
||||||
|
use flowy_error::FlowyError;
|
||||||
|
use futures::{ready, Stream};
|
||||||
|
use pin_project::pin_project;
|
||||||
|
use std::pin::Pin;
|
||||||
|
use std::task::{Context, Poll};
|
||||||
|
|
||||||
|
#[pin_project]
|
||||||
|
pub struct LocalAIStreamAdaptor {
|
||||||
|
stream: Pin<Box<dyn Stream<Item = Result<Bytes, PluginError>> + Send>>,
|
||||||
|
buffer: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LocalAIStreamAdaptor {
|
||||||
|
pub fn new<S>(stream: S) -> Self
|
||||||
|
where
|
||||||
|
S: Stream<Item = Result<Bytes, PluginError>> + Send + 'static,
|
||||||
|
{
|
||||||
|
LocalAIStreamAdaptor {
|
||||||
|
stream: Box::pin(stream),
|
||||||
|
buffer: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Stream for LocalAIStreamAdaptor {
|
||||||
|
type Item = Result<QuestionStreamValue, FlowyError>;
|
||||||
|
|
||||||
|
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||||
|
let this = self.project();
|
||||||
|
return match ready!(this.stream.as_mut().poll_next(cx)) {
|
||||||
|
Some(Ok(bytes)) => match String::from_utf8(bytes.to_vec()) {
|
||||||
|
Ok(s) => Poll::Ready(Some(Ok(QuestionStreamValue::Answer { value: s }))),
|
||||||
|
Err(err) => Poll::Ready(Some(Err(FlowyError::internal().with_context(err)))),
|
||||||
|
},
|
||||||
|
Some(Err(err)) => Poll::Ready(Some(Err(FlowyError::local_ai().with_context(err)))),
|
||||||
|
None => Poll::Ready(None),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@ -6,14 +6,16 @@ use crate::persistence::select_single_message;
|
|||||||
use appflowy_plugin::error::PluginError;
|
use appflowy_plugin::error::PluginError;
|
||||||
|
|
||||||
use flowy_ai_pub::cloud::{
|
use flowy_ai_pub::cloud::{
|
||||||
ChatCloudService, ChatMessage, ChatMessageType, CompletionType, LocalAIConfig, MessageCursor,
|
ChatCloudService, ChatMessage, ChatMessageMetadata, ChatMessageType, CompletionType,
|
||||||
RelatedQuestion, RepeatedChatMessage, RepeatedRelatedQuestion, StreamAnswer, StreamComplete,
|
CreateTextChatContext, LocalAIConfig, MessageCursor, RelatedQuestion, RepeatedChatMessage,
|
||||||
|
RepeatedRelatedQuestion, StreamAnswer, StreamComplete,
|
||||||
};
|
};
|
||||||
use flowy_error::{FlowyError, FlowyResult};
|
use flowy_error::{FlowyError, FlowyResult};
|
||||||
use futures::{stream, StreamExt, TryStreamExt};
|
use futures::{stream, StreamExt, TryStreamExt};
|
||||||
use lib_infra::async_trait::async_trait;
|
use lib_infra::async_trait::async_trait;
|
||||||
use lib_infra::future::FutureResult;
|
use lib_infra::future::FutureResult;
|
||||||
|
|
||||||
|
use crate::local_ai::stream_util::LocalAIStreamAdaptor;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
@ -77,31 +79,33 @@ impl ChatCloudService for AICloudServiceMiddleware {
|
|||||||
self.cloud_service.create_chat(uid, workspace_id, chat_id)
|
self.cloud_service.create_chat(uid, workspace_id, chat_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn save_question(
|
fn create_question(
|
||||||
&self,
|
&self,
|
||||||
workspace_id: &str,
|
workspace_id: &str,
|
||||||
chat_id: &str,
|
chat_id: &str,
|
||||||
message: &str,
|
message: &str,
|
||||||
message_type: ChatMessageType,
|
message_type: ChatMessageType,
|
||||||
|
metadata: Vec<ChatMessageMetadata>,
|
||||||
) -> FutureResult<ChatMessage, FlowyError> {
|
) -> FutureResult<ChatMessage, FlowyError> {
|
||||||
self
|
self
|
||||||
.cloud_service
|
.cloud_service
|
||||||
.save_question(workspace_id, chat_id, message, message_type)
|
.create_question(workspace_id, chat_id, message, message_type, metadata)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn save_answer(
|
fn create_answer(
|
||||||
&self,
|
&self,
|
||||||
workspace_id: &str,
|
workspace_id: &str,
|
||||||
chat_id: &str,
|
chat_id: &str,
|
||||||
message: &str,
|
message: &str,
|
||||||
question_id: i64,
|
question_id: i64,
|
||||||
|
metadata: Option<serde_json::Value>,
|
||||||
) -> FutureResult<ChatMessage, FlowyError> {
|
) -> FutureResult<ChatMessage, FlowyError> {
|
||||||
self
|
self
|
||||||
.cloud_service
|
.cloud_service
|
||||||
.save_answer(workspace_id, chat_id, message, question_id)
|
.create_answer(workspace_id, chat_id, message, question_id, metadata)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn ask_question(
|
async fn stream_answer(
|
||||||
&self,
|
&self,
|
||||||
workspace_id: &str,
|
workspace_id: &str,
|
||||||
chat_id: &str,
|
chat_id: &str,
|
||||||
@ -114,11 +118,7 @@ impl ChatCloudService for AICloudServiceMiddleware {
|
|||||||
.stream_question(chat_id, &content)
|
.stream_question(chat_id, &content)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(stream) => Ok(
|
Ok(stream) => Ok(LocalAIStreamAdaptor::new(stream).boxed()),
|
||||||
stream
|
|
||||||
.map_err(|err| FlowyError::local_ai().with_context(err))
|
|
||||||
.boxed(),
|
|
||||||
),
|
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
self.handle_plugin_error(err);
|
self.handle_plugin_error(err);
|
||||||
Ok(stream::once(async { Err(FlowyError::local_ai_unavailable()) }).boxed())
|
Ok(stream::once(async { Err(FlowyError::local_ai_unavailable()) }).boxed())
|
||||||
@ -127,12 +127,12 @@ impl ChatCloudService for AICloudServiceMiddleware {
|
|||||||
} else {
|
} else {
|
||||||
self
|
self
|
||||||
.cloud_service
|
.cloud_service
|
||||||
.ask_question(workspace_id, chat_id, message_id)
|
.stream_answer(workspace_id, chat_id, message_id)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn generate_answer(
|
async fn get_answer(
|
||||||
&self,
|
&self,
|
||||||
workspace_id: &str,
|
workspace_id: &str,
|
||||||
chat_id: &str,
|
chat_id: &str,
|
||||||
@ -146,9 +146,10 @@ impl ChatCloudService for AICloudServiceMiddleware {
|
|||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(answer) => {
|
Ok(answer) => {
|
||||||
|
// TODO(nathan): metadata
|
||||||
let message = self
|
let message = self
|
||||||
.cloud_service
|
.cloud_service
|
||||||
.save_answer(workspace_id, chat_id, &answer, question_message_id)
|
.create_answer(workspace_id, chat_id, &answer, question_message_id, None)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(message)
|
Ok(message)
|
||||||
},
|
},
|
||||||
@ -160,7 +161,7 @@ impl ChatCloudService for AICloudServiceMiddleware {
|
|||||||
} else {
|
} else {
|
||||||
self
|
self
|
||||||
.cloud_service
|
.cloud_service
|
||||||
.generate_answer(workspace_id, chat_id, question_message_id)
|
.get_answer(workspace_id, chat_id, question_message_id)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -262,4 +263,20 @@ impl ChatCloudService for AICloudServiceMiddleware {
|
|||||||
async fn get_local_ai_config(&self, workspace_id: &str) -> Result<LocalAIConfig, FlowyError> {
|
async fn get_local_ai_config(&self, workspace_id: &str) -> Result<LocalAIConfig, FlowyError> {
|
||||||
self.cloud_service.get_local_ai_config(workspace_id).await
|
self.cloud_service.get_local_ai_config(workspace_id).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn create_chat_context(
|
||||||
|
&self,
|
||||||
|
workspace_id: &str,
|
||||||
|
chat_context: CreateTextChatContext,
|
||||||
|
) -> Result<(), FlowyError> {
|
||||||
|
if self.local_llm_controller.is_running() {
|
||||||
|
// TODO(nathan): support offline ai context
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
self
|
||||||
|
.cloud_service
|
||||||
|
.create_chat_context(workspace_id, chat_context)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,6 +19,7 @@ pub struct ChatMessageTable {
|
|||||||
pub author_type: i64,
|
pub author_type: i64,
|
||||||
pub author_id: String,
|
pub author_id: String,
|
||||||
pub reply_message_id: Option<i64>,
|
pub reply_message_id: Option<i64>,
|
||||||
|
pub metadata: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn insert_chat_messages(
|
pub fn insert_chat_messages(
|
||||||
|
@ -19,8 +19,8 @@ use collab_integrate::collab_builder::{
|
|||||||
CollabCloudPluginProvider, CollabPluginProviderContext, CollabPluginProviderType,
|
CollabCloudPluginProvider, CollabPluginProviderContext, CollabPluginProviderType,
|
||||||
};
|
};
|
||||||
use flowy_ai_pub::cloud::{
|
use flowy_ai_pub::cloud::{
|
||||||
ChatCloudService, ChatMessage, LocalAIConfig, MessageCursor, RepeatedChatMessage, StreamAnswer,
|
ChatCloudService, ChatMessage, ChatMessageMetadata, LocalAIConfig, MessageCursor,
|
||||||
StreamComplete,
|
RepeatedChatMessage, StreamAnswer, StreamComplete,
|
||||||
};
|
};
|
||||||
use flowy_database_pub::cloud::{
|
use flowy_database_pub::cloud::{
|
||||||
CollabDocStateByOid, DatabaseAIService, DatabaseCloudService, DatabaseSnapshot,
|
CollabDocStateByOid, DatabaseAIService, DatabaseCloudService, DatabaseSnapshot,
|
||||||
@ -611,12 +611,13 @@ impl ChatCloudService for ServerProvider {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn save_question(
|
fn create_question(
|
||||||
&self,
|
&self,
|
||||||
workspace_id: &str,
|
workspace_id: &str,
|
||||||
chat_id: &str,
|
chat_id: &str,
|
||||||
message: &str,
|
message: &str,
|
||||||
message_type: ChatMessageType,
|
message_type: ChatMessageType,
|
||||||
|
metadata: Vec<ChatMessageMetadata>,
|
||||||
) -> FutureResult<ChatMessage, FlowyError> {
|
) -> FutureResult<ChatMessage, FlowyError> {
|
||||||
let workspace_id = workspace_id.to_string();
|
let workspace_id = workspace_id.to_string();
|
||||||
let chat_id = chat_id.to_string();
|
let chat_id = chat_id.to_string();
|
||||||
@ -626,17 +627,18 @@ impl ChatCloudService for ServerProvider {
|
|||||||
FutureResult::new(async move {
|
FutureResult::new(async move {
|
||||||
server?
|
server?
|
||||||
.chat_service()
|
.chat_service()
|
||||||
.save_question(&workspace_id, &chat_id, &message, message_type)
|
.create_question(&workspace_id, &chat_id, &message, message_type, metadata)
|
||||||
.await
|
.await
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn save_answer(
|
fn create_answer(
|
||||||
&self,
|
&self,
|
||||||
workspace_id: &str,
|
workspace_id: &str,
|
||||||
chat_id: &str,
|
chat_id: &str,
|
||||||
message: &str,
|
message: &str,
|
||||||
question_id: i64,
|
question_id: i64,
|
||||||
|
metadata: Option<serde_json::Value>,
|
||||||
) -> FutureResult<ChatMessage, FlowyError> {
|
) -> FutureResult<ChatMessage, FlowyError> {
|
||||||
let workspace_id = workspace_id.to_string();
|
let workspace_id = workspace_id.to_string();
|
||||||
let chat_id = chat_id.to_string();
|
let chat_id = chat_id.to_string();
|
||||||
@ -645,12 +647,12 @@ impl ChatCloudService for ServerProvider {
|
|||||||
FutureResult::new(async move {
|
FutureResult::new(async move {
|
||||||
server?
|
server?
|
||||||
.chat_service()
|
.chat_service()
|
||||||
.save_answer(&workspace_id, &chat_id, &message, question_id)
|
.create_answer(&workspace_id, &chat_id, &message, question_id, metadata)
|
||||||
.await
|
.await
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn ask_question(
|
async fn stream_answer(
|
||||||
&self,
|
&self,
|
||||||
workspace_id: &str,
|
workspace_id: &str,
|
||||||
chat_id: &str,
|
chat_id: &str,
|
||||||
@ -661,7 +663,7 @@ impl ChatCloudService for ServerProvider {
|
|||||||
let server = self.get_server()?;
|
let server = self.get_server()?;
|
||||||
server
|
server
|
||||||
.chat_service()
|
.chat_service()
|
||||||
.ask_question(&workspace_id, &chat_id, message_id)
|
.stream_answer(&workspace_id, &chat_id, message_id)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -696,7 +698,7 @@ impl ChatCloudService for ServerProvider {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn generate_answer(
|
async fn get_answer(
|
||||||
&self,
|
&self,
|
||||||
workspace_id: &str,
|
workspace_id: &str,
|
||||||
chat_id: &str,
|
chat_id: &str,
|
||||||
@ -705,7 +707,7 @@ impl ChatCloudService for ServerProvider {
|
|||||||
let server = self.get_server();
|
let server = self.get_server();
|
||||||
server?
|
server?
|
||||||
.chat_service()
|
.chat_service()
|
||||||
.generate_answer(workspace_id, chat_id, question_message_id)
|
.get_answer(workspace_id, chat_id, question_message_id)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -203,6 +203,11 @@ pub struct DocumentDataPB {
|
|||||||
pub meta: MetaPB,
|
pub meta: MetaPB,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, ProtoBuf)]
|
||||||
|
pub struct DocumentTextPB {
|
||||||
|
#[pb(index = 1)]
|
||||||
|
pub text: String,
|
||||||
|
}
|
||||||
#[derive(Default, ProtoBuf, Debug, Clone)]
|
#[derive(Default, ProtoBuf, Debug, Clone)]
|
||||||
pub struct BlockPB {
|
pub struct BlockPB {
|
||||||
#[pb(index = 1)]
|
#[pb(index = 1)]
|
||||||
|
@ -74,7 +74,7 @@ pub(crate) async fn open_document_handler(
|
|||||||
let doc_id = params.document_id;
|
let doc_id = params.document_id;
|
||||||
manager.open_document(&doc_id).await?;
|
manager.open_document(&doc_id).await?;
|
||||||
|
|
||||||
let document = manager.get_document(&doc_id).await?;
|
let document = manager.get_opened_document(&doc_id).await?;
|
||||||
let document_data = document.lock().get_document_data()?;
|
let document_data = document.lock().get_document_data()?;
|
||||||
data_result_ok(DocumentDataPB::from(document_data))
|
data_result_ok(DocumentDataPB::from(document_data))
|
||||||
}
|
}
|
||||||
@ -103,6 +103,17 @@ pub(crate) async fn get_document_data_handler(
|
|||||||
data_result_ok(DocumentDataPB::from(document_data))
|
data_result_ok(DocumentDataPB::from(document_data))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn get_document_text_handler(
|
||||||
|
data: AFPluginData<OpenDocumentPayloadPB>,
|
||||||
|
manager: AFPluginState<Weak<DocumentManager>>,
|
||||||
|
) -> DataResult<DocumentTextPB, FlowyError> {
|
||||||
|
let manager = upgrade_document(manager)?;
|
||||||
|
let params: OpenDocumentParams = data.into_inner().try_into()?;
|
||||||
|
let doc_id = params.document_id;
|
||||||
|
let text = manager.get_document_text(&doc_id).await?;
|
||||||
|
data_result_ok(DocumentTextPB { text })
|
||||||
|
}
|
||||||
|
|
||||||
// Handler for applying an action to a document
|
// Handler for applying an action to a document
|
||||||
pub(crate) async fn apply_action_handler(
|
pub(crate) async fn apply_action_handler(
|
||||||
data: AFPluginData<ApplyActionPayloadPB>,
|
data: AFPluginData<ApplyActionPayloadPB>,
|
||||||
@ -111,7 +122,7 @@ pub(crate) async fn apply_action_handler(
|
|||||||
let manager = upgrade_document(manager)?;
|
let manager = upgrade_document(manager)?;
|
||||||
let params: ApplyActionParams = data.into_inner().try_into()?;
|
let params: ApplyActionParams = data.into_inner().try_into()?;
|
||||||
let doc_id = params.document_id;
|
let doc_id = params.document_id;
|
||||||
let document = manager.get_document(&doc_id).await?;
|
let document = manager.get_opened_document(&doc_id).await?;
|
||||||
let actions = params.actions;
|
let actions = params.actions;
|
||||||
if cfg!(feature = "verbose_log") {
|
if cfg!(feature = "verbose_log") {
|
||||||
tracing::trace!("{} applying actions: {:?}", doc_id, actions);
|
tracing::trace!("{} applying actions: {:?}", doc_id, actions);
|
||||||
@ -128,7 +139,7 @@ pub(crate) async fn create_text_handler(
|
|||||||
let manager = upgrade_document(manager)?;
|
let manager = upgrade_document(manager)?;
|
||||||
let params: TextDeltaParams = data.into_inner().try_into()?;
|
let params: TextDeltaParams = data.into_inner().try_into()?;
|
||||||
let doc_id = params.document_id;
|
let doc_id = params.document_id;
|
||||||
let document = manager.get_document(&doc_id).await?;
|
let document = manager.get_opened_document(&doc_id).await?;
|
||||||
let document = document.lock();
|
let document = document.lock();
|
||||||
document.create_text(¶ms.text_id, params.delta);
|
document.create_text(¶ms.text_id, params.delta);
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -142,7 +153,7 @@ pub(crate) async fn apply_text_delta_handler(
|
|||||||
let manager = upgrade_document(manager)?;
|
let manager = upgrade_document(manager)?;
|
||||||
let params: TextDeltaParams = data.into_inner().try_into()?;
|
let params: TextDeltaParams = data.into_inner().try_into()?;
|
||||||
let doc_id = params.document_id;
|
let doc_id = params.document_id;
|
||||||
let document = manager.get_document(&doc_id).await?;
|
let document = manager.get_opened_document(&doc_id).await?;
|
||||||
let text_id = params.text_id;
|
let text_id = params.text_id;
|
||||||
let delta = params.delta;
|
let delta = params.delta;
|
||||||
let document = document.lock();
|
let document = document.lock();
|
||||||
@ -183,7 +194,7 @@ pub(crate) async fn redo_handler(
|
|||||||
let manager = upgrade_document(manager)?;
|
let manager = upgrade_document(manager)?;
|
||||||
let params: DocumentRedoUndoParams = data.into_inner().try_into()?;
|
let params: DocumentRedoUndoParams = data.into_inner().try_into()?;
|
||||||
let doc_id = params.document_id;
|
let doc_id = params.document_id;
|
||||||
let document = manager.get_document(&doc_id).await?;
|
let document = manager.get_opened_document(&doc_id).await?;
|
||||||
let document = document.lock();
|
let document = document.lock();
|
||||||
let redo = document.redo();
|
let redo = document.redo();
|
||||||
let can_redo = document.can_redo();
|
let can_redo = document.can_redo();
|
||||||
@ -202,7 +213,7 @@ pub(crate) async fn undo_handler(
|
|||||||
let manager = upgrade_document(manager)?;
|
let manager = upgrade_document(manager)?;
|
||||||
let params: DocumentRedoUndoParams = data.into_inner().try_into()?;
|
let params: DocumentRedoUndoParams = data.into_inner().try_into()?;
|
||||||
let doc_id = params.document_id;
|
let doc_id = params.document_id;
|
||||||
let document = manager.get_document(&doc_id).await?;
|
let document = manager.get_opened_document(&doc_id).await?;
|
||||||
let document = document.lock();
|
let document = document.lock();
|
||||||
let undo = document.undo();
|
let undo = document.undo();
|
||||||
let can_redo = document.can_redo();
|
let can_redo = document.can_redo();
|
||||||
@ -221,7 +232,7 @@ pub(crate) async fn can_undo_redo_handler(
|
|||||||
let manager = upgrade_document(manager)?;
|
let manager = upgrade_document(manager)?;
|
||||||
let params: DocumentRedoUndoParams = data.into_inner().try_into()?;
|
let params: DocumentRedoUndoParams = data.into_inner().try_into()?;
|
||||||
let doc_id = params.document_id;
|
let doc_id = params.document_id;
|
||||||
let document = manager.get_document(&doc_id).await?;
|
let document = manager.get_opened_document(&doc_id).await?;
|
||||||
let document = document.lock();
|
let document = document.lock();
|
||||||
let can_redo = document.can_redo();
|
let can_redo = document.can_redo();
|
||||||
let can_undo = document.can_undo();
|
let can_undo = document.can_undo();
|
||||||
@ -377,7 +388,7 @@ pub async fn convert_document_handler(
|
|||||||
let manager = upgrade_document(manager)?;
|
let manager = upgrade_document(manager)?;
|
||||||
let params: ConvertDocumentParams = data.into_inner().try_into()?;
|
let params: ConvertDocumentParams = data.into_inner().try_into()?;
|
||||||
|
|
||||||
let document = manager.get_document(¶ms.document_id).await?;
|
let document = manager.get_opened_document(¶ms.document_id).await?;
|
||||||
let document_data = document.lock().get_document_data()?;
|
let document_data = document.lock().get_document_data()?;
|
||||||
let parser = DocumentDataParser::new(Arc::new(document_data), params.range);
|
let parser = DocumentDataParser::new(Arc::new(document_data), params.range);
|
||||||
|
|
||||||
|
@ -18,6 +18,7 @@ pub fn init(document_manager: Weak<DocumentManager>) -> AFPlugin {
|
|||||||
.event(DocumentEvent::CloseDocument, close_document_handler)
|
.event(DocumentEvent::CloseDocument, close_document_handler)
|
||||||
.event(DocumentEvent::ApplyAction, apply_action_handler)
|
.event(DocumentEvent::ApplyAction, apply_action_handler)
|
||||||
.event(DocumentEvent::GetDocumentData, get_document_data_handler)
|
.event(DocumentEvent::GetDocumentData, get_document_data_handler)
|
||||||
|
.event(DocumentEvent::GetDocumentText, get_document_text_handler)
|
||||||
.event(
|
.event(
|
||||||
DocumentEvent::GetDocEncodedCollab,
|
DocumentEvent::GetDocEncodedCollab,
|
||||||
get_encode_collab_handler,
|
get_encode_collab_handler,
|
||||||
@ -133,4 +134,7 @@ pub enum DocumentEvent {
|
|||||||
|
|
||||||
#[event(input = "OpenDocumentPayloadPB", output = "EncodedCollabPB")]
|
#[event(input = "OpenDocumentPayloadPB", output = "EncodedCollabPB")]
|
||||||
GetDocEncodedCollab = 19,
|
GetDocEncodedCollab = 19,
|
||||||
|
|
||||||
|
#[event(input = "OpenDocumentPayloadPB", output = "DocumentTextPB")]
|
||||||
|
GetDocumentText = 20,
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ use collab::core::origin::CollabOrigin;
|
|||||||
use collab::entity::EncodedCollab;
|
use collab::entity::EncodedCollab;
|
||||||
use collab::preclude::Collab;
|
use collab::preclude::Collab;
|
||||||
use collab_document::blocks::DocumentData;
|
use collab_document::blocks::DocumentData;
|
||||||
|
use collab_document::conversions::convert_document_to_plain_text;
|
||||||
use collab_document::document::Document;
|
use collab_document::document::Document;
|
||||||
use collab_document::document_awareness::DocumentAwarenessState;
|
use collab_document::document_awareness::DocumentAwarenessState;
|
||||||
use collab_document::document_awareness::DocumentAwarenessUser;
|
use collab_document::document_awareness::DocumentAwarenessUser;
|
||||||
@ -151,7 +152,7 @@ impl DocumentManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_document(&self, doc_id: &str) -> FlowyResult<Arc<MutexDocument>> {
|
pub async fn get_opened_document(&self, doc_id: &str) -> FlowyResult<Arc<MutexDocument>> {
|
||||||
if let Some(doc) = self.documents.get(doc_id).map(|item| item.value().clone()) {
|
if let Some(doc) = self.documents.get(doc_id).map(|item| item.value().clone()) {
|
||||||
return Ok(doc);
|
return Ok(doc);
|
||||||
}
|
}
|
||||||
@ -166,7 +167,7 @@ impl DocumentManager {
|
|||||||
/// If the document does not exist in local disk, try get the doc state from the cloud.
|
/// If the document does not exist in local disk, try get the doc state from the cloud.
|
||||||
/// If the document exists, open the document and cache it
|
/// If the document exists, open the document and cache it
|
||||||
#[tracing::instrument(level = "info", skip(self), err)]
|
#[tracing::instrument(level = "info", skip(self), err)]
|
||||||
async fn create_document_instance(&self, doc_id: &str) -> FlowyResult<Arc<MutexDocument>> {
|
async fn init_document_instance(&self, doc_id: &str) -> FlowyResult<Arc<MutexDocument>> {
|
||||||
if let Some(doc) = self.documents.get(doc_id).map(|item| item.value().clone()) {
|
if let Some(doc) = self.documents.get(doc_id).map(|item| item.value().clone()) {
|
||||||
return Ok(doc);
|
return Ok(doc);
|
||||||
}
|
}
|
||||||
@ -220,6 +221,16 @@ impl DocumentManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_document_data(&self, doc_id: &str) -> FlowyResult<DocumentData> {
|
pub async fn get_document_data(&self, doc_id: &str) -> FlowyResult<DocumentData> {
|
||||||
|
let document = self.get_document(doc_id).await?;
|
||||||
|
document.get_document_data().map_err(internal_error)
|
||||||
|
}
|
||||||
|
pub async fn get_document_text(&self, doc_id: &str) -> FlowyResult<String> {
|
||||||
|
let document = self.get_document(doc_id).await?;
|
||||||
|
let text = convert_document_to_plain_text(document)?;
|
||||||
|
Ok(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_document(&self, doc_id: &str) -> FlowyResult<Document> {
|
||||||
let mut doc_state = DataSource::Disk;
|
let mut doc_state = DataSource::Disk;
|
||||||
if !self.is_doc_exist(doc_id).await? {
|
if !self.is_doc_exist(doc_id).await? {
|
||||||
doc_state = DataSource::DocStateV1(
|
doc_state = DataSource::DocStateV1(
|
||||||
@ -233,9 +244,8 @@ impl DocumentManager {
|
|||||||
let collab = self
|
let collab = self
|
||||||
.collab_for_document(uid, doc_id, doc_state, false)
|
.collab_for_document(uid, doc_id, doc_state, false)
|
||||||
.await?;
|
.await?;
|
||||||
Document::open(collab)?
|
let document = Document::open(collab)?;
|
||||||
.get_document_data()
|
Ok(document)
|
||||||
.map_err(internal_error)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn open_document(&self, doc_id: &str) -> FlowyResult<()> {
|
pub async fn open_document(&self, doc_id: &str) -> FlowyResult<()> {
|
||||||
@ -243,7 +253,7 @@ impl DocumentManager {
|
|||||||
mutex_document.start_init_sync();
|
mutex_document.start_init_sync();
|
||||||
}
|
}
|
||||||
|
|
||||||
let _ = self.create_document_instance(doc_id).await?;
|
let _ = self.init_document_instance(doc_id).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -290,7 +300,7 @@ impl DocumentManager {
|
|||||||
) -> FlowyResult<bool> {
|
) -> FlowyResult<bool> {
|
||||||
let uid = self.user_service.user_id()?;
|
let uid = self.user_service.user_id()?;
|
||||||
let device_id = self.user_service.device_id()?;
|
let device_id = self.user_service.device_id()?;
|
||||||
if let Ok(doc) = self.get_document(doc_id).await {
|
if let Ok(doc) = self.get_opened_document(doc_id).await {
|
||||||
if let Some(doc) = doc.try_lock() {
|
if let Some(doc) = doc.try_lock() {
|
||||||
let user = DocumentAwarenessUser { uid, device_id };
|
let user = DocumentAwarenessUser { uid, device_id };
|
||||||
let selection = state.selection.map(|s| s.into());
|
let selection = state.selection.map(|s| s.into());
|
||||||
|
@ -23,7 +23,7 @@ async fn undo_redo_test() {
|
|||||||
|
|
||||||
// open a document
|
// open a document
|
||||||
test.open_document(&doc_id).await.unwrap();
|
test.open_document(&doc_id).await.unwrap();
|
||||||
let document = test.get_document(&doc_id).await.unwrap();
|
let document = test.get_opened_document(&doc_id).await.unwrap();
|
||||||
let document = document.lock();
|
let document = document.lock();
|
||||||
let page_block = document.get_block(&data.page_id).unwrap();
|
let page_block = document.get_block(&data.page_id).unwrap();
|
||||||
let page_id = page_block.id;
|
let page_id = page_block.id;
|
||||||
|
@ -23,7 +23,7 @@ async fn restore_document() {
|
|||||||
test.open_document(&doc_id).await.unwrap();
|
test.open_document(&doc_id).await.unwrap();
|
||||||
|
|
||||||
let data_b = test
|
let data_b = test
|
||||||
.get_document(&doc_id)
|
.get_opened_document(&doc_id)
|
||||||
.await
|
.await
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.lock()
|
.lock()
|
||||||
@ -37,7 +37,7 @@ async fn restore_document() {
|
|||||||
_ = test.create_document(uid, &doc_id, Some(data.clone())).await;
|
_ = test.create_document(uid, &doc_id, Some(data.clone())).await;
|
||||||
// open a document
|
// open a document
|
||||||
let data_b = test
|
let data_b = test
|
||||||
.get_document(&doc_id)
|
.get_opened_document(&doc_id)
|
||||||
.await
|
.await
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.lock()
|
.lock()
|
||||||
@ -61,7 +61,7 @@ async fn document_apply_insert_action() {
|
|||||||
|
|
||||||
// open a document
|
// open a document
|
||||||
test.open_document(&doc_id).await.unwrap();
|
test.open_document(&doc_id).await.unwrap();
|
||||||
let document = test.get_document(&doc_id).await.unwrap();
|
let document = test.get_opened_document(&doc_id).await.unwrap();
|
||||||
let page_block = document.lock().get_block(&data.page_id).unwrap();
|
let page_block = document.lock().get_block(&data.page_id).unwrap();
|
||||||
|
|
||||||
// insert a text block
|
// insert a text block
|
||||||
@ -91,7 +91,7 @@ async fn document_apply_insert_action() {
|
|||||||
|
|
||||||
// re-open the document
|
// re-open the document
|
||||||
let data_b = test
|
let data_b = test
|
||||||
.get_document(&doc_id)
|
.get_opened_document(&doc_id)
|
||||||
.await
|
.await
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.lock()
|
.lock()
|
||||||
@ -115,7 +115,7 @@ async fn document_apply_update_page_action() {
|
|||||||
|
|
||||||
// open a document
|
// open a document
|
||||||
test.open_document(&doc_id).await.unwrap();
|
test.open_document(&doc_id).await.unwrap();
|
||||||
let document = test.get_document(&doc_id).await.unwrap();
|
let document = test.get_opened_document(&doc_id).await.unwrap();
|
||||||
let page_block = document.lock().get_block(&data.page_id).unwrap();
|
let page_block = document.lock().get_block(&data.page_id).unwrap();
|
||||||
|
|
||||||
let mut page_block_clone = page_block;
|
let mut page_block_clone = page_block;
|
||||||
@ -141,7 +141,7 @@ async fn document_apply_update_page_action() {
|
|||||||
_ = test.close_document(&doc_id).await;
|
_ = test.close_document(&doc_id).await;
|
||||||
|
|
||||||
// re-open the document
|
// re-open the document
|
||||||
let document = test.get_document(&doc_id).await.unwrap();
|
let document = test.get_opened_document(&doc_id).await.unwrap();
|
||||||
let page_block_new = document.lock().get_block(&data.page_id).unwrap();
|
let page_block_new = document.lock().get_block(&data.page_id).unwrap();
|
||||||
assert_eq!(page_block_old, page_block_new);
|
assert_eq!(page_block_old, page_block_new);
|
||||||
assert!(page_block_new.data.contains_key("delta"));
|
assert!(page_block_new.data.contains_key("delta"));
|
||||||
@ -159,7 +159,7 @@ async fn document_apply_update_action() {
|
|||||||
|
|
||||||
// open a document
|
// open a document
|
||||||
test.open_document(&doc_id).await.unwrap();
|
test.open_document(&doc_id).await.unwrap();
|
||||||
let document = test.get_document(&doc_id).await.unwrap();
|
let document = test.get_opened_document(&doc_id).await.unwrap();
|
||||||
let page_block = document.lock().get_block(&data.page_id).unwrap();
|
let page_block = document.lock().get_block(&data.page_id).unwrap();
|
||||||
|
|
||||||
// insert a text block
|
// insert a text block
|
||||||
@ -213,7 +213,7 @@ async fn document_apply_update_action() {
|
|||||||
_ = test.close_document(&doc_id).await;
|
_ = test.close_document(&doc_id).await;
|
||||||
|
|
||||||
// re-open the document
|
// re-open the document
|
||||||
let document = test.get_document(&doc_id).await.unwrap();
|
let document = test.get_opened_document(&doc_id).await.unwrap();
|
||||||
let block = document.lock().get_block(&text_block_id).unwrap();
|
let block = document.lock().get_block(&text_block_id).unwrap();
|
||||||
assert_eq!(block.data, updated_text_block_data);
|
assert_eq!(block.data, updated_text_block_data);
|
||||||
// close a document
|
// close a document
|
||||||
|
@ -126,7 +126,7 @@ pub async fn create_and_open_empty_document() -> (DocumentTest, Arc<MutexDocumen
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
test.open_document(&doc_id).await.unwrap();
|
test.open_document(&doc_id).await.unwrap();
|
||||||
let document = test.get_document(&doc_id).await.unwrap();
|
let document = test.get_opened_document(&doc_id).await.unwrap();
|
||||||
|
|
||||||
(test, document, data.page_id)
|
(test, document, data.page_id)
|
||||||
}
|
}
|
||||||
|
@ -1,17 +1,21 @@
|
|||||||
use crate::af_cloud::AFServer;
|
use crate::af_cloud::AFServer;
|
||||||
use client_api::entity::ai_dto::{CompleteTextParams, CompletionType, RepeatedRelatedQuestion};
|
use client_api::entity::ai_dto::{
|
||||||
|
CompleteTextParams, CompletionType, CreateTextChatContext, RepeatedRelatedQuestion,
|
||||||
|
};
|
||||||
use client_api::entity::{
|
use client_api::entity::{
|
||||||
CreateAnswerMessageParams, CreateChatMessageParams, CreateChatParams, MessageCursor,
|
CreateAnswerMessageParams, CreateChatMessageParams, CreateChatParams, MessageCursor,
|
||||||
RepeatedChatMessage,
|
RepeatedChatMessage,
|
||||||
};
|
};
|
||||||
use flowy_ai_pub::cloud::{
|
use flowy_ai_pub::cloud::{
|
||||||
ChatCloudService, ChatMessage, ChatMessageType, LocalAIConfig, StreamAnswer, StreamComplete,
|
ChatCloudService, ChatMessage, ChatMessageMetadata, ChatMessageType, LocalAIConfig, StreamAnswer,
|
||||||
|
StreamComplete,
|
||||||
};
|
};
|
||||||
use flowy_error::FlowyError;
|
use flowy_error::FlowyError;
|
||||||
use futures_util::{StreamExt, TryStreamExt};
|
use futures_util::{StreamExt, TryStreamExt};
|
||||||
use lib_infra::async_trait::async_trait;
|
use lib_infra::async_trait::async_trait;
|
||||||
use lib_infra::future::FutureResult;
|
use lib_infra::future::FutureResult;
|
||||||
use lib_infra::util::{get_operating_system, OperatingSystem};
|
use lib_infra::util::{get_operating_system, OperatingSystem};
|
||||||
|
use serde_json::json;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
pub(crate) struct AFCloudChatCloudServiceImpl<T> {
|
pub(crate) struct AFCloudChatCloudServiceImpl<T> {
|
||||||
@ -48,12 +52,13 @@ where
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn save_question(
|
fn create_question(
|
||||||
&self,
|
&self,
|
||||||
workspace_id: &str,
|
workspace_id: &str,
|
||||||
chat_id: &str,
|
chat_id: &str,
|
||||||
message: &str,
|
message: &str,
|
||||||
message_type: ChatMessageType,
|
message_type: ChatMessageType,
|
||||||
|
metadata: Vec<ChatMessageMetadata>,
|
||||||
) -> FutureResult<ChatMessage, FlowyError> {
|
) -> FutureResult<ChatMessage, FlowyError> {
|
||||||
let workspace_id = workspace_id.to_string();
|
let workspace_id = workspace_id.to_string();
|
||||||
let chat_id = chat_id.to_string();
|
let chat_id = chat_id.to_string();
|
||||||
@ -61,29 +66,32 @@ where
|
|||||||
let params = CreateChatMessageParams {
|
let params = CreateChatMessageParams {
|
||||||
content: message.to_string(),
|
content: message.to_string(),
|
||||||
message_type,
|
message_type,
|
||||||
|
metadata: Some(json!(metadata)),
|
||||||
};
|
};
|
||||||
|
|
||||||
FutureResult::new(async move {
|
FutureResult::new(async move {
|
||||||
let message = try_get_client?
|
let message = try_get_client?
|
||||||
.save_question(&workspace_id, &chat_id, params)
|
.create_question(&workspace_id, &chat_id, params)
|
||||||
.await
|
.await
|
||||||
.map_err(FlowyError::from)?;
|
.map_err(FlowyError::from)?;
|
||||||
Ok(message)
|
Ok(message)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn save_answer(
|
fn create_answer(
|
||||||
&self,
|
&self,
|
||||||
workspace_id: &str,
|
workspace_id: &str,
|
||||||
chat_id: &str,
|
chat_id: &str,
|
||||||
message: &str,
|
message: &str,
|
||||||
question_id: i64,
|
question_id: i64,
|
||||||
|
metadata: Option<serde_json::Value>,
|
||||||
) -> FutureResult<ChatMessage, FlowyError> {
|
) -> FutureResult<ChatMessage, FlowyError> {
|
||||||
let workspace_id = workspace_id.to_string();
|
let workspace_id = workspace_id.to_string();
|
||||||
let chat_id = chat_id.to_string();
|
let chat_id = chat_id.to_string();
|
||||||
let try_get_client = self.inner.try_get_client();
|
let try_get_client = self.inner.try_get_client();
|
||||||
let params = CreateAnswerMessageParams {
|
let params = CreateAnswerMessageParams {
|
||||||
content: message.to_string(),
|
content: message.to_string(),
|
||||||
|
metadata,
|
||||||
question_message_id: question_id,
|
question_message_id: question_id,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -96,7 +104,7 @@ where
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn ask_question(
|
async fn stream_answer(
|
||||||
&self,
|
&self,
|
||||||
workspace_id: &str,
|
workspace_id: &str,
|
||||||
chat_id: &str,
|
chat_id: &str,
|
||||||
@ -104,14 +112,14 @@ where
|
|||||||
) -> Result<StreamAnswer, FlowyError> {
|
) -> Result<StreamAnswer, FlowyError> {
|
||||||
let try_get_client = self.inner.try_get_client();
|
let try_get_client = self.inner.try_get_client();
|
||||||
let stream = try_get_client?
|
let stream = try_get_client?
|
||||||
.ask_question(workspace_id, chat_id, message_id)
|
.stream_answer_v2(workspace_id, chat_id, message_id)
|
||||||
.await
|
.await
|
||||||
.map_err(FlowyError::from)?
|
.map_err(FlowyError::from)?
|
||||||
.map_err(FlowyError::from);
|
.map_err(FlowyError::from);
|
||||||
Ok(stream.boxed())
|
Ok(stream.boxed())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn generate_answer(
|
async fn get_answer(
|
||||||
&self,
|
&self,
|
||||||
workspace_id: &str,
|
workspace_id: &str,
|
||||||
chat_id: &str,
|
chat_id: &str,
|
||||||
@ -119,7 +127,7 @@ where
|
|||||||
) -> Result<ChatMessage, FlowyError> {
|
) -> Result<ChatMessage, FlowyError> {
|
||||||
let try_get_client = self.inner.try_get_client();
|
let try_get_client = self.inner.try_get_client();
|
||||||
let resp = try_get_client?
|
let resp = try_get_client?
|
||||||
.generate_answer(workspace_id, chat_id, question_message_id)
|
.get_answer(workspace_id, chat_id, question_message_id)
|
||||||
.await
|
.await
|
||||||
.map_err(FlowyError::from)?;
|
.map_err(FlowyError::from)?;
|
||||||
Ok(resp)
|
Ok(resp)
|
||||||
@ -211,4 +219,17 @@ where
|
|||||||
.await?;
|
.await?;
|
||||||
Ok(config)
|
Ok(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn create_chat_context(
|
||||||
|
&self,
|
||||||
|
workspace_id: &str,
|
||||||
|
chat_context: CreateTextChatContext,
|
||||||
|
) -> Result<(), FlowyError> {
|
||||||
|
self
|
||||||
|
.inner
|
||||||
|
.try_get_client()?
|
||||||
|
.create_chat_context(workspace_id, chat_context)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
use client_api::entity::ai_dto::{CompletionType, LocalAIConfig, RepeatedRelatedQuestion};
|
use client_api::entity::ai_dto::{CompletionType, LocalAIConfig, RepeatedRelatedQuestion};
|
||||||
use client_api::entity::{ChatMessageType, MessageCursor, RepeatedChatMessage};
|
use client_api::entity::{ChatMessageType, MessageCursor, RepeatedChatMessage};
|
||||||
use flowy_ai_pub::cloud::{ChatCloudService, ChatMessage, StreamAnswer, StreamComplete};
|
use flowy_ai_pub::cloud::{
|
||||||
|
ChatCloudService, ChatMessage, ChatMessageMetadata, StreamAnswer, StreamComplete,
|
||||||
|
};
|
||||||
use flowy_error::FlowyError;
|
use flowy_error::FlowyError;
|
||||||
use lib_infra::async_trait::async_trait;
|
use lib_infra::async_trait::async_trait;
|
||||||
use lib_infra::future::FutureResult;
|
use lib_infra::future::FutureResult;
|
||||||
@ -21,31 +23,33 @@ impl ChatCloudService for DefaultChatCloudServiceImpl {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn save_question(
|
fn create_question(
|
||||||
&self,
|
&self,
|
||||||
_workspace_id: &str,
|
_workspace_id: &str,
|
||||||
_chat_id: &str,
|
_chat_id: &str,
|
||||||
_message: &str,
|
_message: &str,
|
||||||
_message_type: ChatMessageType,
|
_message_type: ChatMessageType,
|
||||||
|
_metadata: Vec<ChatMessageMetadata>,
|
||||||
) -> FutureResult<ChatMessage, FlowyError> {
|
) -> FutureResult<ChatMessage, FlowyError> {
|
||||||
FutureResult::new(async move {
|
FutureResult::new(async move {
|
||||||
Err(FlowyError::not_support().with_context("Chat is not supported in local server."))
|
Err(FlowyError::not_support().with_context("Chat is not supported in local server."))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn save_answer(
|
fn create_answer(
|
||||||
&self,
|
&self,
|
||||||
_workspace_id: &str,
|
_workspace_id: &str,
|
||||||
_chat_id: &str,
|
_chat_id: &str,
|
||||||
_message: &str,
|
_message: &str,
|
||||||
_question_id: i64,
|
_question_id: i64,
|
||||||
|
_metadata: Option<serde_json::Value>,
|
||||||
) -> FutureResult<ChatMessage, FlowyError> {
|
) -> FutureResult<ChatMessage, FlowyError> {
|
||||||
FutureResult::new(async move {
|
FutureResult::new(async move {
|
||||||
Err(FlowyError::not_support().with_context("Chat is not supported in local server."))
|
Err(FlowyError::not_support().with_context("Chat is not supported in local server."))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn ask_question(
|
async fn stream_answer(
|
||||||
&self,
|
&self,
|
||||||
_workspace_id: &str,
|
_workspace_id: &str,
|
||||||
_chat_id: &str,
|
_chat_id: &str,
|
||||||
@ -75,7 +79,7 @@ impl ChatCloudService for DefaultChatCloudServiceImpl {
|
|||||||
Err(FlowyError::not_support().with_context("Chat is not supported in local server."))
|
Err(FlowyError::not_support().with_context("Chat is not supported in local server."))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn generate_answer(
|
async fn get_answer(
|
||||||
&self,
|
&self,
|
||||||
_workspace_id: &str,
|
_workspace_id: &str,
|
||||||
_chat_id: &str,
|
_chat_id: &str,
|
||||||
|
@ -0,0 +1,3 @@
|
|||||||
|
-- This file should undo anything in `up.sql`
|
||||||
|
ALTER TABLE chat_message_table DROP COLUMN metadata;
|
||||||
|
|
@ -0,0 +1,2 @@
|
|||||||
|
-- Your SQL goes here
|
||||||
|
ALTER TABLE chat_message_table ADD COLUMN metadata TEXT;
|
@ -17,6 +17,7 @@ diesel::table! {
|
|||||||
author_type -> BigInt,
|
author_type -> BigInt,
|
||||||
author_id -> Text,
|
author_id -> Text,
|
||||||
reply_message_id -> Nullable<BigInt>,
|
reply_message_id -> Nullable<BigInt>,
|
||||||
|
metadata -> Nullable<Text>,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user