diff --git a/frontend/appflowy_flutter/lib/ai/service/ai_model_state_notifier.dart b/frontend/appflowy_flutter/lib/ai/service/ai_model_state_notifier.dart index 783046a3d0..1d9ba5bf0e 100644 --- a/frontend/appflowy_flutter/lib/ai/service/ai_model_state_notifier.dart +++ b/frontend/appflowy_flutter/lib/ai/service/ai_model_state_notifier.dart @@ -7,7 +7,6 @@ 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(AIModelState state); @@ -52,25 +51,29 @@ class AIModelStateNotifier { final String objectId; final LocalAIStateListener? _localAIListener; final AIModelSwitchListener _aiModelSwitchListener; - LocalAIPB? _localAIState; - ModelSelectionPB? _sourceModelSelection; - // callbacks + LocalAIPB? _localAIState; + ModelSelectionPB? _modelSelection; + + AIModelState _currentState = _defaultState(); + List _availableModels = []; + AIModelPB? _selectedModel; + final List _stateChangedCallbacks = []; final List _availableModelsChangedCallbacks = []; + /// Starts platform-specific listeners void _startListening() { if (UniversalPlatform.isDesktop) { _localAIListener?.start( stateCallback: (state) async { _localAIState = state; - _notifyStateChanged(); - + _updateAll(); if (state.state == RunningStatePB.Running || state.state == RunningStatePB.Stopped) { await _loadModelSelection(); - _notifyAvailableModelsChanged(); + _updateAll(); } }, ); @@ -78,25 +81,25 @@ class AIModelStateNotifier { _aiModelSwitchListener.start( onUpdateSelectedModel: (model) async { - final updatedModels = _sourceModelSelection?.deepCopy() - ?..selectedModel = model; - _sourceModelSelection = updatedModels; - - _notifyAvailableModelsChanged(); + _selectedModel = model; + _updateAll(); if (model.isLocal && UniversalPlatform.isDesktop) { - await _loadLocalAiState(); + await _loadLocalState(); + _updateAll(); } - _notifyStateChanged(); }, ); } - void _init() async { - await Future.wait([_loadLocalAiState(), _loadModelSelection()]); - _notifyStateChanged(); - _notifyAvailableModelsChanged(); + Future _init() async { + await Future.wait([ + if (UniversalPlatform.isDesktop) _loadLocalState(), + _loadModelSelection(), + ]); + _updateAll(); } + /// Register callbacks for state or available-models changes void addListener({ OnModelStateChangedCallback? onStateChanged, OnAvailableModelsChangedCallback? onAvailableModelsChanged, @@ -109,6 +112,7 @@ class AIModelStateNotifier { } } + /// Remove previously registered callbacks void removeListener({ OnModelStateChangedCallback? onStateChanged, OnAvailableModelsChangedCallback? onAvailableModelsChanged, @@ -128,116 +132,88 @@ class AIModelStateNotifier { await _aiModelSwitchListener.stop(); } - AIModelState getState() { - if (UniversalPlatform.isMobile) { - return AIModelState( - type: AiType.cloud, - hintText: LocaleKeys.chat_inputMessageHint.tr(), - tooltip: null, - isEditable: true, - localAIEnabled: false, - ); + /// Returns current AIModelState + AIModelState getState() => _currentState; + + /// Returns available models and the selected model + (List, AIModelPB?) getModelSelection() => + (_availableModels, _selectedModel); + + void _updateAll() { + _currentState = _computeState(); + for (final cb in _stateChangedCallbacks) { + cb(_currentState); } - - final availableModels = _sourceModelSelection; - final localAiState = _localAIState; - - if (availableModels == null) { - return AIModelState( - type: AiType.cloud, - hintText: LocaleKeys.chat_inputMessageHint.tr(), - isEditable: true, - tooltip: null, - localAIEnabled: false, - ); - } - if (localAiState == null) { - return AIModelState( - type: AiType.cloud, - hintText: LocaleKeys.chat_inputMessageHint.tr(), - tooltip: null, - isEditable: true, - localAIEnabled: false, - ); + for (final cb in _availableModelsChangedCallbacks) { + cb(_availableModels, _selectedModel); } + } - if (!availableModels.selectedModel.isLocal) { - return AIModelState( - type: AiType.cloud, - hintText: LocaleKeys.chat_inputMessageHint.tr(), - tooltip: null, - isEditable: true, - localAIEnabled: false, - ); - } - - final editable = localAiState.state == RunningStatePB.Running; - final tooltip = localAiState.enabled - ? (editable - ? null - : LocaleKeys.settings_aiPage_keys_localAINotReadyTextFieldPrompt - .tr()) - : LocaleKeys.settings_aiPage_keys_localAIDisabledTextFieldPrompt.tr(); - - final hintText = localAiState.enabled - ? (editable - ? LocaleKeys.chat_inputLocalAIMessageHint.tr() - : LocaleKeys.settings_aiPage_keys_localAIInitializing.tr()) - : LocaleKeys.settings_aiPage_keys_localAIDisabled.tr(); - - return AIModelState( - type: AiType.local, - hintText: hintText, - tooltip: tooltip, - isEditable: editable, - localAIEnabled: localAiState.enabled, + Future _loadModelSelection() async { + await AIEventGetSourceModelSelection( + ModelSourcePB(source: objectId), + ).send().fold( + (ms) { + _modelSelection = ms; + _availableModels = ms.models; + _selectedModel = ms.selectedModel; + }, + (e) => Log.error("Failed to fetch models: \$e"), ); } - (List, AIModelPB?) getModelSelection() { - final availableModels = _sourceModelSelection; - if (availableModels == null) { - return ([], null); - } - return (availableModels.models, availableModels.selectedModel); - } - - void _notifyAvailableModelsChanged() { - final (models, selectedModel) = getModelSelection(); - for (final callback in _availableModelsChangedCallbacks) { - callback(models, selectedModel); - } - } - - void _notifyStateChanged() { - final state = getState(); - for (final callback in _stateChangedCallbacks) { - callback(state); - } - } - - Future _loadModelSelection() { - final payload = ModelSourcePB(source: objectId); - return AIEventGetSourceModelSelection(payload).send().fold( - (models) => _sourceModelSelection = models, - (err) => Log.error("Failed to get available models: $err"), + Future _loadLocalState() async { + await AIEventGetLocalAIState().send().fold( + (s) => _localAIState = s, + (e) => Log.error("Failed to fetch local AI state: \$e"), ); } - Future _loadLocalAiState() { - return AIEventGetLocalAIState().send().fold( - (localAIState) => _localAIState = localAIState, - (error) => Log.error("Failed to get local AI state: $error"), - ); + static AIModelState _defaultState() => AIModelState( + type: AiType.cloud, + hintText: LocaleKeys.chat_inputMessageHint.tr(), + tooltip: null, + isEditable: true, + localAIEnabled: false, + ); + + /// Core logic computing the state from local and selection data + AIModelState _computeState() { + if (UniversalPlatform.isMobile) return _defaultState(); + + if (_modelSelection == null || _localAIState == null) { + return _defaultState(); + } + + if (!_selectedModel!.isLocal) { + return _defaultState(); + } + + final enabled = _localAIState!.enabled; + final running = _localAIState!.state == RunningStatePB.Running; + final hintKey = enabled + ? (running + ? LocaleKeys.chat_inputLocalAIMessageHint + : LocaleKeys.settings_aiPage_keys_localAIInitializing) + : LocaleKeys.settings_aiPage_keys_localAIDisabled; + final tooltipKey = enabled + ? (running + ? null + : LocaleKeys.settings_aiPage_keys_localAINotReadyTextFieldPrompt) + : LocaleKeys.settings_aiPage_keys_localAIDisabledTextFieldPrompt; + + return AIModelState( + type: AiType.local, + hintText: hintKey.tr(), + tooltip: tooltipKey?.tr(), + isEditable: running, + localAIEnabled: enabled, + ); } } -extension AiModelExtension on AIModelPB { - bool get isDefault { - return name == "Auto"; - } - - String get i18n { - return isDefault ? LocaleKeys.chat_switchModel_autoModel.tr() : name; - } +extension AIModelPBExtension on AIModelPB { + bool get isDefault => name == 'Auto'; + String get i18n => + isDefault ? LocaleKeys.chat_switchModel_autoModel.tr() : name; } diff --git a/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/select_model_menu.dart b/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/select_model_menu.dart index 317f90ac21..d826be78d8 100644 --- a/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/select_model_menu.dart +++ b/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/select_model_menu.dart @@ -217,51 +217,41 @@ class _CurrentModelButton extends StatelessWidget { behavior: HitTestBehavior.opaque, child: SizedBox( height: DesktopAIPromptSizes.actionBarButtonSize, - child: AnimatedSize( - duration: const Duration(milliseconds: 200), - curve: Curves.easeOutCubic, - alignment: AlignmentDirectional.centerStart, - clipBehavior: Clip.none, - child: FlowyHover( - style: const HoverStyle( - borderRadius: BorderRadius.all(Radius.circular(8)), - ), - child: Padding( - padding: const EdgeInsetsDirectional.all(4.0), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - // TODO: remove this after change icon to 20px - padding: EdgeInsets.all(2), - child: FlowySvg( - FlowySvgs.ai_sparks_s, - color: Theme.of(context).hintColor, - size: Size.square(16), - ), - ), - if (model != null && !model!.isDefault) - AnimatedSize( - duration: const Duration(milliseconds: 150), - curve: Curves.easeOutCubic, - child: Padding( - padding: EdgeInsetsDirectional.only(end: 2.0), - child: FlowyText( - model!.i18n, - fontSize: 12, - figmaLineHeight: 16, - color: Theme.of(context).hintColor, - overflow: TextOverflow.ellipsis, - ), - ), - ), - FlowySvg( - FlowySvgs.ai_source_drop_down_s, + child: FlowyHover( + style: const HoverStyle( + borderRadius: BorderRadius.all(Radius.circular(8)), + ), + child: Padding( + padding: const EdgeInsetsDirectional.all(4.0), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + // TODO: remove this after change icon to 20px + padding: EdgeInsets.all(2), + child: FlowySvg( + FlowySvgs.ai_sparks_s, color: Theme.of(context).hintColor, - size: const Size.square(8), + size: Size.square(16), ), - ], - ), + ), + if (model != null && !model!.isDefault) + Padding( + padding: EdgeInsetsDirectional.only(end: 2.0), + child: FlowyText( + model!.i18n, + fontSize: 12, + figmaLineHeight: 16, + color: Theme.of(context).hintColor, + overflow: TextOverflow.ellipsis, + ), + ), + FlowySvg( + FlowySvgs.ai_source_drop_down_s, + color: Theme.of(context).hintColor, + size: const Size.square(8), + ), + ], ), ), ), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/shared/sidebar_setting.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/shared/sidebar_setting.dart index d1e32985fa..d21605bc00 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/shared/sidebar_setting.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/shared/sidebar_setting.dart @@ -31,7 +31,10 @@ HotKeyItem openSettingsHotKey( ), keyDownHandler: (_) { if (_settingsDialogKey.currentContext == null) { - showSettingsDialog(context); + showSettingsDialog( + context, + userWorkspaceBloc: context.read(), + ); } else { Navigator.of(context, rootNavigator: true) .popUntil((route) => route.isFirst); @@ -110,7 +113,7 @@ class _UserSettingButtonState extends State { void showSettingsDialog( BuildContext context, { - UserWorkspaceBloc? userWorkspaceBloc, + required UserWorkspaceBloc userWorkspaceBloc, PasswordBloc? passwordBloc, SettingsPage? initPage, }) { @@ -126,7 +129,7 @@ void showSettingsDialog( ) : BlocProvider( create: (context) => PasswordBloc( - context.read().state.userProfile, + userWorkspaceBloc.state.userProfile, ) ..add(PasswordEvent.init()) ..add(PasswordEvent.checkHasPassword()), @@ -135,11 +138,11 @@ void showSettingsDialog( value: BlocProvider.of(dialogContext), ), BlocProvider.value( - value: userWorkspaceBloc ?? context.read(), + value: userWorkspaceBloc, ), ], child: SettingsDialog( - context.read().state.userProfile, + userWorkspaceBloc.state.userProfile, initPage: initPage, didLogout: () async { // Pop the dialog using the dialog context