mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
chore: chat with pdf ui (#5811)
* chore: chat with pdf ui * chore: only enable local ai on macos * chore: add todo * chore: adjust UI * chore: clippy
This commit is contained in:
parent
d1af172fb7
commit
a2e211555e
@ -17,7 +17,7 @@ class ChatAIMessageBloc extends Bloc<ChatAIMessageEvent, ChatAIMessageState> {
|
||||
required this.questionId,
|
||||
}) : super(ChatAIMessageState.initial(message)) {
|
||||
if (state.stream != null) {
|
||||
_subscription = state.stream!.listen(
|
||||
state.stream!.listen(
|
||||
onData: (text) {
|
||||
if (!isClosed) {
|
||||
add(ChatAIMessageEvent.updateText(text));
|
||||
@ -108,13 +108,6 @@ class ChatAIMessageBloc extends Bloc<ChatAIMessageEvent, ChatAIMessageState> {
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() {
|
||||
_subscription?.cancel();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
StreamSubscription<String>? _subscription;
|
||||
final String chatId;
|
||||
final Int64? questionId;
|
||||
}
|
||||
|
@ -586,7 +586,7 @@ class AnswerStream {
|
||||
_port.close();
|
||||
}
|
||||
|
||||
StreamSubscription<String> listen({
|
||||
void listen({
|
||||
void Function(String text)? onData,
|
||||
void Function()? onStart,
|
||||
void Function()? onEnd,
|
||||
@ -602,7 +602,5 @@ class AnswerStream {
|
||||
if (_onStart != null) {
|
||||
_onStart!();
|
||||
}
|
||||
|
||||
return _subscription;
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,5 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:appflowy/workspace/application/settings/ai/local_llm_listener.dart';
|
||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
@ -13,6 +15,11 @@ class ChatFileBloc extends Bloc<ChatFileEvent, ChatFileState> {
|
||||
}) : listener = LocalLLMListener(),
|
||||
super(const ChatFileState()) {
|
||||
listener.start(
|
||||
stateCallback: (pluginState) {
|
||||
if (!isClosed) {
|
||||
add(ChatFileEvent.updatePluginState(pluginState));
|
||||
}
|
||||
},
|
||||
chatStateCallback: (chatState) {
|
||||
if (!isClosed) {
|
||||
add(ChatFileEvent.updateChatState(chatState));
|
||||
@ -38,18 +45,55 @@ class ChatFileBloc extends Bloc<ChatFileEvent, ChatFileState> {
|
||||
},
|
||||
);
|
||||
},
|
||||
newFile: (String filePath) {
|
||||
newFile: (String filePath, String fileName) async {
|
||||
emit(
|
||||
state.copyWith(
|
||||
indexFileIndicator: IndexFileIndicator.indexing(fileName),
|
||||
),
|
||||
);
|
||||
final payload = ChatFilePB(filePath: filePath, chatId: chatId);
|
||||
ChatEventChatWithFile(payload).send();
|
||||
unawaited(
|
||||
ChatEventChatWithFile(payload).send().then((result) {
|
||||
if (!isClosed) {
|
||||
result.fold((_) {
|
||||
add(
|
||||
ChatFileEvent.updateIndexFile(
|
||||
IndexFileIndicator.finish(fileName),
|
||||
),
|
||||
);
|
||||
}, (err) {
|
||||
add(
|
||||
ChatFileEvent.updateIndexFile(
|
||||
IndexFileIndicator.error(err.msg),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}),
|
||||
);
|
||||
},
|
||||
updateChatState: (LocalAIChatPB chatState) {
|
||||
// Only user enable chat with file and the plugin is already running
|
||||
final supportChatWithFile = chatState.fileEnabled &&
|
||||
chatState.pluginState.state == RunningStatePB.Running;
|
||||
emit(
|
||||
state.copyWith(supportChatWithFile: supportChatWithFile),
|
||||
state.copyWith(
|
||||
supportChatWithFile: supportChatWithFile,
|
||||
chatState: chatState,
|
||||
),
|
||||
);
|
||||
},
|
||||
updateIndexFile: (IndexFileIndicator indicator) {
|
||||
emit(
|
||||
state.copyWith(indexFileIndicator: indicator),
|
||||
);
|
||||
},
|
||||
updatePluginState: (LocalAIPluginStatePB chatState) {
|
||||
final fileEnabled = state.chatState?.fileEnabled ?? false;
|
||||
final supportChatWithFile =
|
||||
fileEnabled && chatState.state == RunningStatePB.Running;
|
||||
emit(state.copyWith(supportChatWithFile: supportChatWithFile));
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
@ -67,20 +111,29 @@ class ChatFileBloc extends Bloc<ChatFileEvent, ChatFileState> {
|
||||
@freezed
|
||||
class ChatFileEvent with _$ChatFileEvent {
|
||||
const factory ChatFileEvent.initial() = Initial;
|
||||
const factory ChatFileEvent.newFile(String filePath) = _NewFile;
|
||||
const factory ChatFileEvent.newFile(String filePath, String fileName) =
|
||||
_NewFile;
|
||||
const factory ChatFileEvent.updateChatState(LocalAIChatPB chatState) =
|
||||
_UpdateChatState;
|
||||
const factory ChatFileEvent.updatePluginState(
|
||||
LocalAIPluginStatePB chatState,
|
||||
) = _UpdatePluginState;
|
||||
const factory ChatFileEvent.updateIndexFile(IndexFileIndicator indicator) =
|
||||
_UpdateIndexFile;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class ChatFileState with _$ChatFileState {
|
||||
const factory ChatFileState({
|
||||
@Default(false) bool supportChatWithFile,
|
||||
IndexFileIndicator? indexFileIndicator,
|
||||
LocalAIChatPB? chatState,
|
||||
}) = _ChatFileState;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class LocalAIChatFileIndicator with _$LocalAIChatFileIndicator {
|
||||
const factory LocalAIChatFileIndicator.ready(bool isEnabled) = _Ready;
|
||||
const factory LocalAIChatFileIndicator.loading() = _Loading;
|
||||
class IndexFileIndicator with _$IndexFileIndicator {
|
||||
const factory IndexFileIndicator.finish(String fileName) = _Finish;
|
||||
const factory IndexFileIndicator.indexing(String fileName) = _Indexing;
|
||||
const factory IndexFileIndicator.error(String error) = _Error;
|
||||
}
|
||||
|
@ -1,5 +1,7 @@
|
||||
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/workspace/presentation/home/menu/sidebar/space/shared_widget.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:desktop_drop/desktop_drop.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
@ -71,7 +73,8 @@ class AIChatPage extends StatelessWidget {
|
||||
return MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider(
|
||||
create: (_) => ChatFileBloc(chatId: view.id.toString()),
|
||||
create: (_) => ChatFileBloc(chatId: view.id.toString())
|
||||
..add(const ChatFileEvent.initial()),
|
||||
),
|
||||
BlocProvider(
|
||||
create: (_) => ChatBloc(
|
||||
@ -81,28 +84,40 @@ class AIChatPage extends StatelessWidget {
|
||||
),
|
||||
BlocProvider(create: (_) => ChatInputBloc()),
|
||||
],
|
||||
child: BlocListener<ChatFileBloc, ChatFileState>(
|
||||
listenWhen: (previous, current) =>
|
||||
previous.indexFileIndicator != current.indexFileIndicator,
|
||||
listener: (context, state) {
|
||||
_handleIndexIndicator(state.indexFileIndicator, context);
|
||||
},
|
||||
child: BlocBuilder<ChatFileBloc, ChatFileState>(
|
||||
builder: (context, state) {
|
||||
Widget child = _ChatContentPage(
|
||||
view: view,
|
||||
userProfile: userProfile,
|
||||
);
|
||||
|
||||
// If the chat supports file upload, wrap the chat content with a drop target
|
||||
if (state.supportChatWithFile) {
|
||||
child = DropTarget(
|
||||
return DropTarget(
|
||||
onDragDone: (DropDoneDetails detail) async {
|
||||
if (state.supportChatWithFile) {
|
||||
await showConfirmDialog(
|
||||
context: context,
|
||||
style: ConfirmPopupStyle.cancelAndOk,
|
||||
title: LocaleKeys.chat_chatWithFilePrompt.tr(),
|
||||
confirmLabel: LocaleKeys.button_confirm.tr(),
|
||||
onConfirm: () {
|
||||
for (final file in detail.files) {
|
||||
context
|
||||
.read<ChatFileBloc>()
|
||||
.add(ChatFileEvent.newFile(file.path));
|
||||
.add(ChatFileEvent.newFile(file.path, file.name));
|
||||
}
|
||||
},
|
||||
child: child,
|
||||
description: '',
|
||||
);
|
||||
}
|
||||
return child;
|
||||
},
|
||||
child: _ChatContentPage(
|
||||
view: view,
|
||||
userProfile: userProfile,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -114,6 +129,35 @@ class AIChatPage extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _handleIndexIndicator(
|
||||
IndexFileIndicator? indicator,
|
||||
BuildContext context,
|
||||
) {
|
||||
if (indicator != null) {
|
||||
indicator.when(
|
||||
finish: (fileName) {
|
||||
showSnackBarMessage(
|
||||
context,
|
||||
LocaleKeys.chat_indexFileSuccess.tr(args: [fileName]),
|
||||
);
|
||||
},
|
||||
indexing: (fileName) {
|
||||
showSnackBarMessage(
|
||||
context,
|
||||
LocaleKeys.chat_indexingFile.tr(args: [fileName]),
|
||||
duration: const Duration(seconds: 2),
|
||||
);
|
||||
},
|
||||
error: (err) {
|
||||
showSnackBarMessage(
|
||||
context,
|
||||
err,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _ChatContentPage extends StatefulWidget {
|
||||
|
@ -67,8 +67,7 @@ class _ChatInputState extends State<ChatInput> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_textController =
|
||||
widget.options.textEditingController ?? InputTextFieldController();
|
||||
_textController = InputTextFieldController();
|
||||
_handleSendButtonVisibilityModeChange();
|
||||
}
|
||||
|
||||
@ -85,11 +84,9 @@ class _ChatInputState extends State<ChatInput> {
|
||||
final partialText = types.PartialText(text: trimmedText);
|
||||
widget.onSendPressed(partialText);
|
||||
|
||||
if (widget.options.inputClearMode == InputClearMode.always) {
|
||||
_textController.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _handleTextControllerChange() {
|
||||
if (_textController.value.isComposingRangeValid) {
|
||||
@ -106,7 +103,6 @@ class _ChatInputState extends State<ChatInput> {
|
||||
const inputPadding = EdgeInsets.all(6);
|
||||
|
||||
return Focus(
|
||||
autofocus: !widget.options.autofocus,
|
||||
child: Padding(
|
||||
padding: inputPadding,
|
||||
child: Material(
|
||||
@ -148,15 +144,11 @@ class _ChatInputState extends State<ChatInput> {
|
||||
style: TextStyle(
|
||||
color: AFThemeExtension.of(context).textColor,
|
||||
),
|
||||
autocorrect: widget.options.autocorrect,
|
||||
autofocus: widget.options.autofocus,
|
||||
enableSuggestions: widget.options.enableSuggestions,
|
||||
keyboardType: widget.options.keyboardType,
|
||||
keyboardType: TextInputType.multiline,
|
||||
textCapitalization: TextCapitalization.sentences,
|
||||
maxLines: 10,
|
||||
minLines: 1,
|
||||
onChanged: widget.options.onTextChanged,
|
||||
onTap: widget.options.onTextFieldTap,
|
||||
onChanged: (_) {},
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -207,53 +199,6 @@ class _ChatInputState extends State<ChatInput> {
|
||||
);
|
||||
}
|
||||
|
||||
@immutable
|
||||
class InputOptions {
|
||||
const InputOptions({
|
||||
this.inputClearMode = InputClearMode.always,
|
||||
this.keyboardType = TextInputType.multiline,
|
||||
this.onTextChanged,
|
||||
this.onTextFieldTap,
|
||||
this.textEditingController,
|
||||
this.autocorrect = true,
|
||||
this.autofocus = false,
|
||||
this.enableSuggestions = true,
|
||||
this.enabled = true,
|
||||
});
|
||||
|
||||
/// Controls the [ChatInput] clear behavior. Defaults to [InputClearMode.always].
|
||||
final InputClearMode inputClearMode;
|
||||
|
||||
/// Controls the [ChatInput] keyboard type. Defaults to [TextInputType.multiline].
|
||||
final TextInputType keyboardType;
|
||||
|
||||
/// Will be called whenever the text inside [TextField] changes.
|
||||
final void Function(String)? onTextChanged;
|
||||
|
||||
/// Will be called on [TextField] tap.
|
||||
final VoidCallback? onTextFieldTap;
|
||||
|
||||
/// Custom [TextEditingController]. If not provided, defaults to the
|
||||
/// [InputTextFieldController], which extends [TextEditingController] and has
|
||||
/// additional fatures like markdown support. If you want to keep additional
|
||||
/// features but still need some methods from the default [TextEditingController],
|
||||
/// you can create your own [InputTextFieldController] (imported from this lib)
|
||||
/// and pass it here.
|
||||
final TextEditingController? textEditingController;
|
||||
|
||||
/// Controls the [TextInput] autocorrect behavior. Defaults to [true].
|
||||
final bool autocorrect;
|
||||
|
||||
/// Whether [TextInput] should have focus. Defaults to [false].
|
||||
final bool autofocus;
|
||||
|
||||
/// Controls the [TextInput] enableSuggestions behavior. Defaults to [true].
|
||||
final bool enableSuggestions;
|
||||
|
||||
/// Controls the [TextInput] enabled behavior. Defaults to [true].
|
||||
final bool enabled;
|
||||
}
|
||||
|
||||
final isMobile = defaultTargetPlatform == TargetPlatform.android ||
|
||||
defaultTargetPlatform == TargetPlatform.iOS;
|
||||
|
||||
|
@ -6,7 +6,6 @@ import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
|
||||
class RelatedQuestionList extends StatelessWidget {
|
||||
const RelatedQuestionList({
|
||||
required this.chatId,
|
||||
@ -97,6 +96,7 @@ class _RelatedQuestionItemState extends State<RelatedQuestionItem> {
|
||||
style: TextStyle(
|
||||
color: _isHovered ? Theme.of(context).colorScheme.primary : null,
|
||||
fontSize: 14,
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
|
@ -4,6 +4,7 @@ import 'package:appflowy/core/config/kv.dart';
|
||||
import 'package:appflowy/core/config/kv_keys.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
typedef FeatureFlagMap = Map<FeatureFlag, bool>;
|
||||
|
||||
@ -91,7 +92,7 @@ enum FeatureFlag {
|
||||
|
||||
bool get isOn {
|
||||
if ([
|
||||
// if (kDebugMode) FeatureFlag.planBilling,
|
||||
if (kDebugMode) FeatureFlag.planBilling,
|
||||
// release this feature in version 0.6.1
|
||||
FeatureFlag.spaceDesign,
|
||||
// release this feature in version 0.5.9
|
||||
|
@ -7,6 +7,7 @@ import 'package:appflowy/workspace/presentation/settings/pages/setting_ai_view/m
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
@ -128,7 +129,7 @@ class _LocalAIOnBoarding extends StatelessWidget {
|
||||
child: BlocBuilder<LocalAIOnBoardingBloc, LocalAIOnBoardingState>(
|
||||
builder: (context, state) {
|
||||
// Show the local AI settings if the user has purchased the AI Local plan
|
||||
if (state.isPurchaseAILocal) {
|
||||
if (kDebugMode || state.isPurchaseAILocal) {
|
||||
return const LocalAISetting();
|
||||
} else {
|
||||
// Show the upgrade to AI Local plan button if the user has not purchased the AI Local plan
|
||||
|
@ -1,3 +1,5 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/util/int64_extension.dart';
|
||||
@ -209,6 +211,10 @@ class _SettingsBillingViewState extends State<SettingsBillingView> {
|
||||
),
|
||||
),
|
||||
const SettingsDashedDivider(),
|
||||
|
||||
// Currently, the AI Local tile is only available on macOS
|
||||
// TODO(nathan): enable windows and linux
|
||||
if (Platform.isMacOS)
|
||||
_AITile(
|
||||
plan: SubscriptionPlanPB.AiLocal,
|
||||
label: LocaleKeys
|
||||
|
@ -1,3 +1,5 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
@ -136,6 +138,10 @@ class _SettingsPlanViewState extends State<SettingsPlanView> {
|
||||
),
|
||||
),
|
||||
const HSpace(8),
|
||||
|
||||
// Currently, the AI Local tile is only available on macOS
|
||||
// TODO(nathan): enable windows and linux
|
||||
if (Platform.isMacOS)
|
||||
Flexible(
|
||||
child: _AddOnBox(
|
||||
title: LocaleKeys
|
||||
@ -147,7 +153,9 @@ class _SettingsPlanViewState extends State<SettingsPlanView> {
|
||||
price: LocaleKeys
|
||||
.settings_planPage_planUsage_addons_aiOnDevice_price
|
||||
.tr(
|
||||
args: [SubscriptionPlanPB.AiLocal.priceAnnualBilling],
|
||||
args: [
|
||||
SubscriptionPlanPB.AiLocal.priceAnnualBilling,
|
||||
],
|
||||
),
|
||||
priceInfo: LocaleKeys
|
||||
.settings_planPage_planUsage_addons_aiOnDevice_priceInfo
|
||||
@ -155,7 +163,9 @@ class _SettingsPlanViewState extends State<SettingsPlanView> {
|
||||
billingInfo: LocaleKeys
|
||||
.settings_planPage_planUsage_addons_aiOnDevice_billingInfo
|
||||
.tr(
|
||||
args: [SubscriptionPlanPB.AiLocal.priceMonthBilling],
|
||||
args: [
|
||||
SubscriptionPlanPB.AiLocal.priceMonthBilling,
|
||||
],
|
||||
),
|
||||
buttonText: state.subscriptionInfo.hasAIOnDevice
|
||||
? LocaleKeys
|
||||
|
@ -169,7 +169,10 @@
|
||||
"question2": "Explain the GTD method",
|
||||
"question3": "Why use Rust",
|
||||
"question4": "Recipe with what's in my kitchen",
|
||||
"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?",
|
||||
"indexFileSuccess": "Indexing file successfully",
|
||||
"indexingFile": "Indexing {}"
|
||||
},
|
||||
"trash": {
|
||||
"text": "Trash",
|
||||
|
@ -12,7 +12,7 @@ use crate::entities::*;
|
||||
use crate::local_ai::local_llm_chat::LLMModelInfo;
|
||||
use crate::notification::{make_notification, ChatNotification, APPFLOWY_AI_NOTIFICATION_KEY};
|
||||
use crate::tools::AITools;
|
||||
use flowy_error::{FlowyError, FlowyResult};
|
||||
use flowy_error::{ErrorCode, FlowyError, FlowyResult};
|
||||
use lib_dispatch::prelude::{data_result_ok, AFPluginData, AFPluginState, DataResult};
|
||||
use lib_infra::isolate_stream::IsolateSink;
|
||||
|
||||
@ -208,6 +208,25 @@ pub(crate) async fn chat_file_handler(
|
||||
) -> Result<(), FlowyError> {
|
||||
let data = data.try_into_inner()?;
|
||||
let file_path = PathBuf::from(&data.file_path);
|
||||
|
||||
let allowed_extensions = ["pdf", "md", "txt"];
|
||||
let extension = file_path
|
||||
.extension()
|
||||
.and_then(|ext| ext.to_str())
|
||||
.ok_or_else(|| {
|
||||
FlowyError::new(
|
||||
ErrorCode::UnsupportedFileFormat,
|
||||
"Can't find file extension",
|
||||
)
|
||||
})?;
|
||||
|
||||
if !allowed_extensions.contains(&extension) {
|
||||
return Err(FlowyError::new(
|
||||
ErrorCode::UnsupportedFileFormat,
|
||||
"Only support pdf,md and txt",
|
||||
));
|
||||
}
|
||||
|
||||
let (tx, rx) = oneshot::channel::<Result<(), FlowyError>>();
|
||||
tokio::spawn(async move {
|
||||
let chat_manager = upgrade_chat_manager(chat_manager)?;
|
||||
|
@ -150,7 +150,8 @@ impl LocalAIController {
|
||||
pub fn is_rag_enabled(&self) -> bool {
|
||||
self
|
||||
.store_preferences
|
||||
.get_bool_or_default(APPFLOWY_LOCAL_AI_CHAT_RAG_ENABLED)
|
||||
.get_bool(APPFLOWY_LOCAL_AI_CHAT_RAG_ENABLED)
|
||||
.unwrap_or(true)
|
||||
}
|
||||
|
||||
pub fn open_chat(&self, chat_id: &str) {
|
||||
|
@ -151,7 +151,6 @@ impl DocumentManager {
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "info", skip(self), err)]
|
||||
pub async fn get_document(&self, doc_id: &str) -> FlowyResult<Arc<MutexDocument>> {
|
||||
if let Some(doc) = self.documents.get(doc_id).map(|item| item.value().clone()) {
|
||||
return Ok(doc);
|
||||
@ -160,7 +159,7 @@ impl DocumentManager {
|
||||
if let Some(doc) = self.restore_document_from_removing(doc_id) {
|
||||
return Ok(doc);
|
||||
}
|
||||
return Err(FlowyError::internal().with_context("Call open document first"));
|
||||
Err(FlowyError::internal().with_context("Call open document first"))
|
||||
}
|
||||
|
||||
/// Returns Document for given object id
|
||||
|
@ -298,6 +298,9 @@ pub enum ErrorCode {
|
||||
|
||||
#[error("Response timeout")]
|
||||
ResponseTimeout = 103,
|
||||
|
||||
#[error("Unsupported file format")]
|
||||
UnsupportedFileFormat = 104,
|
||||
}
|
||||
|
||||
impl ErrorCode {
|
||||
|
Loading…
Reference in New Issue
Block a user