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_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<AIModelPB> _availableModels = [];
AIModelPB? _selectedModel;
final List<OnModelStateChangedCallback> _stateChangedCallbacks = [];
final List<OnAvailableModelsChangedCallback>
_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<void> _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>, 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<void> _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>, 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<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> _loadLocalState() async {
await AIEventGetLocalAIState().send().fold(
(s) => _localAIState = s,
(e) => Log.error("Failed to fetch local AI state: \$e"),
);
}
Future<void> _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;
}

View File

@ -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),
),
],
),
),
),

View File

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