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,
|
required this.questionId,
|
||||||
}) : super(ChatAIMessageState.initial(message)) {
|
}) : super(ChatAIMessageState.initial(message)) {
|
||||||
if (state.stream != null) {
|
if (state.stream != null) {
|
||||||
_subscription = state.stream!.listen(
|
state.stream!.listen(
|
||||||
onData: (text) {
|
onData: (text) {
|
||||||
if (!isClosed) {
|
if (!isClosed) {
|
||||||
add(ChatAIMessageEvent.updateText(text));
|
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 String chatId;
|
||||||
final Int64? questionId;
|
final Int64? questionId;
|
||||||
}
|
}
|
||||||
|
@ -586,7 +586,7 @@ class AnswerStream {
|
|||||||
_port.close();
|
_port.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
StreamSubscription<String> listen({
|
void listen({
|
||||||
void Function(String text)? onData,
|
void Function(String text)? onData,
|
||||||
void Function()? onStart,
|
void Function()? onStart,
|
||||||
void Function()? onEnd,
|
void Function()? onEnd,
|
||||||
@ -602,7 +602,5 @@ class AnswerStream {
|
|||||||
if (_onStart != null) {
|
if (_onStart != null) {
|
||||||
_onStart!();
|
_onStart!();
|
||||||
}
|
}
|
||||||
|
|
||||||
return _subscription;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:appflowy/workspace/application/settings/ai/local_llm_listener.dart';
|
import 'package:appflowy/workspace/application/settings/ai/local_llm_listener.dart';
|
||||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||||
import 'package:appflowy_backend/log.dart';
|
import 'package:appflowy_backend/log.dart';
|
||||||
@ -13,6 +15,11 @@ class ChatFileBloc extends Bloc<ChatFileEvent, ChatFileState> {
|
|||||||
}) : listener = LocalLLMListener(),
|
}) : listener = LocalLLMListener(),
|
||||||
super(const ChatFileState()) {
|
super(const ChatFileState()) {
|
||||||
listener.start(
|
listener.start(
|
||||||
|
stateCallback: (pluginState) {
|
||||||
|
if (!isClosed) {
|
||||||
|
add(ChatFileEvent.updatePluginState(pluginState));
|
||||||
|
}
|
||||||
|
},
|
||||||
chatStateCallback: (chatState) {
|
chatStateCallback: (chatState) {
|
||||||
if (!isClosed) {
|
if (!isClosed) {
|
||||||
add(ChatFileEvent.updateChatState(chatState));
|
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);
|
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) {
|
updateChatState: (LocalAIChatPB chatState) {
|
||||||
// Only user enable chat with file and the plugin is already running
|
// Only user enable chat with file and the plugin is already running
|
||||||
final supportChatWithFile = chatState.fileEnabled &&
|
final supportChatWithFile = chatState.fileEnabled &&
|
||||||
chatState.pluginState.state == RunningStatePB.Running;
|
chatState.pluginState.state == RunningStatePB.Running;
|
||||||
emit(
|
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
|
@freezed
|
||||||
class ChatFileEvent with _$ChatFileEvent {
|
class ChatFileEvent with _$ChatFileEvent {
|
||||||
const factory ChatFileEvent.initial() = Initial;
|
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) =
|
const factory ChatFileEvent.updateChatState(LocalAIChatPB chatState) =
|
||||||
_UpdateChatState;
|
_UpdateChatState;
|
||||||
|
const factory ChatFileEvent.updatePluginState(
|
||||||
|
LocalAIPluginStatePB chatState,
|
||||||
|
) = _UpdatePluginState;
|
||||||
|
const factory ChatFileEvent.updateIndexFile(IndexFileIndicator indicator) =
|
||||||
|
_UpdateIndexFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
@freezed
|
@freezed
|
||||||
class ChatFileState with _$ChatFileState {
|
class ChatFileState with _$ChatFileState {
|
||||||
const factory ChatFileState({
|
const factory ChatFileState({
|
||||||
@Default(false) bool supportChatWithFile,
|
@Default(false) bool supportChatWithFile,
|
||||||
|
IndexFileIndicator? indexFileIndicator,
|
||||||
|
LocalAIChatPB? chatState,
|
||||||
}) = _ChatFileState;
|
}) = _ChatFileState;
|
||||||
}
|
}
|
||||||
|
|
||||||
@freezed
|
@freezed
|
||||||
class LocalAIChatFileIndicator with _$LocalAIChatFileIndicator {
|
class IndexFileIndicator with _$IndexFileIndicator {
|
||||||
const factory LocalAIChatFileIndicator.ready(bool isEnabled) = _Ready;
|
const factory IndexFileIndicator.finish(String fileName) = _Finish;
|
||||||
const factory LocalAIChatFileIndicator.loading() = _Loading;
|
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_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/widgets/dialogs.dart';
|
||||||
import 'package:desktop_drop/desktop_drop.dart';
|
import 'package:desktop_drop/desktop_drop.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
@ -71,7 +73,8 @@ class AIChatPage extends StatelessWidget {
|
|||||||
return MultiBlocProvider(
|
return MultiBlocProvider(
|
||||||
providers: [
|
providers: [
|
||||||
BlocProvider(
|
BlocProvider(
|
||||||
create: (_) => ChatFileBloc(chatId: view.id.toString()),
|
create: (_) => ChatFileBloc(chatId: view.id.toString())
|
||||||
|
..add(const ChatFileEvent.initial()),
|
||||||
),
|
),
|
||||||
BlocProvider(
|
BlocProvider(
|
||||||
create: (_) => ChatBloc(
|
create: (_) => ChatBloc(
|
||||||
@ -81,28 +84,40 @@ class AIChatPage extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
BlocProvider(create: (_) => ChatInputBloc()),
|
BlocProvider(create: (_) => ChatInputBloc()),
|
||||||
],
|
],
|
||||||
child: BlocBuilder<ChatFileBloc, ChatFileState>(
|
child: BlocListener<ChatFileBloc, ChatFileState>(
|
||||||
builder: (context, state) {
|
listenWhen: (previous, current) =>
|
||||||
Widget child = _ChatContentPage(
|
previous.indexFileIndicator != current.indexFileIndicator,
|
||||||
view: view,
|
listener: (context, state) {
|
||||||
userProfile: userProfile,
|
_handleIndexIndicator(state.indexFileIndicator, context);
|
||||||
);
|
},
|
||||||
|
child: BlocBuilder<ChatFileBloc, ChatFileState>(
|
||||||
// If the chat supports file upload, wrap the chat content with a drop target
|
builder: (context, state) {
|
||||||
if (state.supportChatWithFile) {
|
return DropTarget(
|
||||||
child = DropTarget(
|
|
||||||
onDragDone: (DropDoneDetails detail) async {
|
onDragDone: (DropDoneDetails detail) async {
|
||||||
for (final file in detail.files) {
|
if (state.supportChatWithFile) {
|
||||||
context
|
await showConfirmDialog(
|
||||||
.read<ChatFileBloc>()
|
context: context,
|
||||||
.add(ChatFileEvent.newFile(file.path));
|
style: ConfirmPopupStyle.cancelAndOk,
|
||||||
|
title: LocaleKeys.chat_chatWithFilePrompt.tr(),
|
||||||
|
confirmLabel: LocaleKeys.button_confirm.tr(),
|
||||||
|
onConfirm: () {
|
||||||
|
for (final file in detail.files) {
|
||||||
|
context
|
||||||
|
.read<ChatFileBloc>()
|
||||||
|
.add(ChatFileEvent.newFile(file.path, file.name));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
description: '',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: child,
|
child: _ChatContentPage(
|
||||||
|
view: view,
|
||||||
|
userProfile: userProfile,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
return child;
|
),
|
||||||
},
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -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 {
|
class _ChatContentPage extends StatefulWidget {
|
||||||
|
@ -67,8 +67,7 @@ class _ChatInputState extends State<ChatInput> {
|
|||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
_textController =
|
_textController = InputTextFieldController();
|
||||||
widget.options.textEditingController ?? InputTextFieldController();
|
|
||||||
_handleSendButtonVisibilityModeChange();
|
_handleSendButtonVisibilityModeChange();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -85,9 +84,7 @@ class _ChatInputState extends State<ChatInput> {
|
|||||||
final partialText = types.PartialText(text: trimmedText);
|
final partialText = types.PartialText(text: trimmedText);
|
||||||
widget.onSendPressed(partialText);
|
widget.onSendPressed(partialText);
|
||||||
|
|
||||||
if (widget.options.inputClearMode == InputClearMode.always) {
|
_textController.clear();
|
||||||
_textController.clear();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -106,7 +103,6 @@ class _ChatInputState extends State<ChatInput> {
|
|||||||
const inputPadding = EdgeInsets.all(6);
|
const inputPadding = EdgeInsets.all(6);
|
||||||
|
|
||||||
return Focus(
|
return Focus(
|
||||||
autofocus: !widget.options.autofocus,
|
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: inputPadding,
|
padding: inputPadding,
|
||||||
child: Material(
|
child: Material(
|
||||||
@ -148,15 +144,11 @@ class _ChatInputState extends State<ChatInput> {
|
|||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: AFThemeExtension.of(context).textColor,
|
color: AFThemeExtension.of(context).textColor,
|
||||||
),
|
),
|
||||||
autocorrect: widget.options.autocorrect,
|
keyboardType: TextInputType.multiline,
|
||||||
autofocus: widget.options.autofocus,
|
|
||||||
enableSuggestions: widget.options.enableSuggestions,
|
|
||||||
keyboardType: widget.options.keyboardType,
|
|
||||||
textCapitalization: TextCapitalization.sentences,
|
textCapitalization: TextCapitalization.sentences,
|
||||||
maxLines: 10,
|
maxLines: 10,
|
||||||
minLines: 1,
|
minLines: 1,
|
||||||
onChanged: widget.options.onTextChanged,
|
onChanged: (_) {},
|
||||||
onTap: widget.options.onTextFieldTap,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -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 ||
|
final isMobile = defaultTargetPlatform == TargetPlatform.android ||
|
||||||
defaultTargetPlatform == TargetPlatform.iOS;
|
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:flowy_infra_ui/widget/spacing.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
|
||||||
class RelatedQuestionList extends StatelessWidget {
|
class RelatedQuestionList extends StatelessWidget {
|
||||||
const RelatedQuestionList({
|
const RelatedQuestionList({
|
||||||
required this.chatId,
|
required this.chatId,
|
||||||
@ -97,6 +96,7 @@ class _RelatedQuestionItemState extends State<RelatedQuestionItem> {
|
|||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: _isHovered ? Theme.of(context).colorScheme.primary : null,
|
color: _isHovered ? Theme.of(context).colorScheme.primary : null,
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
|
height: 1.5,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
|
@ -4,6 +4,7 @@ import 'package:appflowy/core/config/kv.dart';
|
|||||||
import 'package:appflowy/core/config/kv_keys.dart';
|
import 'package:appflowy/core/config/kv_keys.dart';
|
||||||
import 'package:appflowy/startup/startup.dart';
|
import 'package:appflowy/startup/startup.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
typedef FeatureFlagMap = Map<FeatureFlag, bool>;
|
typedef FeatureFlagMap = Map<FeatureFlag, bool>;
|
||||||
|
|
||||||
@ -91,7 +92,7 @@ enum FeatureFlag {
|
|||||||
|
|
||||||
bool get isOn {
|
bool get isOn {
|
||||||
if ([
|
if ([
|
||||||
// if (kDebugMode) FeatureFlag.planBilling,
|
if (kDebugMode) FeatureFlag.planBilling,
|
||||||
// release this feature in version 0.6.1
|
// release this feature in version 0.6.1
|
||||||
FeatureFlag.spaceDesign,
|
FeatureFlag.spaceDesign,
|
||||||
// release this feature in version 0.5.9
|
// 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:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.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:flowy_infra_ui/widget/spacing.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
@ -128,7 +129,7 @@ class _LocalAIOnBoarding extends StatelessWidget {
|
|||||||
child: BlocBuilder<LocalAIOnBoardingBloc, LocalAIOnBoardingState>(
|
child: BlocBuilder<LocalAIOnBoardingBloc, LocalAIOnBoardingState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
// Show the local AI settings if the user has purchased the AI Local plan
|
// Show the local AI settings if the user has purchased the AI Local plan
|
||||||
if (state.isPurchaseAILocal) {
|
if (kDebugMode || state.isPurchaseAILocal) {
|
||||||
return const LocalAISetting();
|
return const LocalAISetting();
|
||||||
} else {
|
} else {
|
||||||
// Show the upgrade to AI Local plan button if the user has not purchased the AI Local plan
|
// 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:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:appflowy/util/int64_extension.dart';
|
import 'package:appflowy/util/int64_extension.dart';
|
||||||
@ -209,22 +211,26 @@ class _SettingsBillingViewState extends State<SettingsBillingView> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SettingsDashedDivider(),
|
const SettingsDashedDivider(),
|
||||||
_AITile(
|
|
||||||
plan: SubscriptionPlanPB.AiLocal,
|
// Currently, the AI Local tile is only available on macOS
|
||||||
label: LocaleKeys
|
// TODO(nathan): enable windows and linux
|
||||||
.settings_billingPage_addons_aiOnDevice_label
|
if (Platform.isMacOS)
|
||||||
.tr(),
|
_AITile(
|
||||||
description: LocaleKeys
|
plan: SubscriptionPlanPB.AiLocal,
|
||||||
.settings_billingPage_addons_aiOnDevice_description,
|
label: LocaleKeys
|
||||||
activeDescription: LocaleKeys
|
.settings_billingPage_addons_aiOnDevice_label
|
||||||
.settings_billingPage_addons_aiOnDevice_activeDescription,
|
.tr(),
|
||||||
canceledDescription: LocaleKeys
|
description: LocaleKeys
|
||||||
.settings_billingPage_addons_aiOnDevice_canceledDescription,
|
.settings_billingPage_addons_aiOnDevice_description,
|
||||||
subscriptionInfo:
|
activeDescription: LocaleKeys
|
||||||
state.subscriptionInfo.addOns.firstWhereOrNull(
|
.settings_billingPage_addons_aiOnDevice_activeDescription,
|
||||||
(a) => a.type == WorkspaceAddOnPBType.AddOnAiLocal,
|
canceledDescription: LocaleKeys
|
||||||
|
.settings_billingPage_addons_aiOnDevice_canceledDescription,
|
||||||
|
subscriptionInfo:
|
||||||
|
state.subscriptionInfo.addOns.firstWhereOrNull(
|
||||||
|
(a) => a.type == WorkspaceAddOnPBType.AddOnAiLocal,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||||
@ -136,38 +138,46 @@ class _SettingsPlanViewState extends State<SettingsPlanView> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const HSpace(8),
|
const HSpace(8),
|
||||||
Flexible(
|
|
||||||
child: _AddOnBox(
|
// Currently, the AI Local tile is only available on macOS
|
||||||
title: LocaleKeys
|
// TODO(nathan): enable windows and linux
|
||||||
.settings_planPage_planUsage_addons_aiOnDevice_title
|
if (Platform.isMacOS)
|
||||||
.tr(),
|
Flexible(
|
||||||
description: LocaleKeys
|
child: _AddOnBox(
|
||||||
.settings_planPage_planUsage_addons_aiOnDevice_description
|
title: LocaleKeys
|
||||||
.tr(),
|
.settings_planPage_planUsage_addons_aiOnDevice_title
|
||||||
price: LocaleKeys
|
.tr(),
|
||||||
.settings_planPage_planUsage_addons_aiOnDevice_price
|
description: LocaleKeys
|
||||||
.tr(
|
.settings_planPage_planUsage_addons_aiOnDevice_description
|
||||||
args: [SubscriptionPlanPB.AiLocal.priceAnnualBilling],
|
.tr(),
|
||||||
|
price: LocaleKeys
|
||||||
|
.settings_planPage_planUsage_addons_aiOnDevice_price
|
||||||
|
.tr(
|
||||||
|
args: [
|
||||||
|
SubscriptionPlanPB.AiLocal.priceAnnualBilling,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
priceInfo: LocaleKeys
|
||||||
|
.settings_planPage_planUsage_addons_aiOnDevice_priceInfo
|
||||||
|
.tr(),
|
||||||
|
billingInfo: LocaleKeys
|
||||||
|
.settings_planPage_planUsage_addons_aiOnDevice_billingInfo
|
||||||
|
.tr(
|
||||||
|
args: [
|
||||||
|
SubscriptionPlanPB.AiLocal.priceMonthBilling,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
buttonText: state.subscriptionInfo.hasAIOnDevice
|
||||||
|
? LocaleKeys
|
||||||
|
.settings_planPage_planUsage_addons_activeLabel
|
||||||
|
.tr()
|
||||||
|
: LocaleKeys
|
||||||
|
.settings_planPage_planUsage_addons_addLabel
|
||||||
|
.tr(),
|
||||||
|
isActive: state.subscriptionInfo.hasAIOnDevice,
|
||||||
|
plan: SubscriptionPlanPB.AiLocal,
|
||||||
),
|
),
|
||||||
priceInfo: LocaleKeys
|
|
||||||
.settings_planPage_planUsage_addons_aiOnDevice_priceInfo
|
|
||||||
.tr(),
|
|
||||||
billingInfo: LocaleKeys
|
|
||||||
.settings_planPage_planUsage_addons_aiOnDevice_billingInfo
|
|
||||||
.tr(
|
|
||||||
args: [SubscriptionPlanPB.AiLocal.priceMonthBilling],
|
|
||||||
),
|
|
||||||
buttonText: state.subscriptionInfo.hasAIOnDevice
|
|
||||||
? LocaleKeys
|
|
||||||
.settings_planPage_planUsage_addons_activeLabel
|
|
||||||
.tr()
|
|
||||||
: LocaleKeys
|
|
||||||
.settings_planPage_planUsage_addons_addLabel
|
|
||||||
.tr(),
|
|
||||||
isActive: state.subscriptionInfo.hasAIOnDevice,
|
|
||||||
plan: SubscriptionPlanPB.AiLocal,
|
|
||||||
),
|
),
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -169,7 +169,10 @@
|
|||||||
"question2": "Explain the GTD method",
|
"question2": "Explain the GTD method",
|
||||||
"question3": "Why use Rust",
|
"question3": "Why use Rust",
|
||||||
"question4": "Recipe with what's in my kitchen",
|
"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": {
|
"trash": {
|
||||||
"text": "Trash",
|
"text": "Trash",
|
||||||
|
@ -12,7 +12,7 @@ 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 crate::tools::AITools;
|
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_dispatch::prelude::{data_result_ok, AFPluginData, AFPluginState, DataResult};
|
||||||
use lib_infra::isolate_stream::IsolateSink;
|
use lib_infra::isolate_stream::IsolateSink;
|
||||||
|
|
||||||
@ -208,6 +208,25 @@ pub(crate) async fn chat_file_handler(
|
|||||||
) -> Result<(), FlowyError> {
|
) -> Result<(), FlowyError> {
|
||||||
let data = data.try_into_inner()?;
|
let data = data.try_into_inner()?;
|
||||||
let file_path = PathBuf::from(&data.file_path);
|
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>>();
|
let (tx, rx) = oneshot::channel::<Result<(), FlowyError>>();
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let chat_manager = upgrade_chat_manager(chat_manager)?;
|
let chat_manager = upgrade_chat_manager(chat_manager)?;
|
||||||
|
@ -150,7 +150,8 @@ impl LocalAIController {
|
|||||||
pub fn is_rag_enabled(&self) -> bool {
|
pub fn is_rag_enabled(&self) -> bool {
|
||||||
self
|
self
|
||||||
.store_preferences
|
.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) {
|
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>> {
|
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()) {
|
if let Some(doc) = self.documents.get(doc_id).map(|item| item.value().clone()) {
|
||||||
return Ok(doc);
|
return Ok(doc);
|
||||||
@ -160,7 +159,7 @@ impl DocumentManager {
|
|||||||
if let Some(doc) = self.restore_document_from_removing(doc_id) {
|
if let Some(doc) = self.restore_document_from_removing(doc_id) {
|
||||||
return Ok(doc);
|
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
|
/// Returns Document for given object id
|
||||||
|
@ -298,6 +298,9 @@ pub enum ErrorCode {
|
|||||||
|
|
||||||
#[error("Response timeout")]
|
#[error("Response timeout")]
|
||||||
ResponseTimeout = 103,
|
ResponseTimeout = 103,
|
||||||
|
|
||||||
|
#[error("Unsupported file format")]
|
||||||
|
UnsupportedFileFormat = 104,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ErrorCode {
|
impl ErrorCode {
|
||||||
|
Loading…
Reference in New Issue
Block a user