chore: polish ui (#5883)

* chore: polish ui

* chore: clippy

* chore: fmt
This commit is contained in:
Nathan.fooo 2024-08-06 13:17:56 +08:00 committed by GitHub
parent 453e6309d5
commit 5757cc9a1d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 124 additions and 72 deletions

View File

@ -175,7 +175,7 @@ SPEC CHECKSUMS:
file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
flowy_infra_ui: 0455e1fa8c51885aa1437848e361e99419f34ebc
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
fluttertoast: e9a18c7be5413da53898f660530c56f35edfba9c
fluttertoast: 723e187574b149e68e63ca4d39b837586b903cfa
image_gallery_saver: cb43cc43141711190510e92c460eb1655cd343cb
image_picker_ios: 99dfe1854b4fa34d0364e74a78448a0151025425
integration_test: ce0a3ffa1de96d1a89ca0ac26fca7ea18a749ef4
@ -197,4 +197,4 @@ SPEC CHECKSUMS:
PODFILE CHECKSUM: d0d9b4ff572d8695c38eb3f9b490f55cdfc57eca
COCOAPODS: 1.15.2
COCOAPODS: 1.11.3

View File

@ -2,10 +2,12 @@ import 'dart:async';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy/workspace/application/view/view_service.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'chat_input_action_control.dart';
@ -43,34 +45,28 @@ class ChatInputActionBloc
);
},
refreshViews: (List<ViewPB> views) {
final List<ViewActionPage> pages = _filterPages(
views,
state.selectedPages,
state.filter,
);
emit(
state.copyWith(
views: views,
pages: views.map((v) => ViewActionPage(view: v)).toList(),
pages: pages,
indicator: const ChatActionMenuIndicator.ready(),
),
);
},
filter: (String filter) {
final List<ViewActionPage> pages = [];
if (filter.isEmpty) {
pages.addAll(state.views.map((v) => ViewActionPage(view: v)));
} else {
pages.addAll(
state.views
.where(
(v) => v.name.toLowerCase().contains(
filter.toLowerCase(),
),
)
.map(
(v) => ViewActionPage(view: v),
),
);
}
pages.retainWhere((view) {
return !state.selectedPages.contains(view);
});
emit(state.copyWith(pages: pages));
Log.debug("Filter chat input pages: $filter");
final List<ViewActionPage> pages = _filterPages(
state.views,
state.selectedPages,
filter,
);
emit(state.copyWith(pages: pages, filter: filter));
},
handleKeyEvent: (PhysicalKeyboardKey physicalKey) {
emit(
@ -81,8 +77,14 @@ class ChatInputActionBloc
},
addPage: (ChatInputActionPage page) {
if (!state.selectedPages.any((p) => p.pageId == page.pageId)) {
final List<ViewActionPage> pages = _filterPages(
state.views,
state.selectedPages,
state.filter,
);
emit(
state.copyWith(
pages: pages,
selectedPages: [...state.selectedPages, page],
),
);
@ -93,9 +95,11 @@ class ChatInputActionBloc
List.from(state.selectedPages);
selectedPages.retainWhere((t) => !text.contains(t.title));
final allPages =
state.views.map((v) => ViewActionPage(view: v)).toList();
allPages.retainWhere((view) => !selectedPages.contains(view));
final List<ViewActionPage> allPages = _filterPages(
state.views,
state.selectedPages,
state.filter,
);
emit(
state.copyWith(
@ -108,6 +112,32 @@ class ChatInputActionBloc
}
}
List<ViewActionPage> _filterPages(
List<ViewPB> views,
List<ChatInputActionPage> selectedPages,
String filter,
) {
final pages = views
.map(
(v) => ViewActionPage(view: v),
)
.toList();
pages.retainWhere((page) {
return !selectedPages.contains(page);
});
if (filter.isEmpty) {
return pages;
}
return pages
.where(
(v) => v.title.toLowerCase().contains(filter.toLowerCase()),
)
.toList();
}
class ViewActionPage extends ChatInputActionPage {
ViewActionPage({required this.view});
@ -124,6 +154,9 @@ class ViewActionPage extends ChatInputActionPage {
@override
dynamic get page => view;
@override
Widget get icon => view.defaultIcon();
}
@freezed
@ -146,7 +179,10 @@ class ChatInputActionState with _$ChatInputActionState {
@Default([]) List<ViewPB> views,
@Default([]) List<ChatInputActionPage> pages,
@Default([]) List<ChatInputActionPage> selectedPages,
@Default("") String filter,
ChatInputKeyboardEvent? keyboardKey,
@Default(ChatActionMenuIndicator.loading())
ChatActionMenuIndicator indicator,
}) = _ChatInputActionState;
}
@ -159,3 +195,9 @@ class ChatInputKeyboardEvent extends Equatable {
@override
List<Object?> get props => [timestamp];
}
@freezed
class ChatActionMenuIndicator with _$ChatActionMenuIndicator {
const factory ChatActionMenuIndicator.ready() = _Ready;
const factory ChatActionMenuIndicator.loading() = _Loading;
}

View File

@ -9,6 +9,7 @@ abstract class ChatInputActionPage extends Equatable {
String get title;
String get pageId;
dynamic get page;
Widget get icon;
}
typedef ChatInputMetadata = Map<String, ChatInputActionPage>;
@ -26,10 +27,9 @@ class ChatInputActionControl extends ChatActionHandler {
final String chatId;
// Private attributes
bool _isShowActionMenu = false;
String _atText = "";
String _prevText = "";
bool _didLoadViews = false;
String _showMenuText = "";
// Getter
List<String> get tags =>
@ -43,12 +43,12 @@ class ChatInputActionControl extends ChatActionHandler {
void handleKeyEvent(KeyEvent event) {
// ignore: deprecated_member_use
if (event is KeyDownEvent || event is RawKeyDownEvent) {
commandBloc.add(ChatInputActionEvent.handleKeyEvent(event.physicalKey));
_commandBloc.add(ChatInputActionEvent.handleKeyEvent(event.physicalKey));
}
}
bool canHandleKeyEvent(KeyEvent event) {
return _isShowActionMenu &&
return _showMenuText.isNotEmpty &&
<PhysicalKeyboardKey>{
PhysicalKeyboardKey.arrowDown,
PhysicalKeyboardKey.arrowUp,
@ -58,35 +58,29 @@ class ChatInputActionControl extends ChatActionHandler {
}
void dispose() {
commandBloc.close();
_commandBloc.close();
}
@override
void onSelected(ChatInputActionPage page) {
_atText = "";
_isShowActionMenu = false;
_commandBloc.add(ChatInputActionEvent.addPage(page));
textController.text =
"${textController.text.replaceAll(_atText, '')}${page.title}";
_prevText = textController.text;
textController.text = "$_showMenuText${page.title}";
onExit();
}
@override
void onExit() {
_atText = "";
_isShowActionMenu = false;
_didLoadViews = false;
commandBloc.add(const ChatInputActionEvent.filter(""));
_showMenuText = "";
_prevText = "";
_commandBloc.add(const ChatInputActionEvent.filter(""));
}
@override
void onEnter() {
if (!_didLoadViews) {
_didLoadViews = true;
commandBloc.add(const ChatInputActionEvent.started());
}
_isShowActionMenu = true;
_commandBloc.add(const ChatInputActionEvent.started());
_showMenuText = textController.text;
}
@override
@ -134,16 +128,15 @@ class ChatInputActionControl extends ChatActionHandler {
}
// If the action menu is shown, filter the views
if (_isShowActionMenu) {
// before filter the views, remove the first character '@' if it exists
if (inputText.startsWith("@")) {
final filter = inputText.substring(1);
commandBloc.add(ChatInputActionEvent.filter(filter));
if (_showMenuText.isNotEmpty) {
if (text.length >= _showMenuText.length) {
final filterText = inputText.substring(_showMenuText.length);
_commandBloc.add(ChatInputActionEvent.filter(filterText));
}
// If the text change from "xxx @"" to "xxx", which means user delete the @, we should hide the action menu
if (_atText.isNotEmpty && !inputText.contains(_atText)) {
commandBloc.add(
_commandBloc.add(
const ChatInputActionEvent.handleKeyEvent(PhysicalKeyboardKey.escape),
);
}

View File

@ -489,6 +489,7 @@ class _ChatContentPageState extends State<_ChatContentPage> {
child: BlocBuilder<ChatInputStateBloc, ChatInputStateState>(
builder: (context, state) {
// Show different hint text based on the AI type
final aiType = state.aiType;
final hintText = state.aiType.when(
appflowyAI: () => LocaleKeys.chat_inputMessageHint.tr(),
localAI: () => LocaleKeys.chat_inputLocalAIMessageHint.tr(),
@ -500,6 +501,7 @@ class _ChatContentPageState extends State<_ChatContentPage> {
selector: (state) => state.streamingStatus,
builder: (context, state) {
return ChatInput(
aiType: aiType,
chatId: widget.view.id,
onSendPressed: (message) {
context.read<ChatBloc>().add(

View File

@ -1,3 +1,4 @@
import 'package:appflowy/plugins/ai_chat/application/chat_input_bloc.dart';
import 'package:appflowy/plugins/ai_chat/presentation/chat_input_action_menu.dart';
import 'package:appflowy/plugins/ai_chat/application/chat_input_action_control.dart';
import 'package:extended_text_field/extended_text_field.dart';
@ -26,6 +27,7 @@ class ChatInput extends StatefulWidget {
required this.isStreaming,
required this.onStopStreaming,
required this.hintText,
required this.aiType,
});
final bool? isAttachmentUploading;
@ -36,6 +38,7 @@ class ChatInput extends StatefulWidget {
final String chatId;
final bool isStreaming;
final String hintText;
final AIType aiType;
@override
State<ChatInput> createState() => _ChatInputState();
@ -117,7 +120,9 @@ class _ChatInputState extends State<ChatInput> {
Expanded(child: _inputTextField(textPadding)),
// TODO(lucas): support mobile
if (PlatformExtension.isDesktop) _atButton(buttonPadding),
if (PlatformExtension.isDesktop &&
widget.aiType == const AIType.appflowyAI())
_atButton(buttonPadding),
_sendButton(buttonPadding),
const HSpace(14),
],
@ -169,6 +174,7 @@ class _ChatInputState extends State<ChatInput> {
decoration: InputDecoration(
border: InputBorder.none,
hintText: widget.hintText,
focusedBorder: InputBorder.none,
hintStyle: TextStyle(
color: AFThemeExtension.of(context).textColor.withOpacity(0.5),
),
@ -190,6 +196,10 @@ class _ChatInputState extends State<ChatInput> {
}
void _handleOnTextChange(BuildContext context, String text) {
if (widget.aiType != const AIType.appflowyAI()) {
return;
}
if (PlatformExtension.isDesktop) {
if (_inputActionControl.onTextChanged(text)) {
ChatActionsMenu(

View File

@ -78,6 +78,8 @@ class ChatActionsMenu {
),
maxHeight,
);
final isLoading =
state.indicator == const ChatActionMenuIndicator.loading();
return Stack(
children: [
@ -106,6 +108,7 @@ class ChatActionsMenu {
vertical: 2,
),
child: ActionList(
isLoading: isLoading,
handler: handler,
onDismiss: () => dismiss(),
pages: state.pages,
@ -149,6 +152,7 @@ class _ActionItem extends StatelessWidget {
borderRadius: BorderRadius.circular(4.0),
),
child: FlowyButton(
leftIcon: item.icon,
margin: const EdgeInsets.symmetric(horizontal: 6),
iconPadding: 10.0,
text: FlowyText.regular(
@ -166,11 +170,13 @@ class ActionList extends StatefulWidget {
required this.handler,
required this.onDismiss,
required this.pages,
required this.isLoading,
});
final ChatActionHandler handler;
final VoidCallback? onDismiss;
final List<ChatInputActionPage> pages;
final bool isLoading;
@override
State<ActionList> createState() => _ActionListState();
@ -223,13 +229,22 @@ class _ActionListState extends State<ActionList> {
child: ListView(
shrinkWrap: true,
controller: _scrollController,
padding: const EdgeInsets.all(8),
padding: const EdgeInsets.all(4),
children: _buildPages(),
),
);
}
List<Widget> _buildPages() {
if (widget.isLoading) {
return [
SizedBox(
height: _noPageHeight.toDouble(),
child: const Center(child: CircularProgressIndicator.adaptive()),
),
];
}
if (widget.pages.isEmpty) {
return [
SizedBox(

View File

@ -30,13 +30,13 @@ impl Stream for LocalAIStreamAdaptor {
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
let this = self.project();
return match ready!(this.stream.as_mut().poll_next(cx)) {
match ready!(this.stream.as_mut().poll_next(cx)) {
Some(Ok(bytes)) => match String::from_utf8(bytes.to_vec()) {
Ok(s) => Poll::Ready(Some(Ok(QuestionStreamValue::Answer { value: s }))),
Err(err) => Poll::Ready(Some(Err(FlowyError::internal().with_context(err)))),
},
Some(Err(err)) => Poll::Ready(Some(Err(FlowyError::local_ai().with_context(err)))),
None => Poll::Ready(None),
};
}
}
}

View File

@ -10,7 +10,7 @@ use crate::manager_observer::{
ChildViewChangeReason,
};
use crate::notification::{
send_notification, send_workspace_setting_notification, FolderNotification,
send_current_workspace_notification, send_notification, FolderNotification,
};
use crate::publish_util::{generate_publish_name, view_pb_to_publish_view};
use crate::share::{ImportParams, ImportValue};
@ -978,7 +978,11 @@ impl FolderManager {
}
let workspace_id = self.user.workspace_id()?;
send_workspace_setting_notification(workspace_id, view);
let setting = WorkspaceSettingPB {
workspace_id,
latest_view: view,
};
send_current_workspace_notification(FolderNotification::DidUpdateWorkspaceSetting, setting);
Ok(())
}

View File

@ -2,8 +2,6 @@ use flowy_derive::ProtoBuf_Enum;
use flowy_notification::NotificationBuilder;
use lib_dispatch::prelude::ToBytes;
use crate::entities::{ViewPB, WorkspaceSettingPB};
const FOLDER_OBSERVABLE_SOURCE: &str = "Workspace";
#[derive(ProtoBuf_Enum, Debug, Default)]
@ -78,20 +76,8 @@ pub(crate) fn send_notification(id: &str, ty: FolderNotification) -> Notificatio
/// The [CURRENT_WORKSPACE] represents as the current workspace that opened by the
/// user. Only one workspace can be opened at a time.
const CURRENT_WORKSPACE: &str = "current-workspace";
pub(crate) fn send_workspace_notification<T: ToBytes>(ty: FolderNotification, payload: T) {
pub(crate) fn send_current_workspace_notification<T: ToBytes>(ty: FolderNotification, payload: T) {
send_notification(CURRENT_WORKSPACE, ty)
.payload(payload)
.send();
}
pub(crate) fn send_workspace_setting_notification(
workspace_id: String,
latest_view: Option<ViewPB>,
) -> Option<()> {
let setting = WorkspaceSettingPB {
workspace_id,
latest_view,
};
send_workspace_notification(FolderNotification::DidUpdateWorkspaceSetting, setting);
None
}