chore: update

This commit is contained in:
Nathan 2025-04-26 10:40:33 +08:00
parent 549e8aee03
commit f374ca1574
3 changed files with 135 additions and 166 deletions

View File

@ -7,7 +7,6 @@ import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart';
import 'package:appflowy_result/appflowy_result.dart'; import 'package:appflowy_result/appflowy_result.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:protobuf/protobuf.dart';
import 'package:universal_platform/universal_platform.dart'; import 'package:universal_platform/universal_platform.dart';
typedef OnModelStateChangedCallback = void Function(AIModelState state); typedef OnModelStateChangedCallback = void Function(AIModelState state);
@ -52,25 +51,29 @@ class AIModelStateNotifier {
final String objectId; final String objectId;
final LocalAIStateListener? _localAIListener; final LocalAIStateListener? _localAIListener;
final AIModelSwitchListener _aiModelSwitchListener; final AIModelSwitchListener _aiModelSwitchListener;
LocalAIPB? _localAIState;
ModelSelectionPB? _sourceModelSelection;
// callbacks LocalAIPB? _localAIState;
ModelSelectionPB? _modelSelection;
AIModelState _currentState = _defaultState();
List<AIModelPB> _availableModels = [];
AIModelPB? _selectedModel;
final List<OnModelStateChangedCallback> _stateChangedCallbacks = []; final List<OnModelStateChangedCallback> _stateChangedCallbacks = [];
final List<OnAvailableModelsChangedCallback> final List<OnAvailableModelsChangedCallback>
_availableModelsChangedCallbacks = []; _availableModelsChangedCallbacks = [];
/// Starts platform-specific listeners
void _startListening() { void _startListening() {
if (UniversalPlatform.isDesktop) { if (UniversalPlatform.isDesktop) {
_localAIListener?.start( _localAIListener?.start(
stateCallback: (state) async { stateCallback: (state) async {
_localAIState = state; _localAIState = state;
_notifyStateChanged(); _updateAll();
if (state.state == RunningStatePB.Running || if (state.state == RunningStatePB.Running ||
state.state == RunningStatePB.Stopped) { state.state == RunningStatePB.Stopped) {
await _loadModelSelection(); await _loadModelSelection();
_notifyAvailableModelsChanged(); _updateAll();
} }
}, },
); );
@ -78,25 +81,25 @@ class AIModelStateNotifier {
_aiModelSwitchListener.start( _aiModelSwitchListener.start(
onUpdateSelectedModel: (model) async { onUpdateSelectedModel: (model) async {
final updatedModels = _sourceModelSelection?.deepCopy() _selectedModel = model;
?..selectedModel = model; _updateAll();
_sourceModelSelection = updatedModels;
_notifyAvailableModelsChanged();
if (model.isLocal && UniversalPlatform.isDesktop) { if (model.isLocal && UniversalPlatform.isDesktop) {
await _loadLocalAiState(); await _loadLocalState();
_updateAll();
} }
_notifyStateChanged();
}, },
); );
} }
void _init() async { Future<void> _init() async {
await Future.wait([_loadLocalAiState(), _loadModelSelection()]); await Future.wait([
_notifyStateChanged(); if (UniversalPlatform.isDesktop) _loadLocalState(),
_notifyAvailableModelsChanged(); _loadModelSelection(),
]);
_updateAll();
} }
/// Register callbacks for state or available-models changes
void addListener({ void addListener({
OnModelStateChangedCallback? onStateChanged, OnModelStateChangedCallback? onStateChanged,
OnAvailableModelsChangedCallback? onAvailableModelsChanged, OnAvailableModelsChangedCallback? onAvailableModelsChanged,
@ -109,6 +112,7 @@ class AIModelStateNotifier {
} }
} }
/// Remove previously registered callbacks
void removeListener({ void removeListener({
OnModelStateChangedCallback? onStateChanged, OnModelStateChangedCallback? onStateChanged,
OnAvailableModelsChangedCallback? onAvailableModelsChanged, OnAvailableModelsChangedCallback? onAvailableModelsChanged,
@ -128,116 +132,88 @@ class AIModelStateNotifier {
await _aiModelSwitchListener.stop(); await _aiModelSwitchListener.stop();
} }
AIModelState getState() { /// Returns current AIModelState
if (UniversalPlatform.isMobile) { AIModelState getState() => _currentState;
return AIModelState(
type: AiType.cloud, /// Returns available models and the selected model
hintText: LocaleKeys.chat_inputMessageHint.tr(), (List<AIModelPB>, AIModelPB?) getModelSelection() =>
tooltip: null, (_availableModels, _selectedModel);
isEditable: true,
localAIEnabled: false, void _updateAll() {
); _currentState = _computeState();
for (final cb in _stateChangedCallbacks) {
cb(_currentState);
} }
for (final cb in _availableModelsChangedCallbacks) {
final availableModels = _sourceModelSelection; cb(_availableModels, _selectedModel);
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,
);
} }
}
if (!availableModels.selectedModel.isLocal) { Future<void> _loadModelSelection() async {
return AIModelState( await AIEventGetSourceModelSelection(
type: AiType.cloud, ModelSourcePB(source: objectId),
hintText: LocaleKeys.chat_inputMessageHint.tr(), ).send().fold(
tooltip: null, (ms) {
isEditable: true, _modelSelection = ms;
localAIEnabled: false, _availableModels = ms.models;
); _selectedModel = ms.selectedModel;
} },
(e) => Log.error("Failed to fetch models: \$e"),
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,
); );
} }
(List<AIModelPB>, AIModelPB?) getModelSelection() { Future<void> _loadLocalState() async {
final availableModels = _sourceModelSelection; await AIEventGetLocalAIState().send().fold(
if (availableModels == null) { (s) => _localAIState = s,
return ([], null); (e) => Log.error("Failed to fetch local AI state: \$e"),
}
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<void> _loadModelSelection() {
final payload = ModelSourcePB(source: objectId);
return AIEventGetSourceModelSelection(payload).send().fold(
(models) => _sourceModelSelection = models,
(err) => Log.error("Failed to get available models: $err"),
); );
} }
Future<void> _loadLocalAiState() { static AIModelState _defaultState() => AIModelState(
return AIEventGetLocalAIState().send().fold( type: AiType.cloud,
(localAIState) => _localAIState = localAIState, hintText: LocaleKeys.chat_inputMessageHint.tr(),
(error) => Log.error("Failed to get local AI state: $error"), 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 { extension AIModelPBExtension on AIModelPB {
bool get isDefault { bool get isDefault => name == 'Auto';
return name == "Auto"; String get i18n =>
} isDefault ? LocaleKeys.chat_switchModel_autoModel.tr() : name;
String get i18n {
return isDefault ? LocaleKeys.chat_switchModel_autoModel.tr() : name;
}
} }

View File

@ -217,51 +217,41 @@ class _CurrentModelButton extends StatelessWidget {
behavior: HitTestBehavior.opaque, behavior: HitTestBehavior.opaque,
child: SizedBox( child: SizedBox(
height: DesktopAIPromptSizes.actionBarButtonSize, height: DesktopAIPromptSizes.actionBarButtonSize,
child: AnimatedSize( child: FlowyHover(
duration: const Duration(milliseconds: 200), style: const HoverStyle(
curve: Curves.easeOutCubic, borderRadius: BorderRadius.all(Radius.circular(8)),
alignment: AlignmentDirectional.centerStart, ),
clipBehavior: Clip.none, child: Padding(
child: FlowyHover( padding: const EdgeInsetsDirectional.all(4.0),
style: const HoverStyle( child: Row(
borderRadius: BorderRadius.all(Radius.circular(8)), mainAxisSize: MainAxisSize.min,
), children: [
child: Padding( Padding(
padding: const EdgeInsetsDirectional.all(4.0), // TODO: remove this after change icon to 20px
child: Row( padding: EdgeInsets.all(2),
mainAxisSize: MainAxisSize.min, child: FlowySvg(
children: [ FlowySvgs.ai_sparks_s,
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,
color: Theme.of(context).hintColor, 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),
),
],
), ),
), ),
), ),

View File

@ -31,7 +31,10 @@ HotKeyItem openSettingsHotKey(
), ),
keyDownHandler: (_) { keyDownHandler: (_) {
if (_settingsDialogKey.currentContext == null) { if (_settingsDialogKey.currentContext == null) {
showSettingsDialog(context); showSettingsDialog(
context,
userWorkspaceBloc: context.read<UserWorkspaceBloc>(),
);
} else { } else {
Navigator.of(context, rootNavigator: true) Navigator.of(context, rootNavigator: true)
.popUntil((route) => route.isFirst); .popUntil((route) => route.isFirst);
@ -110,7 +113,7 @@ class _UserSettingButtonState extends State<UserSettingButton> {
void showSettingsDialog( void showSettingsDialog(
BuildContext context, { BuildContext context, {
UserWorkspaceBloc? userWorkspaceBloc, required UserWorkspaceBloc userWorkspaceBloc,
PasswordBloc? passwordBloc, PasswordBloc? passwordBloc,
SettingsPage? initPage, SettingsPage? initPage,
}) { }) {
@ -126,7 +129,7 @@ void showSettingsDialog(
) )
: BlocProvider( : BlocProvider(
create: (context) => PasswordBloc( create: (context) => PasswordBloc(
context.read<UserWorkspaceBloc>().state.userProfile, userWorkspaceBloc.state.userProfile,
) )
..add(PasswordEvent.init()) ..add(PasswordEvent.init())
..add(PasswordEvent.checkHasPassword()), ..add(PasswordEvent.checkHasPassword()),
@ -135,11 +138,11 @@ void showSettingsDialog(
value: BlocProvider.of<DocumentAppearanceCubit>(dialogContext), value: BlocProvider.of<DocumentAppearanceCubit>(dialogContext),
), ),
BlocProvider.value( BlocProvider.value(
value: userWorkspaceBloc ?? context.read<UserWorkspaceBloc>(), value: userWorkspaceBloc,
), ),
], ],
child: SettingsDialog( child: SettingsDialog(
context.read<UserWorkspaceBloc>().state.userProfile, userWorkspaceBloc.state.userProfile,
initPage: initPage, initPage: initPage,
didLogout: () async { didLogout: () async {
// Pop the dialog using the dialog context // Pop the dialog using the dialog context