mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2025-11-16 10:27:40 +00:00
Merge pull request #7617 from richardshiue/chore/improve-model-selection-ui
feat: regenerate message with different model
This commit is contained in:
commit
584f762e11
@ -2,6 +2,8 @@ export 'service/ai_entities.dart';
|
|||||||
export 'service/ai_prompt_input_bloc.dart';
|
export 'service/ai_prompt_input_bloc.dart';
|
||||||
export 'service/appflowy_ai_service.dart';
|
export 'service/appflowy_ai_service.dart';
|
||||||
export 'service/error.dart';
|
export 'service/error.dart';
|
||||||
|
export 'service/ai_model_state_notifier.dart';
|
||||||
|
export 'service/select_model_bloc.dart';
|
||||||
export 'widgets/loading_indicator.dart';
|
export 'widgets/loading_indicator.dart';
|
||||||
export 'widgets/prompt_input/action_buttons.dart';
|
export 'widgets/prompt_input/action_buttons.dart';
|
||||||
export 'widgets/prompt_input/desktop_prompt_text_field.dart';
|
export 'widgets/prompt_input/desktop_prompt_text_field.dart';
|
||||||
@ -13,4 +15,5 @@ export 'widgets/prompt_input/mentioned_page_text_span.dart';
|
|||||||
export 'widgets/prompt_input/predefined_format_buttons.dart';
|
export 'widgets/prompt_input/predefined_format_buttons.dart';
|
||||||
export 'widgets/prompt_input/select_sources_bottom_sheet.dart';
|
export 'widgets/prompt_input/select_sources_bottom_sheet.dart';
|
||||||
export 'widgets/prompt_input/select_sources_menu.dart';
|
export 'widgets/prompt_input/select_sources_menu.dart';
|
||||||
|
export 'widgets/prompt_input/select_model_menu.dart';
|
||||||
export 'widgets/prompt_input/send_button.dart';
|
export 'widgets/prompt_input/send_button.dart';
|
||||||
|
|||||||
@ -4,6 +4,14 @@ import 'package:appflowy_backend/protobuf/flowy-ai/protobuf.dart';
|
|||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
|
|
||||||
|
enum AiType {
|
||||||
|
cloud,
|
||||||
|
local;
|
||||||
|
|
||||||
|
bool get isCloud => this == cloud;
|
||||||
|
bool get isLocal => this == local;
|
||||||
|
}
|
||||||
|
|
||||||
class PredefinedFormat extends Equatable {
|
class PredefinedFormat extends Equatable {
|
||||||
const PredefinedFormat({
|
const PredefinedFormat({
|
||||||
required this.imageFormat,
|
required this.imageFormat,
|
||||||
|
|||||||
@ -1,138 +0,0 @@
|
|||||||
import 'package:appflowy/ai/service/ai_prompt_input_bloc.dart';
|
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
|
||||||
import 'package:appflowy/plugins/ai_chat/application/ai_model_switch_listener.dart';
|
|
||||||
import 'package:appflowy/workspace/application/settings/ai/local_llm_listener.dart';
|
|
||||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
|
||||||
import 'package:appflowy_backend/log.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart';
|
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
|
||||||
import 'package:protobuf/protobuf.dart';
|
|
||||||
import 'package:universal_platform/universal_platform.dart';
|
|
||||||
|
|
||||||
class AIModelStateNotifier {
|
|
||||||
AIModelStateNotifier({required this.objectId})
|
|
||||||
: _isDesktop = UniversalPlatform.isDesktop,
|
|
||||||
_localAIListener =
|
|
||||||
UniversalPlatform.isDesktop ? LocalAIStateListener() : null,
|
|
||||||
_aiModelSwitchListener = AIModelSwitchListener(objectId: objectId);
|
|
||||||
|
|
||||||
final String objectId;
|
|
||||||
final bool _isDesktop;
|
|
||||||
final LocalAIStateListener? _localAIListener;
|
|
||||||
final AIModelSwitchListener _aiModelSwitchListener;
|
|
||||||
|
|
||||||
LocalAIPB? _localAIState;
|
|
||||||
AvailableModelsPB? _availableModels;
|
|
||||||
|
|
||||||
// Callbacks
|
|
||||||
void Function(AiType, bool, String)? onChanged;
|
|
||||||
void Function(AvailableModelsPB)? onAvailableModelsChanged;
|
|
||||||
|
|
||||||
String hintText() {
|
|
||||||
final aiType = getCurrentAiType();
|
|
||||||
if (aiType.isLocal) {
|
|
||||||
return isEditable()
|
|
||||||
? LocaleKeys.chat_inputLocalAIMessageHint.tr()
|
|
||||||
: LocaleKeys.settings_aiPage_keys_localAIInitializing.tr();
|
|
||||||
}
|
|
||||||
return LocaleKeys.chat_inputMessageHint.tr();
|
|
||||||
}
|
|
||||||
|
|
||||||
AiType getCurrentAiType() {
|
|
||||||
// On non-desktop platforms, always return cloud type.
|
|
||||||
if (!_isDesktop) return AiType.cloud;
|
|
||||||
return (_availableModels?.selectedModel.isLocal ?? false)
|
|
||||||
? AiType.local
|
|
||||||
: AiType.cloud;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool isEditable() {
|
|
||||||
// On non-desktop platforms, always editable.
|
|
||||||
if (!_isDesktop) return true;
|
|
||||||
return getCurrentAiType().isLocal
|
|
||||||
? _localAIState?.state == RunningStatePB.Running
|
|
||||||
: true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void _notifyStateChanged() {
|
|
||||||
onChanged?.call(getCurrentAiType(), isEditable(), hintText());
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> init() async {
|
|
||||||
// Load both available models and local state concurrently.
|
|
||||||
await Future.wait([
|
|
||||||
_loadAvailableModels(),
|
|
||||||
_loadLocalAIState(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _loadAvailableModels() async {
|
|
||||||
final payload = AvailableModelsQueryPB(source: objectId);
|
|
||||||
final result = await AIEventGetAvailableModels(payload).send();
|
|
||||||
result.fold(
|
|
||||||
(models) {
|
|
||||||
_availableModels = models;
|
|
||||||
onAvailableModelsChanged?.call(models);
|
|
||||||
_notifyStateChanged();
|
|
||||||
},
|
|
||||||
(err) => Log.error("Failed to get available models: $err"),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _loadLocalAIState() async {
|
|
||||||
final result = await AIEventGetLocalAIState().send();
|
|
||||||
result.fold(
|
|
||||||
(state) {
|
|
||||||
_localAIState = state;
|
|
||||||
_notifyStateChanged();
|
|
||||||
},
|
|
||||||
(error) {
|
|
||||||
Log.error("Failed to get local AI state: $error");
|
|
||||||
_notifyStateChanged();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void startListening({
|
|
||||||
void Function(AiType, bool, String)? onChanged,
|
|
||||||
void Function(AvailableModelsPB)? onAvailableModelsChanged,
|
|
||||||
}) {
|
|
||||||
this.onChanged = onChanged;
|
|
||||||
this.onAvailableModelsChanged = onAvailableModelsChanged;
|
|
||||||
|
|
||||||
// Only start local AI listener on desktop platforms.
|
|
||||||
if (_isDesktop) {
|
|
||||||
_localAIListener?.start(
|
|
||||||
stateCallback: (state) {
|
|
||||||
_localAIState = state;
|
|
||||||
if (state.state == RunningStatePB.Running ||
|
|
||||||
state.state == RunningStatePB.Stopped) {
|
|
||||||
_loadAvailableModels();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
_aiModelSwitchListener.start(
|
|
||||||
onUpdateSelectedModel: (model) {
|
|
||||||
if (_availableModels != null) {
|
|
||||||
final updatedModels = _availableModels!.deepCopy()
|
|
||||||
..selectedModel = model;
|
|
||||||
_availableModels = updatedModels;
|
|
||||||
onAvailableModelsChanged?.call(updatedModels);
|
|
||||||
}
|
|
||||||
if (model.isLocal && _isDesktop) {
|
|
||||||
_loadLocalAIState();
|
|
||||||
} else {
|
|
||||||
_notifyStateChanged();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> stop() async {
|
|
||||||
onChanged = null;
|
|
||||||
await _localAIListener?.stop();
|
|
||||||
await _aiModelSwitchListener.stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -0,0 +1,172 @@
|
|||||||
|
import 'package:appflowy/ai/ai.dart';
|
||||||
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
|
import 'package:appflowy/plugins/ai_chat/application/ai_model_switch_listener.dart';
|
||||||
|
import 'package:appflowy/workspace/application/settings/ai/local_llm_listener.dart';
|
||||||
|
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||||
|
import 'package:appflowy_backend/log.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart';
|
||||||
|
import 'package:appflowy_result/appflowy_result.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:protobuf/protobuf.dart';
|
||||||
|
import 'package:universal_platform/universal_platform.dart';
|
||||||
|
|
||||||
|
typedef OnModelStateChangedCallback = void Function(AiType, bool, String);
|
||||||
|
typedef OnAvailableModelsChangedCallback = void Function(
|
||||||
|
List<AIModelPB>,
|
||||||
|
AIModelPB?,
|
||||||
|
);
|
||||||
|
|
||||||
|
class AIModelStateNotifier {
|
||||||
|
AIModelStateNotifier({required this.objectId})
|
||||||
|
: _localAIListener =
|
||||||
|
UniversalPlatform.isDesktop ? LocalAIStateListener() : null,
|
||||||
|
_aiModelSwitchListener = AIModelSwitchListener(objectId: objectId) {
|
||||||
|
_startListening();
|
||||||
|
_init();
|
||||||
|
}
|
||||||
|
|
||||||
|
final String objectId;
|
||||||
|
final LocalAIStateListener? _localAIListener;
|
||||||
|
final AIModelSwitchListener _aiModelSwitchListener;
|
||||||
|
LocalAIPB? _localAIState;
|
||||||
|
AvailableModelsPB? _availableModels;
|
||||||
|
|
||||||
|
// callbacks
|
||||||
|
final List<OnModelStateChangedCallback> _stateChangedCallbacks = [];
|
||||||
|
final List<OnAvailableModelsChangedCallback>
|
||||||
|
_availableModelsChangedCallbacks = [];
|
||||||
|
|
||||||
|
void _startListening() {
|
||||||
|
if (UniversalPlatform.isDesktop) {
|
||||||
|
_localAIListener?.start(
|
||||||
|
stateCallback: (state) async {
|
||||||
|
_localAIState = state;
|
||||||
|
_notifyStateChanged();
|
||||||
|
|
||||||
|
if (state.state == RunningStatePB.Running ||
|
||||||
|
state.state == RunningStatePB.Stopped) {
|
||||||
|
await _loadAvailableModels();
|
||||||
|
_notifyAvailableModelsChanged();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_aiModelSwitchListener.start(
|
||||||
|
onUpdateSelectedModel: (model) async {
|
||||||
|
final updatedModels = _availableModels?.deepCopy()
|
||||||
|
?..selectedModel = model;
|
||||||
|
_availableModels = updatedModels;
|
||||||
|
_notifyAvailableModelsChanged();
|
||||||
|
|
||||||
|
if (model.isLocal && UniversalPlatform.isDesktop) {
|
||||||
|
await _loadLocalAiState();
|
||||||
|
}
|
||||||
|
_notifyStateChanged();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _init() async {
|
||||||
|
await Future.wait([_loadLocalAiState(), _loadAvailableModels()]);
|
||||||
|
_notifyStateChanged();
|
||||||
|
_notifyAvailableModelsChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
void addListener({
|
||||||
|
OnModelStateChangedCallback? onStateChanged,
|
||||||
|
OnAvailableModelsChangedCallback? onAvailableModelsChanged,
|
||||||
|
}) {
|
||||||
|
if (onStateChanged != null) {
|
||||||
|
_stateChangedCallbacks.add(onStateChanged);
|
||||||
|
}
|
||||||
|
if (onAvailableModelsChanged != null) {
|
||||||
|
_availableModelsChangedCallbacks.add(onAvailableModelsChanged);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void removeListener({
|
||||||
|
OnModelStateChangedCallback? onStateChanged,
|
||||||
|
OnAvailableModelsChangedCallback? onAvailableModelsChanged,
|
||||||
|
}) {
|
||||||
|
if (onStateChanged != null) {
|
||||||
|
_stateChangedCallbacks.remove(onStateChanged);
|
||||||
|
}
|
||||||
|
if (onAvailableModelsChanged != null) {
|
||||||
|
_availableModelsChangedCallbacks.remove(onAvailableModelsChanged);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> dispose() async {
|
||||||
|
_stateChangedCallbacks.clear();
|
||||||
|
_availableModelsChangedCallbacks.clear();
|
||||||
|
await _localAIListener?.stop();
|
||||||
|
await _aiModelSwitchListener.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
(AiType, String, bool) getState() {
|
||||||
|
if (UniversalPlatform.isMobile) {
|
||||||
|
return (AiType.cloud, LocaleKeys.chat_inputMessageHint.tr(), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
final availableModels = _availableModels;
|
||||||
|
final localAiState = _localAIState;
|
||||||
|
|
||||||
|
if (availableModels == null) {
|
||||||
|
Log.warn("No available models");
|
||||||
|
return (AiType.cloud, LocaleKeys.chat_inputMessageHint.tr(), true);
|
||||||
|
}
|
||||||
|
if (localAiState == null) {
|
||||||
|
Log.warn("Cannot get local AI state");
|
||||||
|
return (AiType.cloud, LocaleKeys.chat_inputMessageHint.tr(), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!availableModels.selectedModel.isLocal) {
|
||||||
|
return (AiType.cloud, LocaleKeys.chat_inputMessageHint.tr(), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
final editable = localAiState.state == RunningStatePB.Running;
|
||||||
|
final hintText = editable
|
||||||
|
? LocaleKeys.chat_inputLocalAIMessageHint.tr()
|
||||||
|
: LocaleKeys.settings_aiPage_keys_localAIInitializing.tr();
|
||||||
|
|
||||||
|
return (AiType.local, hintText, editable);
|
||||||
|
}
|
||||||
|
|
||||||
|
(List<AIModelPB>, AIModelPB?) getAvailableModels() {
|
||||||
|
final availableModels = _availableModels;
|
||||||
|
if (availableModels == null) {
|
||||||
|
return ([], null);
|
||||||
|
}
|
||||||
|
return (availableModels.models, availableModels.selectedModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _notifyAvailableModelsChanged() {
|
||||||
|
final (models, selectedModel) = getAvailableModels();
|
||||||
|
for (final callback in _availableModelsChangedCallbacks) {
|
||||||
|
callback(models, selectedModel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _notifyStateChanged() {
|
||||||
|
final (type, hintText, isEditable) = getState();
|
||||||
|
for (final callback in _stateChangedCallbacks) {
|
||||||
|
callback(type, isEditable, hintText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadAvailableModels() {
|
||||||
|
final payload = AvailableModelsQueryPB(source: objectId);
|
||||||
|
return AIEventGetAvailableModels(payload).send().fold(
|
||||||
|
(models) => _availableModels = models,
|
||||||
|
(err) => Log.error("Failed to get available models: $err"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadLocalAiState() {
|
||||||
|
return AIEventGetLocalAIState().send().fold(
|
||||||
|
(localAIState) => _localAIState = localAIState,
|
||||||
|
(error) => Log.error("Failed to get local AI state: $error"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:appflowy/ai/service/ai_input_control.dart';
|
import 'package:appflowy/ai/service/ai_model_state_notifier.dart';
|
||||||
import 'package:appflowy/plugins/ai_chat/application/chat_entity.dart';
|
import 'package:appflowy/plugins/ai_chat/application/chat_entity.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
|
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
@ -14,17 +14,18 @@ class AIPromptInputBloc extends Bloc<AIPromptInputEvent, AIPromptInputState> {
|
|||||||
AIPromptInputBloc({
|
AIPromptInputBloc({
|
||||||
required String objectId,
|
required String objectId,
|
||||||
required PredefinedFormat? predefinedFormat,
|
required PredefinedFormat? predefinedFormat,
|
||||||
}) : _aiModelStateNotifier = AIModelStateNotifier(objectId: objectId),
|
}) : aiModelStateNotifier = AIModelStateNotifier(objectId: objectId),
|
||||||
super(AIPromptInputState.initial(objectId, predefinedFormat)) {
|
super(AIPromptInputState.initial(predefinedFormat)) {
|
||||||
_dispatch();
|
_dispatch();
|
||||||
|
_startListening();
|
||||||
_init();
|
_init();
|
||||||
}
|
}
|
||||||
|
|
||||||
final AIModelStateNotifier _aiModelStateNotifier;
|
final AIModelStateNotifier aiModelStateNotifier;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> close() async {
|
Future<void> close() async {
|
||||||
await _aiModelStateNotifier.stop();
|
await aiModelStateNotifier.dispose();
|
||||||
return super.close();
|
return super.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -36,7 +37,6 @@ class AIPromptInputBloc extends Bloc<AIPromptInputEvent, AIPromptInputState> {
|
|||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
aiType: aiType,
|
aiType: aiType,
|
||||||
supportChatWithFile: false,
|
|
||||||
editable: editable,
|
editable: editable,
|
||||||
hintText: hintText,
|
hintText: hintText,
|
||||||
),
|
),
|
||||||
@ -103,16 +103,17 @@ class AIPromptInputBloc extends Bloc<AIPromptInputEvent, AIPromptInputState> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _init() {
|
void _startListening() {
|
||||||
_aiModelStateNotifier.startListening(
|
aiModelStateNotifier.addListener(
|
||||||
onChanged: (aiType, editable, hintText) {
|
onStateChanged: (aiType, editable, hintText) {
|
||||||
if (!isClosed) {
|
add(AIPromptInputEvent.updateAIState(aiType, editable, hintText));
|
||||||
add(AIPromptInputEvent.updateAIState(aiType, editable, hintText));
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
_aiModelStateNotifier.init();
|
void _init() {
|
||||||
|
final (aiType, hintText, isEditable) = aiModelStateNotifier.getState();
|
||||||
|
add(AIPromptInputEvent.updateAIState(aiType, isEditable, hintText));
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, dynamic> consumeMetadata() {
|
Map<String, dynamic> consumeMetadata() {
|
||||||
@ -155,7 +156,6 @@ class AIPromptInputEvent with _$AIPromptInputEvent {
|
|||||||
@freezed
|
@freezed
|
||||||
class AIPromptInputState with _$AIPromptInputState {
|
class AIPromptInputState with _$AIPromptInputState {
|
||||||
const factory AIPromptInputState({
|
const factory AIPromptInputState({
|
||||||
required String objectId,
|
|
||||||
required AiType aiType,
|
required AiType aiType,
|
||||||
required bool supportChatWithFile,
|
required bool supportChatWithFile,
|
||||||
required bool showPredefinedFormats,
|
required bool showPredefinedFormats,
|
||||||
@ -166,12 +166,8 @@ class AIPromptInputState with _$AIPromptInputState {
|
|||||||
required String hintText,
|
required String hintText,
|
||||||
}) = _AIPromptInputState;
|
}) = _AIPromptInputState;
|
||||||
|
|
||||||
factory AIPromptInputState.initial(
|
factory AIPromptInputState.initial(PredefinedFormat? format) =>
|
||||||
String objectId,
|
|
||||||
PredefinedFormat? format,
|
|
||||||
) =>
|
|
||||||
AIPromptInputState(
|
AIPromptInputState(
|
||||||
objectId: objectId,
|
|
||||||
aiType: AiType.cloud,
|
aiType: AiType.cloud,
|
||||||
supportChatWithFile: false,
|
supportChatWithFile: false,
|
||||||
showPredefinedFormats: format != null,
|
showPredefinedFormats: format != null,
|
||||||
@ -182,11 +178,3 @@ class AIPromptInputState with _$AIPromptInputState {
|
|||||||
hintText: '',
|
hintText: '',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
enum AiType {
|
|
||||||
cloud,
|
|
||||||
local;
|
|
||||||
|
|
||||||
bool get isCloud => this == cloud;
|
|
||||||
bool get isLocal => this == local;
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,66 +1,66 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:appflowy/ai/service/ai_input_control.dart';
|
import 'package:appflowy/ai/service/ai_model_state_notifier.dart';
|
||||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-ai/entities.pbserver.dart';
|
import 'package:appflowy_backend/protobuf/flowy-ai/entities.pbserver.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
import 'package:protobuf/protobuf.dart';
|
|
||||||
|
|
||||||
part 'select_model_bloc.freezed.dart';
|
part 'select_model_bloc.freezed.dart';
|
||||||
|
|
||||||
class SelectModelBloc extends Bloc<SelectModelEvent, SelectModelState> {
|
class SelectModelBloc extends Bloc<SelectModelEvent, SelectModelState> {
|
||||||
SelectModelBloc({
|
SelectModelBloc({
|
||||||
required this.objectId,
|
required AIModelStateNotifier aiModelStateNotifier,
|
||||||
}) : _aiModelStateNotifier = AIModelStateNotifier(objectId: objectId),
|
}) : _aiModelStateNotifier = aiModelStateNotifier,
|
||||||
super(const SelectModelState()) {
|
super(SelectModelState.initial(aiModelStateNotifier)) {
|
||||||
_aiModelStateNotifier.init();
|
|
||||||
_aiModelStateNotifier.startListening(
|
|
||||||
onAvailableModelsChanged: (models) {
|
|
||||||
if (!isClosed) {
|
|
||||||
add(SelectModelEvent.didLoadModels(models));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
on<SelectModelEvent>(
|
on<SelectModelEvent>(
|
||||||
(event, emit) async {
|
(event, emit) {
|
||||||
await event.when(
|
event.when(
|
||||||
selectModel: (AIModelPB model) async {
|
selectModel: (model) {
|
||||||
await AIEventUpdateSelectedModel(
|
AIEventUpdateSelectedModel(
|
||||||
UpdateSelectedModelPB(
|
UpdateSelectedModelPB(
|
||||||
source: objectId,
|
source: _aiModelStateNotifier.objectId,
|
||||||
selectedModel: model,
|
selectedModel: model,
|
||||||
),
|
),
|
||||||
).send();
|
).send();
|
||||||
|
|
||||||
state.availableModels?.freeze();
|
emit(state.copyWith(selectedModel: model));
|
||||||
final newAvailableModels = state.availableModels?.rebuild((m) {
|
},
|
||||||
m.selectedModel = model;
|
didLoadModels: (models, selectedModel) {
|
||||||
});
|
|
||||||
|
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
SelectModelState(
|
||||||
availableModels: newAvailableModels,
|
models: models,
|
||||||
|
selectedModel: selectedModel,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
didLoadModels: (AvailableModelsPB models) {
|
|
||||||
emit(state.copyWith(availableModels: models));
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
_aiModelStateNotifier.addListener(
|
||||||
|
onAvailableModelsChanged: _onAvailableModelsChanged,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final String objectId;
|
|
||||||
final AIModelStateNotifier _aiModelStateNotifier;
|
final AIModelStateNotifier _aiModelStateNotifier;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> close() async {
|
Future<void> close() async {
|
||||||
await _aiModelStateNotifier.stop();
|
_aiModelStateNotifier.removeListener(
|
||||||
|
onAvailableModelsChanged: _onAvailableModelsChanged,
|
||||||
|
);
|
||||||
await super.close();
|
await super.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _onAvailableModelsChanged(
|
||||||
|
List<AIModelPB> models,
|
||||||
|
AIModelPB? selectedModel,
|
||||||
|
) {
|
||||||
|
if (!isClosed) {
|
||||||
|
add(SelectModelEvent.didLoadModels(models, selectedModel));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@freezed
|
@freezed
|
||||||
@ -70,13 +70,23 @@ class SelectModelEvent with _$SelectModelEvent {
|
|||||||
) = _SelectModel;
|
) = _SelectModel;
|
||||||
|
|
||||||
const factory SelectModelEvent.didLoadModels(
|
const factory SelectModelEvent.didLoadModels(
|
||||||
AvailableModelsPB models,
|
List<AIModelPB> models,
|
||||||
|
AIModelPB? selectedModel,
|
||||||
) = _DidLoadModels;
|
) = _DidLoadModels;
|
||||||
}
|
}
|
||||||
|
|
||||||
@freezed
|
@freezed
|
||||||
class SelectModelState with _$SelectModelState {
|
class SelectModelState with _$SelectModelState {
|
||||||
const factory SelectModelState({
|
const factory SelectModelState({
|
||||||
AvailableModelsPB? availableModels,
|
required List<AIModelPB> models,
|
||||||
|
required AIModelPB? selectedModel,
|
||||||
}) = _SelectModelState;
|
}) = _SelectModelState;
|
||||||
|
|
||||||
|
factory SelectModelState.initial(AIModelStateNotifier notifier) {
|
||||||
|
final (models, selectedModel) = notifier.getAvailableModels();
|
||||||
|
return SelectModelState(
|
||||||
|
models: models,
|
||||||
|
selectedModel: selectedModel,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,17 +1,14 @@
|
|||||||
import 'package:appflowy/ai/ai.dart';
|
import 'package:appflowy/ai/ai.dart';
|
||||||
import 'package:appflowy/ai/service/select_model_bloc.dart';
|
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy/plugins/ai_chat/application/chat_input_control_cubit.dart';
|
import 'package:appflowy/plugins/ai_chat/application/chat_input_control_cubit.dart';
|
||||||
import 'package:appflowy/plugins/ai_chat/presentation/layout_define.dart';
|
import 'package:appflowy/plugins/ai_chat/presentation/layout_define.dart';
|
||||||
import 'package:appflowy/startup/startup.dart';
|
import 'package:appflowy/startup/startup.dart';
|
||||||
import 'package:appflowy/util/theme_extension.dart';
|
import 'package:appflowy/util/theme_extension.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
|
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:extended_text_field/extended_text_field.dart';
|
import 'package:extended_text_field/extended_text_field.dart';
|
||||||
import 'package:flowy_infra/file_picker/file_picker_service.dart';
|
import 'package:flowy_infra/file_picker/file_picker_service.dart';
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
@ -169,7 +166,6 @@ class _DesktopPromptInputState extends State<DesktopPromptInput> {
|
|||||||
top: null,
|
top: null,
|
||||||
child: TextFieldTapRegion(
|
child: TextFieldTapRegion(
|
||||||
child: _PromptBottomActions(
|
child: _PromptBottomActions(
|
||||||
objectId: state.objectId,
|
|
||||||
showPredefinedFormats:
|
showPredefinedFormats:
|
||||||
state.showPredefinedFormats,
|
state.showPredefinedFormats,
|
||||||
onTogglePredefinedFormatSection: () =>
|
onTogglePredefinedFormatSection: () =>
|
||||||
@ -567,7 +563,6 @@ class PromptInputTextField extends StatelessWidget {
|
|||||||
|
|
||||||
class _PromptBottomActions extends StatelessWidget {
|
class _PromptBottomActions extends StatelessWidget {
|
||||||
const _PromptBottomActions({
|
const _PromptBottomActions({
|
||||||
required this.objectId,
|
|
||||||
required this.sendButtonState,
|
required this.sendButtonState,
|
||||||
required this.showPredefinedFormats,
|
required this.showPredefinedFormats,
|
||||||
required this.onTogglePredefinedFormatSection,
|
required this.onTogglePredefinedFormatSection,
|
||||||
@ -579,7 +574,6 @@ class _PromptBottomActions extends StatelessWidget {
|
|||||||
this.extraBottomActionButton,
|
this.extraBottomActionButton,
|
||||||
});
|
});
|
||||||
|
|
||||||
final String objectId;
|
|
||||||
final bool showPredefinedFormats;
|
final bool showPredefinedFormats;
|
||||||
final void Function() onTogglePredefinedFormatSection;
|
final void Function() onTogglePredefinedFormatSection;
|
||||||
final void Function() onStartMention;
|
final void Function() onStartMention;
|
||||||
@ -600,10 +594,16 @@ class _PromptBottomActions extends StatelessWidget {
|
|||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
_predefinedFormatButton(),
|
_predefinedFormatButton(),
|
||||||
SelectModelButton(objectId: objectId),
|
const HSpace(
|
||||||
|
DesktopAIChatSizes.inputActionBarButtonSpacing,
|
||||||
|
),
|
||||||
|
SelectModelMenu(
|
||||||
|
aiModelStateNotifier:
|
||||||
|
context.read<AIPromptInputBloc>().aiModelStateNotifier,
|
||||||
|
),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
if (state.aiType.isCloud) ...[
|
if (state.aiType.isCloud) ...[
|
||||||
_selectSourcesButton(context),
|
_selectSourcesButton(),
|
||||||
const HSpace(
|
const HSpace(
|
||||||
DesktopAIChatSizes.inputActionBarButtonSpacing,
|
DesktopAIChatSizes.inputActionBarButtonSpacing,
|
||||||
),
|
),
|
||||||
@ -639,7 +639,7 @@ class _PromptBottomActions extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _selectSourcesButton(BuildContext context) {
|
Widget _selectSourcesButton() {
|
||||||
return PromptInputDesktopSelectSourcesButton(
|
return PromptInputDesktopSelectSourcesButton(
|
||||||
onUpdateSelectedSources: onUpdateSelectedSources,
|
onUpdateSelectedSources: onUpdateSelectedSources,
|
||||||
selectedSourcesNotifier: selectedSourcesNotifier,
|
selectedSourcesNotifier: selectedSourcesNotifier,
|
||||||
@ -686,225 +686,3 @@ class _PromptBottomActions extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SelectModelButton extends StatefulWidget {
|
|
||||||
const SelectModelButton({
|
|
||||||
super.key,
|
|
||||||
required this.objectId,
|
|
||||||
});
|
|
||||||
|
|
||||||
final String objectId;
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<SelectModelButton> createState() => _SelectModelButtonState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _SelectModelButtonState extends State<SelectModelButton> {
|
|
||||||
final popoverController = PopoverController();
|
|
||||||
late SelectModelBloc bloc;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
bloc = SelectModelBloc(objectId: widget.objectId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
popoverController.close();
|
|
||||||
bloc.close();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return BlocProvider.value(
|
|
||||||
value: bloc,
|
|
||||||
child: BlocBuilder<SelectModelBloc, SelectModelState>(
|
|
||||||
builder: (context, state) {
|
|
||||||
return AppFlowyPopover(
|
|
||||||
// constraints: BoxConstraints.loose(const Size(250, 200)),
|
|
||||||
offset: const Offset(0.0, -10.0),
|
|
||||||
direction: PopoverDirection.topWithLeftAligned,
|
|
||||||
margin: EdgeInsets.zero,
|
|
||||||
controller: popoverController,
|
|
||||||
onOpen: () {},
|
|
||||||
onClose: () {},
|
|
||||||
popupBuilder: (_) {
|
|
||||||
return BlocProvider.value(
|
|
||||||
value: bloc,
|
|
||||||
child: _PopoverSelectModel(
|
|
||||||
onClose: () => popoverController.close(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: _CurrentModelButton(
|
|
||||||
key: ValueKey(state.availableModels?.selectedModel.name),
|
|
||||||
modelName: state.availableModels?.selectedModel.name ?? "",
|
|
||||||
onTap: () => popoverController.show(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _PopoverSelectModel extends StatelessWidget {
|
|
||||||
const _PopoverSelectModel({
|
|
||||||
required this.onClose,
|
|
||||||
});
|
|
||||||
|
|
||||||
final VoidCallback onClose;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return BlocBuilder<SelectModelBloc, SelectModelState>(
|
|
||||||
builder: (context, state) {
|
|
||||||
if (state.availableModels == null ||
|
|
||||||
state.availableModels!.models.isEmpty) {
|
|
||||||
return const SizedBox.shrink();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Separate models into local and cloud models
|
|
||||||
final localModels = state.availableModels!.models
|
|
||||||
.where((model) => model.isLocal)
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
final cloudModels = state.availableModels!.models
|
|
||||||
.where((model) => !model.isLocal)
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.fromLTRB(8, 4, 8, 12),
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
// Local AI Models Section
|
|
||||||
if (localModels.isNotEmpty) ...[
|
|
||||||
_ModelSectionHeader(
|
|
||||||
title: LocaleKeys.chat_changeFormat_localModel.tr(),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 4),
|
|
||||||
...localModels.map(
|
|
||||||
(model) => _ModelItem(
|
|
||||||
model: model,
|
|
||||||
onTap: () {
|
|
||||||
context.read<SelectModelBloc>().add(
|
|
||||||
SelectModelEvent.selectModel(model),
|
|
||||||
);
|
|
||||||
onClose();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
],
|
|
||||||
|
|
||||||
// Cloud AI Models Section
|
|
||||||
if (cloudModels.isNotEmpty) ...[
|
|
||||||
if (localModels.isNotEmpty)
|
|
||||||
_ModelSectionHeader(
|
|
||||||
title: LocaleKeys.chat_changeFormat_cloudModel.tr(),
|
|
||||||
),
|
|
||||||
const VSpace(4),
|
|
||||||
...cloudModels.map(
|
|
||||||
(model) => _ModelItem(
|
|
||||||
model: model,
|
|
||||||
onTap: () {
|
|
||||||
context.read<SelectModelBloc>().add(
|
|
||||||
SelectModelEvent.selectModel(model),
|
|
||||||
);
|
|
||||||
onClose();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ModelSectionHeader extends StatelessWidget {
|
|
||||||
const _ModelSectionHeader({
|
|
||||||
required this.title,
|
|
||||||
});
|
|
||||||
|
|
||||||
final String title;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.only(top: 4, bottom: 2),
|
|
||||||
child: FlowyText(
|
|
||||||
title,
|
|
||||||
fontSize: 12,
|
|
||||||
color: Theme.of(context).hintColor,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ModelItem extends StatelessWidget {
|
|
||||||
const _ModelItem({
|
|
||||||
required this.model,
|
|
||||||
required this.onTap,
|
|
||||||
});
|
|
||||||
|
|
||||||
final AIModelPB model;
|
|
||||||
final VoidCallback onTap;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final modelName = model.name;
|
|
||||||
|
|
||||||
return FlowyTextButton(
|
|
||||||
modelName,
|
|
||||||
fillColor: Colors.transparent,
|
|
||||||
onPressed: onTap,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _CurrentModelButton extends StatelessWidget {
|
|
||||||
const _CurrentModelButton({
|
|
||||||
required this.modelName,
|
|
||||||
required this.onTap,
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
final String modelName;
|
|
||||||
final VoidCallback onTap;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return FlowyTooltip(
|
|
||||||
message: LocaleKeys.chat_changeFormat_switchModel.tr(),
|
|
||||||
child: GestureDetector(
|
|
||||||
onTap: onTap,
|
|
||||||
behavior: HitTestBehavior.opaque,
|
|
||||||
child: SizedBox(
|
|
||||||
height: DesktopAIPromptSizes.actionBarButtonSize,
|
|
||||||
child: FlowyHover(
|
|
||||||
style: const HoverStyle(
|
|
||||||
borderRadius: BorderRadius.all(Radius.circular(8)),
|
|
||||||
),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsetsDirectional.fromSTEB(6, 6, 4, 6),
|
|
||||||
child: FlowyText(
|
|
||||||
modelName,
|
|
||||||
fontSize: 12,
|
|
||||||
figmaLineHeight: 16,
|
|
||||||
color: Theme.of(context).hintColor,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -104,6 +104,7 @@ class ChangeFormatBar extends StatelessWidget {
|
|||||||
},
|
},
|
||||||
child: FlowyTooltip(
|
child: FlowyTooltip(
|
||||||
message: format.i18n,
|
message: format.i18n,
|
||||||
|
preferBelow: false,
|
||||||
child: SizedBox.square(
|
child: SizedBox.square(
|
||||||
dimension: _buttonSize,
|
dimension: _buttonSize,
|
||||||
child: FlowyHover(
|
child: FlowyHover(
|
||||||
@ -150,6 +151,7 @@ class ChangeFormatBar extends StatelessWidget {
|
|||||||
},
|
},
|
||||||
child: FlowyTooltip(
|
child: FlowyTooltip(
|
||||||
message: format.i18n,
|
message: format.i18n,
|
||||||
|
preferBelow: false,
|
||||||
child: SizedBox.square(
|
child: SizedBox.square(
|
||||||
dimension: _buttonSize,
|
dimension: _buttonSize,
|
||||||
child: FlowyHover(
|
child: FlowyHover(
|
||||||
|
|||||||
@ -0,0 +1,222 @@
|
|||||||
|
import 'package:appflowy/ai/ai.dart';
|
||||||
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||||
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-ai/protobuf.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
|
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
|
class SelectModelMenu extends StatefulWidget {
|
||||||
|
const SelectModelMenu({
|
||||||
|
super.key,
|
||||||
|
required this.aiModelStateNotifier,
|
||||||
|
});
|
||||||
|
|
||||||
|
final AIModelStateNotifier aiModelStateNotifier;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<SelectModelMenu> createState() => _SelectModelMenuState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SelectModelMenuState extends State<SelectModelMenu> {
|
||||||
|
final popoverController = PopoverController();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocProvider(
|
||||||
|
create: (context) => SelectModelBloc(
|
||||||
|
aiModelStateNotifier: widget.aiModelStateNotifier,
|
||||||
|
),
|
||||||
|
child: BlocBuilder<SelectModelBloc, SelectModelState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
if (state.selectedModel == null) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
return AppFlowyPopover(
|
||||||
|
offset: Offset(-12.0, 0.0),
|
||||||
|
constraints: BoxConstraints(maxWidth: 250, maxHeight: 600),
|
||||||
|
direction: PopoverDirection.topWithLeftAligned,
|
||||||
|
margin: EdgeInsets.zero,
|
||||||
|
controller: popoverController,
|
||||||
|
popupBuilder: (popoverContext) {
|
||||||
|
return SelectModelPopoverContent(
|
||||||
|
models: state.models,
|
||||||
|
selectedModel: state.selectedModel,
|
||||||
|
onSelectModel: (model) {
|
||||||
|
if (model != state.selectedModel) {
|
||||||
|
context
|
||||||
|
.read<SelectModelBloc>()
|
||||||
|
.add(SelectModelEvent.selectModel(model));
|
||||||
|
}
|
||||||
|
popoverController.close();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: _CurrentModelButton(
|
||||||
|
modelName: state.selectedModel!.name,
|
||||||
|
onTap: () => popoverController.show(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SelectModelPopoverContent extends StatelessWidget {
|
||||||
|
const SelectModelPopoverContent({
|
||||||
|
super.key,
|
||||||
|
required this.models,
|
||||||
|
required this.selectedModel,
|
||||||
|
this.onSelectModel,
|
||||||
|
});
|
||||||
|
|
||||||
|
final List<AIModelPB> models;
|
||||||
|
final AIModelPB? selectedModel;
|
||||||
|
final void Function(AIModelPB)? onSelectModel;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (models.isEmpty) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
|
// separate models into local and cloud models
|
||||||
|
final localModels = models.where((model) => model.isLocal).toList();
|
||||||
|
final cloudModels = models.where((model) => !model.isLocal).toList();
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
if (localModels.isNotEmpty) ...[
|
||||||
|
_ModelSectionHeader(
|
||||||
|
title: LocaleKeys.chat_switchModel_localModel.tr(),
|
||||||
|
),
|
||||||
|
const VSpace(4.0),
|
||||||
|
],
|
||||||
|
...localModels.map(
|
||||||
|
(model) => _ModelItem(
|
||||||
|
model: model,
|
||||||
|
isSelected: model == selectedModel,
|
||||||
|
onTap: () => onSelectModel?.call(model),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (cloudModels.isNotEmpty && localModels.isNotEmpty) ...[
|
||||||
|
const VSpace(8.0),
|
||||||
|
_ModelSectionHeader(
|
||||||
|
title: LocaleKeys.chat_switchModel_cloudModel.tr(),
|
||||||
|
),
|
||||||
|
const VSpace(4.0),
|
||||||
|
],
|
||||||
|
...cloudModels.map(
|
||||||
|
(model) => _ModelItem(
|
||||||
|
model: model,
|
||||||
|
isSelected: model == selectedModel,
|
||||||
|
onTap: () => onSelectModel?.call(model),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ModelSectionHeader extends StatelessWidget {
|
||||||
|
const _ModelSectionHeader({
|
||||||
|
required this.title,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String title;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 4, bottom: 2),
|
||||||
|
child: FlowyText(
|
||||||
|
title,
|
||||||
|
fontSize: 12,
|
||||||
|
figmaLineHeight: 16,
|
||||||
|
color: Theme.of(context).hintColor,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ModelItem extends StatelessWidget {
|
||||||
|
const _ModelItem({
|
||||||
|
required this.model,
|
||||||
|
required this.isSelected,
|
||||||
|
required this.onTap,
|
||||||
|
});
|
||||||
|
|
||||||
|
final AIModelPB model;
|
||||||
|
final bool isSelected;
|
||||||
|
final VoidCallback onTap;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SizedBox(
|
||||||
|
height: 32,
|
||||||
|
child: FlowyButton(
|
||||||
|
onTap: onTap,
|
||||||
|
margin: EdgeInsets.symmetric(horizontal: 8.0, vertical: 6.0),
|
||||||
|
text: FlowyText(model.name),
|
||||||
|
rightIcon: isSelected ? FlowySvg(FlowySvgs.check_s) : null,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CurrentModelButton extends StatelessWidget {
|
||||||
|
const _CurrentModelButton({
|
||||||
|
required this.modelName,
|
||||||
|
required this.onTap,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String modelName;
|
||||||
|
final VoidCallback onTap;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return FlowyTooltip(
|
||||||
|
message: LocaleKeys.chat_switchModel_label.tr(),
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: onTap,
|
||||||
|
behavior: HitTestBehavior.opaque,
|
||||||
|
child: SizedBox(
|
||||||
|
height: DesktopAIPromptSizes.actionBarButtonSize,
|
||||||
|
child: FlowyHover(
|
||||||
|
style: const HoverStyle(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsetsDirectional.all(4.0),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
FlowyText(
|
||||||
|
modelName,
|
||||||
|
fontSize: 12,
|
||||||
|
figmaLineHeight: 16,
|
||||||
|
color: Theme.of(context).hintColor,
|
||||||
|
),
|
||||||
|
HSpace(2.0),
|
||||||
|
FlowySvg(
|
||||||
|
FlowySvgs.ai_source_drop_down_s,
|
||||||
|
color: Theme.of(context).hintColor,
|
||||||
|
size: const Size.square(8),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -239,9 +239,9 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
regenerateAnswer: (id, format) {
|
regenerateAnswer: (id, format, model) {
|
||||||
_clearRelatedQuestions();
|
_clearRelatedQuestions();
|
||||||
_regenerateAnswer(id, format);
|
_regenerateAnswer(id, format, model);
|
||||||
lastSentMessage = null;
|
lastSentMessage = null;
|
||||||
|
|
||||||
isFetchingRelatedQuestions = false;
|
isFetchingRelatedQuestions = false;
|
||||||
@ -483,6 +483,7 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
|
|||||||
void _regenerateAnswer(
|
void _regenerateAnswer(
|
||||||
String answerMessageIdString,
|
String answerMessageIdString,
|
||||||
PredefinedFormat? format,
|
PredefinedFormat? format,
|
||||||
|
AIModelPB? model,
|
||||||
) async {
|
) async {
|
||||||
final id = temporaryMessageIDMap.entries
|
final id = temporaryMessageIDMap.entries
|
||||||
.firstWhereOrNull((e) => e.value == answerMessageIdString)
|
.firstWhereOrNull((e) => e.value == answerMessageIdString)
|
||||||
@ -505,6 +506,9 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
|
|||||||
if (format != null) {
|
if (format != null) {
|
||||||
payload.format = format.toPB();
|
payload.format = format.toPB();
|
||||||
}
|
}
|
||||||
|
if (model != null) {
|
||||||
|
payload.model = model;
|
||||||
|
}
|
||||||
|
|
||||||
await AIEventRegenerateResponse(payload).send().fold(
|
await AIEventRegenerateResponse(payload).send().fold(
|
||||||
(success) {
|
(success) {
|
||||||
@ -637,6 +641,7 @@ class ChatEvent with _$ChatEvent {
|
|||||||
const factory ChatEvent.regenerateAnswer(
|
const factory ChatEvent.regenerateAnswer(
|
||||||
String id,
|
String id,
|
||||||
PredefinedFormat? format,
|
PredefinedFormat? format,
|
||||||
|
AIModelPB? model,
|
||||||
) = _RegenerateAnswer;
|
) = _RegenerateAnswer;
|
||||||
|
|
||||||
// streaming answer
|
// streaming answer
|
||||||
|
|||||||
@ -265,10 +265,13 @@ class _ChatContentPage extends StatelessWidget {
|
|||||||
_onSelectMetadata(context, metadata),
|
_onSelectMetadata(context, metadata),
|
||||||
onRegenerate: () => context
|
onRegenerate: () => context
|
||||||
.read<ChatBloc>()
|
.read<ChatBloc>()
|
||||||
.add(ChatEvent.regenerateAnswer(message.id, null)),
|
.add(ChatEvent.regenerateAnswer(message.id, null, null)),
|
||||||
onChangeFormat: (format) => context
|
onChangeFormat: (format) => context
|
||||||
.read<ChatBloc>()
|
.read<ChatBloc>()
|
||||||
.add(ChatEvent.regenerateAnswer(message.id, format)),
|
.add(ChatEvent.regenerateAnswer(message.id, format, null)),
|
||||||
|
onChangeModel: (model) => context
|
||||||
|
.read<ChatBloc>()
|
||||||
|
.add(ChatEvent.regenerateAnswer(message.id, null, model)),
|
||||||
onStopStream: () => context.read<ChatBloc>().add(
|
onStopStream: () => context.read<ChatBloc>().add(
|
||||||
const ChatEvent.stopStream(),
|
const ChatEvent.stopStream(),
|
||||||
),
|
),
|
||||||
|
|||||||
@ -0,0 +1,145 @@
|
|||||||
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
|
import 'package:appflowy/mobile/presentation/base/app_bar/app_bar_actions.dart';
|
||||||
|
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
|
||||||
|
import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-ai/protobuf.dart';
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
Future<AIModelPB?> showChangeModelBottomSheet(
|
||||||
|
BuildContext context,
|
||||||
|
List<AIModelPB> models,
|
||||||
|
) {
|
||||||
|
return showMobileBottomSheet<AIModelPB?>(
|
||||||
|
context,
|
||||||
|
showDragHandle: true,
|
||||||
|
builder: (context) => _ChangeModelBottomSheetContent(models: models),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ChangeModelBottomSheetContent extends StatefulWidget {
|
||||||
|
const _ChangeModelBottomSheetContent({
|
||||||
|
required this.models,
|
||||||
|
});
|
||||||
|
|
||||||
|
final List<AIModelPB> models;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_ChangeModelBottomSheetContent> createState() =>
|
||||||
|
_ChangeModelBottomSheetContentState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ChangeModelBottomSheetContentState
|
||||||
|
extends State<_ChangeModelBottomSheetContent> {
|
||||||
|
AIModelPB? model;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
_Header(
|
||||||
|
onCancel: () => Navigator.of(context).pop(),
|
||||||
|
onDone: () => Navigator.of(context).pop(model),
|
||||||
|
),
|
||||||
|
const VSpace(4.0),
|
||||||
|
_Body(
|
||||||
|
models: widget.models,
|
||||||
|
selectedModel: model,
|
||||||
|
onSelectModel: (format) {
|
||||||
|
setState(() => model = format);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const VSpace(16.0),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _Header extends StatelessWidget {
|
||||||
|
const _Header({
|
||||||
|
required this.onCancel,
|
||||||
|
required this.onDone,
|
||||||
|
});
|
||||||
|
|
||||||
|
final VoidCallback onCancel;
|
||||||
|
final VoidCallback onDone;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SizedBox(
|
||||||
|
height: 44.0,
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: AppBarBackButton(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
vertical: 12,
|
||||||
|
horizontal: 16,
|
||||||
|
),
|
||||||
|
onTap: onCancel,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Align(
|
||||||
|
child: Container(
|
||||||
|
constraints: const BoxConstraints(maxWidth: 250),
|
||||||
|
child: FlowyText(
|
||||||
|
LocaleKeys.chat_switchModel_label.tr(),
|
||||||
|
fontSize: 17.0,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
child: AppBarDoneButton(
|
||||||
|
onTap: onDone,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _Body extends StatelessWidget {
|
||||||
|
const _Body({
|
||||||
|
required this.models,
|
||||||
|
required this.selectedModel,
|
||||||
|
required this.onSelectModel,
|
||||||
|
});
|
||||||
|
|
||||||
|
final List<AIModelPB> models;
|
||||||
|
final AIModelPB? selectedModel;
|
||||||
|
final void Function(AIModelPB) onSelectModel;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: models
|
||||||
|
.mapIndexed(
|
||||||
|
(index, model) => _buildModelButton(model, index == 0),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildModelButton(
|
||||||
|
AIModelPB model, [
|
||||||
|
bool isFirst = false,
|
||||||
|
]) {
|
||||||
|
return FlowyOptionTile.checkbox(
|
||||||
|
text: model.name,
|
||||||
|
isSelected: model == selectedModel,
|
||||||
|
showTopBorder: isFirst,
|
||||||
|
onTap: () {
|
||||||
|
onSelectModel(model);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -21,6 +21,7 @@ import 'package:appflowy/workspace/application/view/view_ext.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/home/menu/view/view_item.dart';
|
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
|
||||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-ai/protobuf.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
|
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
import 'package:appflowy_result/appflowy_result.dart';
|
import 'package:appflowy_result/appflowy_result.dart';
|
||||||
@ -41,6 +42,7 @@ class AIMessageActionBar extends StatefulWidget {
|
|||||||
required this.showDecoration,
|
required this.showDecoration,
|
||||||
this.onRegenerate,
|
this.onRegenerate,
|
||||||
this.onChangeFormat,
|
this.onChangeFormat,
|
||||||
|
this.onChangeModel,
|
||||||
this.onOverrideVisibility,
|
this.onOverrideVisibility,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -48,6 +50,7 @@ class AIMessageActionBar extends StatefulWidget {
|
|||||||
final bool showDecoration;
|
final bool showDecoration;
|
||||||
final void Function()? onRegenerate;
|
final void Function()? onRegenerate;
|
||||||
final void Function(PredefinedFormat)? onChangeFormat;
|
final void Function(PredefinedFormat)? onChangeFormat;
|
||||||
|
final void Function(AIModelPB)? onChangeModel;
|
||||||
final void Function(bool)? onOverrideVisibility;
|
final void Function(bool)? onOverrideVisibility;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -126,6 +129,12 @@ class _AIMessageActionBarState extends State<AIMessageActionBar> {
|
|||||||
popoverMutex: popoverMutex,
|
popoverMutex: popoverMutex,
|
||||||
onOverrideVisibility: widget.onOverrideVisibility,
|
onOverrideVisibility: widget.onOverrideVisibility,
|
||||||
),
|
),
|
||||||
|
ChangeModelButton(
|
||||||
|
isInHoverBar: widget.showDecoration,
|
||||||
|
onRegenerate: widget.onChangeModel,
|
||||||
|
popoverMutex: popoverMutex,
|
||||||
|
onOverrideVisibility: widget.onOverrideVisibility,
|
||||||
|
),
|
||||||
SaveToPageButton(
|
SaveToPageButton(
|
||||||
textMessage: widget.message as TextMessage,
|
textMessage: widget.message as TextMessage,
|
||||||
isInHoverBar: widget.showDecoration,
|
isInHoverBar: widget.showDecoration,
|
||||||
@ -405,6 +414,85 @@ class _ChangeFormatPopoverContentState
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ChangeModelButton extends StatefulWidget {
|
||||||
|
const ChangeModelButton({
|
||||||
|
super.key,
|
||||||
|
required this.isInHoverBar,
|
||||||
|
this.popoverMutex,
|
||||||
|
this.onRegenerate,
|
||||||
|
this.onOverrideVisibility,
|
||||||
|
});
|
||||||
|
|
||||||
|
final bool isInHoverBar;
|
||||||
|
final PopoverMutex? popoverMutex;
|
||||||
|
final void Function(AIModelPB)? onRegenerate;
|
||||||
|
final void Function(bool)? onOverrideVisibility;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ChangeModelButton> createState() => _ChangeModelButtonState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ChangeModelButtonState extends State<ChangeModelButton> {
|
||||||
|
final popoverController = PopoverController();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AppFlowyPopover(
|
||||||
|
controller: popoverController,
|
||||||
|
mutex: widget.popoverMutex,
|
||||||
|
triggerActions: PopoverTriggerFlags.none,
|
||||||
|
margin: EdgeInsets.zero,
|
||||||
|
offset: Offset(8, 0),
|
||||||
|
direction: PopoverDirection.rightWithBottomAligned,
|
||||||
|
constraints: BoxConstraints(maxWidth: 250, maxHeight: 600),
|
||||||
|
onClose: () => widget.onOverrideVisibility?.call(false),
|
||||||
|
child: buildButton(context),
|
||||||
|
popupBuilder: (_) {
|
||||||
|
final bloc = context.read<AIPromptInputBloc>();
|
||||||
|
final (models, _) = bloc.aiModelStateNotifier.getAvailableModels();
|
||||||
|
return SelectModelPopoverContent(
|
||||||
|
models: models,
|
||||||
|
selectedModel: null,
|
||||||
|
onSelectModel: widget.onRegenerate,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildButton(BuildContext context) {
|
||||||
|
return FlowyTooltip(
|
||||||
|
message: LocaleKeys.chat_switchModel_label.tr(),
|
||||||
|
child: FlowyIconButton(
|
||||||
|
width: 32.0,
|
||||||
|
height: DesktopAIChatSizes.messageActionBarIconSize,
|
||||||
|
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||||
|
radius: widget.isInHoverBar
|
||||||
|
? DesktopAIChatSizes.messageHoverActionBarIconRadius
|
||||||
|
: DesktopAIChatSizes.messageActionBarIconRadius,
|
||||||
|
icon: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
FlowySvg(
|
||||||
|
FlowySvgs.ai_sparks_s,
|
||||||
|
color: Theme.of(context).hintColor,
|
||||||
|
size: const Size.square(16),
|
||||||
|
),
|
||||||
|
FlowySvg(
|
||||||
|
FlowySvgs.ai_source_drop_down_s,
|
||||||
|
color: Theme.of(context).hintColor,
|
||||||
|
size: const Size.square(8),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
widget.onOverrideVisibility?.call(true);
|
||||||
|
popoverController.show();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class SaveToPageButton extends StatefulWidget {
|
class SaveToPageButton extends StatefulWidget {
|
||||||
const SaveToPageButton({
|
const SaveToPageButton({
|
||||||
super.key,
|
super.key,
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import 'package:appflowy/shared/markdown_to_document.dart';
|
|||||||
import 'package:appflowy/startup/startup.dart';
|
import 'package:appflowy/startup/startup.dart';
|
||||||
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-ai/protobuf.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra/theme_extension.dart';
|
import 'package:flowy_infra/theme_extension.dart';
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
@ -23,6 +24,7 @@ import 'package:universal_platform/universal_platform.dart';
|
|||||||
|
|
||||||
import '../chat_avatar.dart';
|
import '../chat_avatar.dart';
|
||||||
import '../layout_define.dart';
|
import '../layout_define.dart';
|
||||||
|
import 'ai_change_model_bottom_sheet.dart';
|
||||||
import 'ai_message_action_bar.dart';
|
import 'ai_message_action_bar.dart';
|
||||||
import 'ai_change_format_bottom_sheet.dart';
|
import 'ai_change_format_bottom_sheet.dart';
|
||||||
import 'message_util.dart';
|
import 'message_util.dart';
|
||||||
@ -41,6 +43,7 @@ class ChatAIMessageBubble extends StatelessWidget {
|
|||||||
this.isSelectingMessages = false,
|
this.isSelectingMessages = false,
|
||||||
this.onRegenerate,
|
this.onRegenerate,
|
||||||
this.onChangeFormat,
|
this.onChangeFormat,
|
||||||
|
this.onChangeModel,
|
||||||
});
|
});
|
||||||
|
|
||||||
final Message message;
|
final Message message;
|
||||||
@ -50,6 +53,7 @@ class ChatAIMessageBubble extends StatelessWidget {
|
|||||||
final bool isSelectingMessages;
|
final bool isSelectingMessages;
|
||||||
final void Function()? onRegenerate;
|
final void Function()? onRegenerate;
|
||||||
final void Function(PredefinedFormat)? onChangeFormat;
|
final void Function(PredefinedFormat)? onChangeFormat;
|
||||||
|
final void Function(AIModelPB)? onChangeModel;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -73,6 +77,7 @@ class ChatAIMessageBubble extends StatelessWidget {
|
|||||||
message: message,
|
message: message,
|
||||||
onRegenerate: onRegenerate,
|
onRegenerate: onRegenerate,
|
||||||
onChangeFormat: onChangeFormat,
|
onChangeFormat: onChangeFormat,
|
||||||
|
onChangeModel: onChangeModel,
|
||||||
child: child,
|
child: child,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -82,6 +87,7 @@ class ChatAIMessageBubble extends StatelessWidget {
|
|||||||
message: message,
|
message: message,
|
||||||
onRegenerate: onRegenerate,
|
onRegenerate: onRegenerate,
|
||||||
onChangeFormat: onChangeFormat,
|
onChangeFormat: onChangeFormat,
|
||||||
|
onChangeModel: onChangeModel,
|
||||||
child: child,
|
child: child,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -91,6 +97,7 @@ class ChatAIMessageBubble extends StatelessWidget {
|
|||||||
message: message,
|
message: message,
|
||||||
onRegenerate: onRegenerate,
|
onRegenerate: onRegenerate,
|
||||||
onChangeFormat: onChangeFormat,
|
onChangeFormat: onChangeFormat,
|
||||||
|
onChangeModel: onChangeModel,
|
||||||
child: child,
|
child: child,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -103,12 +110,14 @@ class ChatAIBottomInlineActions extends StatelessWidget {
|
|||||||
required this.message,
|
required this.message,
|
||||||
this.onRegenerate,
|
this.onRegenerate,
|
||||||
this.onChangeFormat,
|
this.onChangeFormat,
|
||||||
|
this.onChangeModel,
|
||||||
});
|
});
|
||||||
|
|
||||||
final Widget child;
|
final Widget child;
|
||||||
final Message message;
|
final Message message;
|
||||||
final void Function()? onRegenerate;
|
final void Function()? onRegenerate;
|
||||||
final void Function(PredefinedFormat)? onChangeFormat;
|
final void Function(PredefinedFormat)? onChangeFormat;
|
||||||
|
final void Function(AIModelPB)? onChangeModel;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -127,6 +136,7 @@ class ChatAIBottomInlineActions extends StatelessWidget {
|
|||||||
showDecoration: false,
|
showDecoration: false,
|
||||||
onRegenerate: onRegenerate,
|
onRegenerate: onRegenerate,
|
||||||
onChangeFormat: onChangeFormat,
|
onChangeFormat: onChangeFormat,
|
||||||
|
onChangeModel: onChangeModel,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const VSpace(32.0),
|
const VSpace(32.0),
|
||||||
@ -142,12 +152,14 @@ class ChatAIMessageHover extends StatefulWidget {
|
|||||||
required this.message,
|
required this.message,
|
||||||
this.onRegenerate,
|
this.onRegenerate,
|
||||||
this.onChangeFormat,
|
this.onChangeFormat,
|
||||||
|
this.onChangeModel,
|
||||||
});
|
});
|
||||||
|
|
||||||
final Widget child;
|
final Widget child;
|
||||||
final Message message;
|
final Message message;
|
||||||
final void Function()? onRegenerate;
|
final void Function()? onRegenerate;
|
||||||
final void Function(PredefinedFormat)? onChangeFormat;
|
final void Function(PredefinedFormat)? onChangeFormat;
|
||||||
|
final void Function(AIModelPB)? onChangeModel;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<ChatAIMessageHover> createState() => _ChatAIMessageHoverState();
|
State<ChatAIMessageHover> createState() => _ChatAIMessageHoverState();
|
||||||
@ -229,6 +241,7 @@ class _ChatAIMessageHoverState extends State<ChatAIMessageHover> {
|
|||||||
showDecoration: true,
|
showDecoration: true,
|
||||||
onRegenerate: widget.onRegenerate,
|
onRegenerate: widget.onRegenerate,
|
||||||
onChangeFormat: widget.onChangeFormat,
|
onChangeFormat: widget.onChangeFormat,
|
||||||
|
onChangeModel: widget.onChangeModel,
|
||||||
onOverrideVisibility: (visibility) {
|
onOverrideVisibility: (visibility) {
|
||||||
overrideVisibility = visibility;
|
overrideVisibility = visibility;
|
||||||
},
|
},
|
||||||
@ -302,12 +315,14 @@ class ChatAIMessagePopup extends StatelessWidget {
|
|||||||
required this.message,
|
required this.message,
|
||||||
this.onRegenerate,
|
this.onRegenerate,
|
||||||
this.onChangeFormat,
|
this.onChangeFormat,
|
||||||
|
this.onChangeModel,
|
||||||
});
|
});
|
||||||
|
|
||||||
final Widget child;
|
final Widget child;
|
||||||
final Message message;
|
final Message message;
|
||||||
final void Function()? onRegenerate;
|
final void Function()? onRegenerate;
|
||||||
final void Function(PredefinedFormat)? onChangeFormat;
|
final void Function(PredefinedFormat)? onChangeFormat;
|
||||||
|
final void Function(AIModelPB)? onChangeModel;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -328,6 +343,8 @@ class ChatAIMessagePopup extends StatelessWidget {
|
|||||||
_divider(),
|
_divider(),
|
||||||
_changeFormatButton(context),
|
_changeFormatButton(context),
|
||||||
_divider(),
|
_divider(),
|
||||||
|
_changeModelButton(context),
|
||||||
|
_divider(),
|
||||||
_saveToPageButton(context),
|
_saveToPageButton(context),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@ -399,6 +416,25 @@ class ChatAIMessagePopup extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _changeModelButton(BuildContext context) {
|
||||||
|
return MobileQuickActionButton(
|
||||||
|
onTap: () async {
|
||||||
|
final bloc = context.read<AIPromptInputBloc>();
|
||||||
|
final (models, _) = bloc.aiModelStateNotifier.getAvailableModels();
|
||||||
|
final result = await showChangeModelBottomSheet(context, models);
|
||||||
|
if (result != null) {
|
||||||
|
onChangeModel?.call(result);
|
||||||
|
if (context.mounted) {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
icon: FlowySvgs.ai_sparks_s,
|
||||||
|
iconSize: const Size.square(20),
|
||||||
|
text: LocaleKeys.chat_switchModel_label.tr(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _saveToPageButton(BuildContext context) {
|
Widget _saveToPageButton(BuildContext context) {
|
||||||
return MobileQuickActionButton(
|
return MobileQuickActionButton(
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
|
|||||||
@ -4,6 +4,7 @@ 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/application/chat_bloc.dart';
|
||||||
import 'package:appflowy/plugins/ai_chat/application/chat_entity.dart';
|
import 'package:appflowy/plugins/ai_chat/application/chat_entity.dart';
|
||||||
import 'package:appflowy/plugins/ai_chat/application/chat_message_stream.dart';
|
import 'package:appflowy/plugins/ai_chat/application/chat_message_stream.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-ai/protobuf.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:fixnum/fixnum.dart';
|
import 'package:fixnum/fixnum.dart';
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
@ -36,6 +37,7 @@ class ChatAIMessageWidget extends StatelessWidget {
|
|||||||
this.onSelectedMetadata,
|
this.onSelectedMetadata,
|
||||||
this.onRegenerate,
|
this.onRegenerate,
|
||||||
this.onChangeFormat,
|
this.onChangeFormat,
|
||||||
|
this.onChangeModel,
|
||||||
this.isLastMessage = false,
|
this.isLastMessage = false,
|
||||||
this.isStreaming = false,
|
this.isStreaming = false,
|
||||||
this.isSelectingMessages = false,
|
this.isSelectingMessages = false,
|
||||||
@ -53,6 +55,7 @@ class ChatAIMessageWidget extends StatelessWidget {
|
|||||||
final void Function()? onRegenerate;
|
final void Function()? onRegenerate;
|
||||||
final void Function() onStopStream;
|
final void Function() onStopStream;
|
||||||
final void Function(PredefinedFormat)? onChangeFormat;
|
final void Function(PredefinedFormat)? onChangeFormat;
|
||||||
|
final void Function(AIModelPB)? onChangeModel;
|
||||||
final bool isStreaming;
|
final bool isStreaming;
|
||||||
final bool isLastMessage;
|
final bool isLastMessage;
|
||||||
final bool isSelectingMessages;
|
final bool isSelectingMessages;
|
||||||
@ -110,6 +113,7 @@ class ChatAIMessageWidget extends StatelessWidget {
|
|||||||
isSelectingMessages: isSelectingMessages,
|
isSelectingMessages: isSelectingMessages,
|
||||||
onRegenerate: onRegenerate,
|
onRegenerate: onRegenerate,
|
||||||
onChangeFormat: onChangeFormat,
|
onChangeFormat: onChangeFormat,
|
||||||
|
onChangeModel: onChangeModel,
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
|||||||
@ -247,14 +247,16 @@
|
|||||||
"table": "Table",
|
"table": "Table",
|
||||||
"blankDescription": "Format response",
|
"blankDescription": "Format response",
|
||||||
"defaultDescription": "Auto mode",
|
"defaultDescription": "Auto mode",
|
||||||
"localModel": "Local Model",
|
|
||||||
"cloudModel": "Cloud Model",
|
|
||||||
"switchModel": "Switch model",
|
|
||||||
"textWithImageDescription": "@:chat.changeFormat.text with image",
|
"textWithImageDescription": "@:chat.changeFormat.text with image",
|
||||||
"numberWithImageDescription": "@:chat.changeFormat.number with image",
|
"numberWithImageDescription": "@:chat.changeFormat.number with image",
|
||||||
"bulletWithImageDescription": "@:chat.changeFormat.bullet with image",
|
"bulletWithImageDescription": "@:chat.changeFormat.bullet with image",
|
||||||
"tableWithImageDescription": "@:chat.changeFormat.table with image"
|
"tableWithImageDescription": "@:chat.changeFormat.table with image"
|
||||||
},
|
},
|
||||||
|
"switchModel": {
|
||||||
|
"label": "Switch model",
|
||||||
|
"localModel": "Local Model",
|
||||||
|
"cloudModel": "Cloud Model"
|
||||||
|
},
|
||||||
"selectBanner": {
|
"selectBanner": {
|
||||||
"saveButton": "Add to …",
|
"saveButton": "Add to …",
|
||||||
"selectMessages": "Select messages",
|
"selectMessages": "Select messages",
|
||||||
|
|||||||
@ -248,22 +248,23 @@ impl AIManager {
|
|||||||
answer_message_id: i64,
|
answer_message_id: i64,
|
||||||
answer_stream_port: i64,
|
answer_stream_port: i64,
|
||||||
format: Option<PredefinedFormatPB>,
|
format: Option<PredefinedFormatPB>,
|
||||||
|
model: Option<AIModelPB>,
|
||||||
) -> FlowyResult<()> {
|
) -> FlowyResult<()> {
|
||||||
let chat = self.get_or_create_chat_instance(chat_id).await?;
|
let chat = self.get_or_create_chat_instance(chat_id).await?;
|
||||||
let question_message_id = chat
|
let question_message_id = chat
|
||||||
.get_question_id_from_answer_id(answer_message_id)
|
.get_question_id_from_answer_id(answer_message_id)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let preferred_model = self
|
let model = model.map_or_else(
|
||||||
.store_preferences
|
|| {
|
||||||
.get_object::<AIModel>(&ai_available_models_key(chat_id));
|
self
|
||||||
|
.store_preferences
|
||||||
|
.get_object::<AIModel>(&ai_available_models_key(chat_id))
|
||||||
|
},
|
||||||
|
|model| Some(model.into()),
|
||||||
|
);
|
||||||
chat
|
chat
|
||||||
.stream_regenerate_response(
|
.stream_regenerate_response(question_message_id, answer_stream_port, format, model)
|
||||||
question_message_id,
|
|
||||||
answer_stream_port,
|
|
||||||
format,
|
|
||||||
preferred_model,
|
|
||||||
)
|
|
||||||
.await?;
|
.await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@ -102,6 +102,9 @@ pub struct RegenerateResponsePB {
|
|||||||
|
|
||||||
#[pb(index = 4, one_of)]
|
#[pb(index = 4, one_of)]
|
||||||
pub format: Option<PredefinedFormatPB>,
|
pub format: Option<PredefinedFormatPB>,
|
||||||
|
|
||||||
|
#[pb(index = 5, one_of)]
|
||||||
|
pub model: Option<AIModelPB>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, ProtoBuf, Validate, Clone, Debug)]
|
#[derive(Default, ProtoBuf, Validate, Clone, Debug)]
|
||||||
|
|||||||
@ -99,6 +99,7 @@ pub(crate) async fn regenerate_response_handler(
|
|||||||
data.answer_message_id,
|
data.answer_message_id,
|
||||||
data.answer_stream_port,
|
data.answer_stream_port,
|
||||||
data.format,
|
data.format,
|
||||||
|
data.model,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user