From 846172a709856f6c91d30a2f8107af434eaa80b4 Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 15 Apr 2025 22:19:16 +0800 Subject: [PATCH] chore: adjust ui --- .../search_result_list_bloc.dart | 5 ++ .../widgets/search_result_cell.dart | 14 +++- .../widgets/search_results_list.dart | 83 ++++++++++++++----- .../widgets/search_summary_cell.dart | 8 +- frontend/rust-lib/flowy-ai/src/ai_manager.rs | 15 ++-- 5 files changed, 96 insertions(+), 29 deletions(-) diff --git a/frontend/appflowy_flutter/lib/workspace/application/command_palette/search_result_list_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/command_palette/search_result_list_bloc.dart index 58e5a951da..e5953ae61b 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/command_palette/search_result_list_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/command_palette/search_result_list_bloc.dart @@ -25,6 +25,7 @@ class SearchResultListBloc state.copyWith( hoveredSummary: event.summary, hoveredResult: null, + userHovered: event.userHovered, openPageId: null, ), ); @@ -38,6 +39,7 @@ class SearchResultListBloc state.copyWith( hoveredSummary: null, hoveredResult: event.item, + userHovered: event.userHovered, openPageId: null, ), ); @@ -55,9 +57,11 @@ class SearchResultListBloc class SearchResultListEvent with _$SearchResultListEvent { const factory SearchResultListEvent.onHoverSummary({ required SearchSummaryPB summary, + required bool userHovered, }) = _OnHoverSummary; const factory SearchResultListEvent.onHoverResult({ required SearchResultItem item, + required bool userHovered, }) = _OnHoverResult; const factory SearchResultListEvent.openPage({ @@ -72,6 +76,7 @@ class SearchResultListState with _$SearchResultListState { @Default(null) SearchSummaryPB? hoveredSummary, @Default(null) SearchResultItem? hoveredResult, @Default(null) String? openPageId, + @Default(false) bool userHovered, }) = _SearchResultListState; factory SearchResultListState.initial() => const SearchResultListState(); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_result_cell.dart b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_result_cell.dart index e8e5a37f82..2485da4a69 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_result_cell.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_result_cell.dart @@ -17,10 +17,12 @@ class SearchResultCell extends StatefulWidget { super.key, required this.item, this.isTrashed = false, + this.isHovered = false, }); final SearchResultItem item; final bool isTrashed; + final bool isHovered; @override State createState() => _SearchResultCellState(); @@ -142,7 +144,10 @@ class _SearchResultCellState extends State { onFocusChange: (hasFocus) { setState(() { context.read().add( - SearchResultListEvent.onHoverResult(item: widget.item), + SearchResultListEvent.onHoverResult( + item: widget.item, + userHovered: true, + ), ); _hasFocus = hasFocus; }); @@ -150,10 +155,13 @@ class _SearchResultCellState extends State { child: FlowyHover( onHover: (value) { context.read().add( - SearchResultListEvent.onHoverResult(item: widget.item), + SearchResultListEvent.onHoverResult( + item: widget.item, + userHovered: true, + ), ); }, - isSelected: () => _hasFocus, + isSelected: () => _hasFocus || widget.isHovered, style: HoverStyle( borderRadius: BorderRadius.circular(8), hoverColor: diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_results_list.dart b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_results_list.dart index 1a7a1d94bb..d90888e3e9 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_results_list.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_results_list.dart @@ -16,7 +16,7 @@ import 'package:flutter_animate/flutter_animate.dart'; import 'search_result_cell.dart'; import 'search_summary_cell.dart'; -class SearchResultList extends StatelessWidget { +class SearchResultList extends StatefulWidget { const SearchResultList({ required this.trash, required this.resultItems, @@ -27,6 +27,26 @@ class SearchResultList extends StatelessWidget { final List trash; final List resultItems; final List resultSummaries; + + @override + State createState() => _SearchResultListState(); +} + +class _SearchResultListState extends State { + late final SearchResultListBloc bloc; + + @override + void initState() { + super.initState(); + bloc = SearchResultListBloc(); + } + + @override + void dispose() { + bloc.close(); + super.dispose(); + } + Widget _buildSectionHeader(String title) => Padding( padding: const EdgeInsets.symmetric(vertical: 8) + const EdgeInsets.only(left: 8), @@ -48,7 +68,21 @@ class SearchResultList extends StatelessWidget { ], ); } - if (resultSummaries.isNotEmpty) { + + if (widget.resultSummaries.isNotEmpty) { + if (!bloc.state.userHovered) { + WidgetsBinding.instance.addPostFrameCallback( + (_) { + bloc.add( + SearchResultListEvent.onHoverSummary( + summary: widget.resultSummaries[0], + userHovered: false, + ), + ); + }, + ); + } + return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -56,10 +90,11 @@ class SearchResultList extends StatelessWidget { ListView.separated( physics: const NeverScrollableScrollPhysics(), shrinkWrap: true, - itemCount: resultSummaries.length, + itemCount: widget.resultSummaries.length, separatorBuilder: (_, __) => const Divider(height: 0), itemBuilder: (_, index) => SearchSummaryCell( - summary: resultSummaries[index], + summary: widget.resultSummaries[index], + isHovered: bloc.state.hoveredSummary != null, ), ), ], @@ -78,13 +113,14 @@ class SearchResultList extends StatelessWidget { ListView.separated( physics: const NeverScrollableScrollPhysics(), shrinkWrap: true, - itemCount: resultItems.length, + itemCount: widget.resultItems.length, separatorBuilder: (_, __) => const Divider(height: 0), itemBuilder: (_, index) { - final item = resultItems[index]; + final item = widget.resultItems[index]; return SearchResultCell( item: item, - isTrashed: trash.any((t) => t.id == item.id), + isTrashed: widget.trash.any((t) => t.id == item.id), + isHovered: bloc.state.hoveredResult?.id == item.id, ); }, ), @@ -96,11 +132,9 @@ class SearchResultList extends StatelessWidget { Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 6), - child: BlocProvider( - create: (context) => SearchResultListBloc(), + child: BlocProvider.value( + value: bloc, child: BlocListener( - listenWhen: (previous, current) => - previous.openPageId != current.openPageId, listener: (context, state) { if (state.openPageId != null) { FlowyOverlay.pop(context); @@ -116,18 +150,27 @@ class SearchResultList extends StatelessWidget { children: [ Flexible( flex: 7, - child: ListView( - shrinkWrap: true, - physics: const ClampingScrollPhysics(), - children: [ - _buildAIOverviewSection(context), - const VSpace(10), - if (resultItems.isNotEmpty) _buildResultsSection(context), - ], + child: BlocBuilder( + buildWhen: (previous, current) => + previous.hoveredResult != current.hoveredResult || + previous.hoveredSummary != current.hoveredSummary, + builder: (context, state) { + return ListView( + shrinkWrap: true, + physics: const ClampingScrollPhysics(), + children: [ + _buildAIOverviewSection(context), + const VSpace(10), + if (widget.resultItems.isNotEmpty) + _buildResultsSection(context), + ], + ); + }, ), ), const HSpace(10), - if (resultItems.any((item) => item.content.isNotEmpty)) ...[ + if (widget.resultItems + .any((item) => item.content.isNotEmpty)) ...[ const VerticalDivider( thickness: 1.0, ), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_summary_cell.dart b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_summary_cell.dart index 89553709fc..20a02589f5 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_summary_cell.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_summary_cell.dart @@ -14,17 +14,23 @@ import 'package:flutter_bloc/flutter_bloc.dart'; class SearchSummaryCell extends StatelessWidget { const SearchSummaryCell({ required this.summary, + required this.isHovered, super.key, }); final SearchSummaryPB summary; + final bool isHovered; @override Widget build(BuildContext context) { return FlowyHover( + isSelected: () => isHovered, onHover: (value) { context.read().add( - SearchResultListEvent.onHoverSummary(summary: summary), + SearchResultListEvent.onHoverSummary( + summary: summary, + userHovered: true, + ), ); }, style: HoverStyle( diff --git a/frontend/rust-lib/flowy-ai/src/ai_manager.rs b/frontend/rust-lib/flowy-ai/src/ai_manager.rs index 0b27f4f525..ec12ac4963 100644 --- a/frontend/rust-lib/flowy-ai/src/ai_manager.rs +++ b/frontend/rust-lib/flowy-ai/src/ai_manager.rs @@ -453,7 +453,6 @@ impl AIManager { if let Some(local_model) = self.local_ai.get_plugin_chat_model() { let model = AIModel::local(local_model, "".to_string()); current_active_local_ai_model = Some(model.clone()); - trace!("[Model Selection] current local ai model: {}", model.name); models.push(model); } @@ -466,7 +465,7 @@ impl AIManager { } // Global active model is the model selected by the user in the workspace settings. - let server_active_model = self + let mut server_active_model = self .get_workspace_select_model() .await .map(|m| AIModel::server(m, "".to_string())) @@ -478,10 +477,14 @@ impl AIManager { ); let mut user_selected_model = server_active_model.clone(); + // when current select model is deprecated, reset the model to default + if !models.iter().any(|m| m.name == server_active_model.name) { + server_active_model = AIModel::default(); + } + let source_key = ai_available_models_key(&source); - // If source is provided, try to get the user-selected model from the store. User selected - // model will be used as the active model if it exists. + // We use source to identify user selected model. source can be document id or chat id. match self.store_preferences.get_object::(&source_key) { None => { // when there is selected model and current local ai is active, then use local ai @@ -491,6 +494,8 @@ impl AIManager { }, Some(mut model) => { trace!("[Model Selection] user previous select model: {:?}", model); + // If source is provided, try to get the user-selected model from the store. User selected + // model will be used as the active model if it exists. if model.is_local { if let Some(local_ai_model) = ¤t_active_local_ai_model { if local_ai_model.name != model.name { @@ -508,7 +513,7 @@ impl AIManager { .iter() .find(|m| m.name == user_selected_model.name) .cloned() - .or(Some(server_active_model)); + .or(Some(server_active_model.clone())); // Update the stored preference if a different model is used. if let Some(ref active_model) = active_model {