mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2025-07-26 03:23:01 +00:00
chore: search with ai summary
This commit is contained in:
@ -49,7 +49,7 @@ void main() {
|
||||
// The score should be higher for "ViewOna" thus it should be shown first
|
||||
final secondDocumentWidget = tester
|
||||
.widget(find.byType(SearchResultTile).first) as SearchResultTile;
|
||||
expect(secondDocumentWidget.result.data, secondDocument);
|
||||
expect(secondDocumentWidget.item.data, secondDocument);
|
||||
|
||||
// Change search to "ViewOne"
|
||||
await tester.enterText(searchFieldFinder, firstDocument);
|
||||
@ -59,7 +59,7 @@ void main() {
|
||||
final firstDocumentWidget = tester.widget(
|
||||
find.byType(SearchResultTile).first,
|
||||
) as SearchResultTile;
|
||||
expect(firstDocumentWidget.result.data, firstDocument);
|
||||
expect(firstDocumentWidget.item.data, firstDocument);
|
||||
});
|
||||
|
||||
testWidgets('Displaying icons in search results', (tester) async {
|
||||
|
@ -2,10 +2,8 @@ import 'dart:async';
|
||||
|
||||
import 'package:appflowy/plugins/trash/application/trash_listener.dart';
|
||||
import 'package:appflowy/plugins/trash/application/trash_service.dart';
|
||||
import 'package:appflowy/workspace/application/command_palette/search_listener.dart';
|
||||
import 'package:appflowy/workspace/application/command_palette/search_service.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/trash.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-search/notification.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-search/result.pb.dart';
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
@ -13,184 +11,285 @@ import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'command_palette_bloc.freezed.dart';
|
||||
|
||||
const _searchChannel = 'CommandPalette';
|
||||
|
||||
class CommandPaletteBloc
|
||||
extends Bloc<CommandPaletteEvent, CommandPaletteState> {
|
||||
CommandPaletteBloc() : super(CommandPaletteState.initial()) {
|
||||
_searchListener.start(
|
||||
onResultsChanged: _onResultsChanged,
|
||||
);
|
||||
// Register event handlers
|
||||
on<_SearchChanged>(_onSearchChanged);
|
||||
on<_PerformSearch>(_onPerformSearch);
|
||||
on<_NewSearchStream>(_onNewSearchStream);
|
||||
on<_ResultsChanged>(_onResultsChanged);
|
||||
on<_TrashChanged>(_onTrashChanged);
|
||||
on<_WorkspaceChanged>(_onWorkspaceChanged);
|
||||
on<_ClearSearch>(_onClearSearch);
|
||||
|
||||
_initTrash();
|
||||
_dispatch();
|
||||
}
|
||||
|
||||
Timer? _debounceOnChanged;
|
||||
final TrashService _trashService = TrashService();
|
||||
final SearchListener _searchListener = SearchListener(
|
||||
channel: _searchChannel,
|
||||
);
|
||||
final TrashListener _trashListener = TrashListener();
|
||||
String? _oldQuery;
|
||||
String? _workspaceId;
|
||||
int _messagesReceived = 0;
|
||||
|
||||
@override
|
||||
Future<void> close() {
|
||||
_trashListener.close();
|
||||
_searchListener.stop();
|
||||
_debounceOnChanged?.cancel();
|
||||
state.searchResponseStream?.dispose();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
void _dispatch() {
|
||||
on<CommandPaletteEvent>((event, emit) async {
|
||||
event.when(
|
||||
searchChanged: _debounceOnSearchChanged,
|
||||
trashChanged: (trash) async {
|
||||
if (trash != null) {
|
||||
return emit(state.copyWith(trash: trash));
|
||||
}
|
||||
|
||||
final trashOrFailure = await _trashService.readTrash();
|
||||
final trashRes = trashOrFailure.fold(
|
||||
(trash) => trash,
|
||||
(error) => null,
|
||||
);
|
||||
|
||||
if (trashRes != null) {
|
||||
emit(state.copyWith(trash: trashRes.items));
|
||||
}
|
||||
},
|
||||
performSearch: (search) async {
|
||||
if (search.isNotEmpty && search != state.query) {
|
||||
_oldQuery = state.query;
|
||||
emit(state.copyWith(query: search, isLoading: true));
|
||||
await SearchBackendService.performSearch(
|
||||
search,
|
||||
workspaceId: _workspaceId,
|
||||
channel: _searchChannel,
|
||||
);
|
||||
} else {
|
||||
emit(state.copyWith(query: null, isLoading: false, results: []));
|
||||
}
|
||||
},
|
||||
resultsChanged: (results) {
|
||||
if (state.query != _oldQuery) {
|
||||
emit(state.copyWith(results: [], isLoading: true));
|
||||
_oldQuery = state.query;
|
||||
_messagesReceived = 0;
|
||||
}
|
||||
|
||||
if (state.query != results.query) {
|
||||
return;
|
||||
}
|
||||
|
||||
_messagesReceived++;
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
results: _filterDuplicates(results.items),
|
||||
isLoading: _messagesReceived != results.sends.toInt(),
|
||||
),
|
||||
);
|
||||
},
|
||||
workspaceChanged: (workspaceId) {
|
||||
_workspaceId = workspaceId;
|
||||
emit(state.copyWith(results: [], query: '', isLoading: false));
|
||||
},
|
||||
clearSearch: () {
|
||||
emit(state.copyWith(results: [], query: '', isLoading: false));
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _initTrash() async {
|
||||
// Start listening for trash updates
|
||||
_trashListener.start(
|
||||
trashUpdated: (trashOrFailed) {
|
||||
final trash = trashOrFailed.toNullable();
|
||||
add(CommandPaletteEvent.trashChanged(trash: trash));
|
||||
add(
|
||||
CommandPaletteEvent.trashChanged(
|
||||
trash: trashOrFailed.toNullable(),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
// Read initial trash state and forward results
|
||||
final trashOrFailure = await _trashService.readTrash();
|
||||
final trash = trashOrFailure.toNullable();
|
||||
|
||||
add(CommandPaletteEvent.trashChanged(trash: trash?.items));
|
||||
}
|
||||
|
||||
void _debounceOnSearchChanged(String value) {
|
||||
_debounceOnChanged?.cancel();
|
||||
_debounceOnChanged = Timer(
|
||||
const Duration(milliseconds: 300),
|
||||
() => _performSearch(value),
|
||||
add(
|
||||
CommandPaletteEvent.trashChanged(
|
||||
trash: trashOrFailure.toNullable()?.items,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<SearchResultPB> _filterDuplicates(List<SearchResultPB> results) {
|
||||
final currentItems = [...state.results];
|
||||
final res = [...results];
|
||||
|
||||
for (final item in results) {
|
||||
final duplicateIndex = currentItems.indexWhere((a) => a.id == item.id);
|
||||
if (duplicateIndex == -1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final duplicate = currentItems[duplicateIndex];
|
||||
if (item.score < duplicate.score) {
|
||||
res.remove(item);
|
||||
} else {
|
||||
currentItems.remove(duplicate);
|
||||
}
|
||||
}
|
||||
|
||||
return res..addAll(currentItems);
|
||||
FutureOr<void> _onSearchChanged(
|
||||
_SearchChanged event,
|
||||
Emitter<CommandPaletteState> emit,
|
||||
) {
|
||||
_debounceOnChanged?.cancel();
|
||||
_debounceOnChanged = Timer(
|
||||
const Duration(milliseconds: 300),
|
||||
() {
|
||||
if (!isClosed) {
|
||||
add(CommandPaletteEvent.performSearch(search: event.search));
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _performSearch(String value) =>
|
||||
add(CommandPaletteEvent.performSearch(search: value));
|
||||
FutureOr<void> _onPerformSearch(
|
||||
_PerformSearch event,
|
||||
Emitter<CommandPaletteState> emit,
|
||||
) async {
|
||||
if (event.search.isNotEmpty && event.search != state.query) {
|
||||
_oldQuery = state.query;
|
||||
emit(state.copyWith(query: event.search, isLoading: true));
|
||||
|
||||
void _onResultsChanged(SearchResultNotificationPB results) =>
|
||||
add(CommandPaletteEvent.resultsChanged(results: results));
|
||||
// Fire off search asynchronously (fire and forget)
|
||||
unawaited(
|
||||
SearchBackendService.performSearch(
|
||||
event.search,
|
||||
workspaceId: _workspaceId,
|
||||
).then(
|
||||
(result) => result.onSuccess((stream) {
|
||||
if (!isClosed) {
|
||||
add(CommandPaletteEvent.newSearchStream(stream: stream));
|
||||
}
|
||||
}),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
// Clear state if search is empty or unchanged
|
||||
emit(
|
||||
state.copyWith(
|
||||
query: null,
|
||||
isLoading: false,
|
||||
resultItems: [],
|
||||
resultSummaries: [],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
FutureOr<void> _onNewSearchStream(
|
||||
_NewSearchStream event,
|
||||
Emitter<CommandPaletteState> emit,
|
||||
) {
|
||||
state.searchResponseStream?.dispose();
|
||||
emit(
|
||||
state.copyWith(
|
||||
searchId: event.stream.searchId,
|
||||
searchResponseStream: event.stream,
|
||||
),
|
||||
);
|
||||
|
||||
event.stream.listen(
|
||||
onItems: (
|
||||
List<SearchResponseItemPB> items,
|
||||
String searchId,
|
||||
bool isLoading,
|
||||
) {
|
||||
if (_isActiveSearch(searchId)) {
|
||||
add(
|
||||
CommandPaletteEvent.resultsChanged(
|
||||
items: items,
|
||||
searchId: searchId,
|
||||
isLoading: isLoading,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
onSummaries: (
|
||||
List<SearchSummaryPB> summaries,
|
||||
String searchId,
|
||||
bool isLoading,
|
||||
) {
|
||||
if (_isActiveSearch(searchId)) {
|
||||
add(
|
||||
CommandPaletteEvent.resultsChanged(
|
||||
summaries: summaries,
|
||||
searchId: searchId,
|
||||
isLoading: isLoading,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
onFinished: (String searchId) {
|
||||
if (_isActiveSearch(searchId)) {
|
||||
add(
|
||||
CommandPaletteEvent.resultsChanged(
|
||||
searchId: searchId,
|
||||
isLoading: false,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
FutureOr<void> _onResultsChanged(
|
||||
_ResultsChanged event,
|
||||
Emitter<CommandPaletteState> emit,
|
||||
) async {
|
||||
// If query was updated since last emission, clear previous results.
|
||||
if (state.query != _oldQuery) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
resultItems: [],
|
||||
resultSummaries: [],
|
||||
isLoading: event.isLoading,
|
||||
),
|
||||
);
|
||||
_oldQuery = state.query;
|
||||
}
|
||||
|
||||
// Check for outdated search streams
|
||||
if (state.searchId != event.searchId) return;
|
||||
|
||||
final updatedItems =
|
||||
event.items ?? List<SearchResponseItemPB>.from(state.resultItems);
|
||||
final updatedSummaries =
|
||||
event.summaries ?? List<SearchSummaryPB>.from(state.resultSummaries);
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
resultItems: updatedItems,
|
||||
resultSummaries: updatedSummaries,
|
||||
isLoading: event.isLoading,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Update trash state and, in case of null, retry reading trash from the service
|
||||
FutureOr<void> _onTrashChanged(
|
||||
_TrashChanged event,
|
||||
Emitter<CommandPaletteState> emit,
|
||||
) async {
|
||||
if (event.trash != null) {
|
||||
emit(state.copyWith(trash: event.trash!));
|
||||
} else {
|
||||
final trashOrFailure = await _trashService.readTrash();
|
||||
trashOrFailure.fold((trash) {
|
||||
emit(state.copyWith(trash: trash.items));
|
||||
}, (error) {
|
||||
// Optionally handle error; otherwise, we simply do nothing.
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Update the workspace and clear current search results and query
|
||||
FutureOr<void> _onWorkspaceChanged(
|
||||
_WorkspaceChanged event,
|
||||
Emitter<CommandPaletteState> emit,
|
||||
) {
|
||||
_workspaceId = event.workspaceId;
|
||||
emit(
|
||||
state.copyWith(
|
||||
query: '',
|
||||
resultItems: [],
|
||||
resultSummaries: [],
|
||||
isLoading: false,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Clear search state
|
||||
FutureOr<void> _onClearSearch(
|
||||
_ClearSearch event,
|
||||
Emitter<CommandPaletteState> emit,
|
||||
) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
query: '',
|
||||
resultItems: [],
|
||||
resultSummaries: [],
|
||||
isLoading: false,
|
||||
searchId: null,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
bool _isActiveSearch(String searchId) =>
|
||||
!isClosed && state.searchId == searchId;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class CommandPaletteEvent with _$CommandPaletteEvent {
|
||||
const factory CommandPaletteEvent.searchChanged({required String search}) =
|
||||
_SearchChanged;
|
||||
|
||||
const factory CommandPaletteEvent.performSearch({required String search}) =
|
||||
_PerformSearch;
|
||||
|
||||
const factory CommandPaletteEvent.newSearchStream({
|
||||
required SearchResponseStream stream,
|
||||
}) = _NewSearchStream;
|
||||
const factory CommandPaletteEvent.resultsChanged({
|
||||
required SearchResultNotificationPB results,
|
||||
required String searchId,
|
||||
required bool isLoading,
|
||||
List<SearchResponseItemPB>? items,
|
||||
List<SearchSummaryPB>? summaries,
|
||||
}) = _ResultsChanged;
|
||||
|
||||
const factory CommandPaletteEvent.trashChanged({
|
||||
@Default(null) List<TrashPB>? trash,
|
||||
}) = _TrashChanged;
|
||||
|
||||
const factory CommandPaletteEvent.workspaceChanged({
|
||||
@Default(null) String? workspaceId,
|
||||
}) = _WorkspaceChanged;
|
||||
|
||||
const factory CommandPaletteEvent.clearSearch() = _ClearSearch;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class CommandPaletteState with _$CommandPaletteState {
|
||||
const CommandPaletteState._();
|
||||
|
||||
const factory CommandPaletteState({
|
||||
@Default(null) String? query,
|
||||
required List<SearchResultPB> results,
|
||||
@Default([]) List<SearchResponseItemPB> resultItems,
|
||||
@Default([]) List<SearchSummaryPB> resultSummaries,
|
||||
@Default(null) SearchResponseStream? searchResponseStream,
|
||||
required bool isLoading,
|
||||
@Default([]) List<TrashPB> trash,
|
||||
@Default(null) String? searchId,
|
||||
}) = _CommandPaletteState;
|
||||
|
||||
factory CommandPaletteState.initial() =>
|
||||
const CommandPaletteState(results: [], isLoading: false);
|
||||
const CommandPaletteState(isLoading: false);
|
||||
}
|
||||
|
@ -1,74 +0,0 @@
|
||||
import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:appflowy/core/notification/search_notification.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-search/notification.pb.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
import 'package:flowy_infra/notifier.dart';
|
||||
|
||||
// Do not modify!
|
||||
const _searchObjectId = "SEARCH_IDENTIFIER";
|
||||
|
||||
class SearchListener {
|
||||
SearchListener({this.channel});
|
||||
|
||||
/// Use this to filter out search results from other channels.
|
||||
///
|
||||
/// If null, it will receive search results from all
|
||||
/// channels, otherwise it will only receive search results from the specified
|
||||
/// channel.
|
||||
///
|
||||
final String? channel;
|
||||
|
||||
PublishNotifier<SearchResultNotificationPB>? _updateNotifier =
|
||||
PublishNotifier();
|
||||
PublishNotifier<SearchResultNotificationPB>? _updateDidCloseNotifier =
|
||||
PublishNotifier();
|
||||
SearchNotificationListener? _listener;
|
||||
|
||||
void start({
|
||||
void Function(SearchResultNotificationPB)? onResultsChanged,
|
||||
void Function(SearchResultNotificationPB)? onResultsClosed,
|
||||
}) {
|
||||
if (onResultsChanged != null) {
|
||||
_updateNotifier?.addPublishListener(onResultsChanged);
|
||||
}
|
||||
|
||||
if (onResultsClosed != null) {
|
||||
_updateDidCloseNotifier?.addPublishListener(onResultsClosed);
|
||||
}
|
||||
|
||||
_listener = SearchNotificationListener(
|
||||
objectId: _searchObjectId,
|
||||
handler: _handler,
|
||||
channel: channel,
|
||||
);
|
||||
}
|
||||
|
||||
void _handler(
|
||||
SearchNotification ty,
|
||||
FlowyResult<Uint8List, FlowyError> result,
|
||||
) {
|
||||
switch (ty) {
|
||||
case SearchNotification.DidUpdateResults:
|
||||
result.fold(
|
||||
(payload) => _updateNotifier?.value =
|
||||
SearchResultNotificationPB.fromBuffer(payload),
|
||||
(err) => Log.error(err),
|
||||
);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> stop() async {
|
||||
await _listener?.stop();
|
||||
_updateNotifier?.dispose();
|
||||
_updateNotifier = null;
|
||||
_updateDidCloseNotifier?.dispose();
|
||||
_updateDidCloseNotifier = null;
|
||||
}
|
||||
}
|
@ -5,7 +5,7 @@ import 'package:appflowy_backend/protobuf/flowy-search/result.pb.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
extension GetIcon on SearchResultPB {
|
||||
extension GetIcon on SearchResponseItemPB {
|
||||
Widget? getIcon() {
|
||||
final iconValue = icon.value, iconType = icon.ty;
|
||||
if (iconType == ResultIconTypePB.Emoji) {
|
||||
|
@ -1,22 +1,107 @@
|
||||
import 'dart:async';
|
||||
import 'dart:ffi';
|
||||
import 'dart:isolate';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-search/notification.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-search/query.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-search/result.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-search/search_filter.pb.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
import 'package:nanoid/nanoid.dart';
|
||||
import 'package:fixnum/fixnum.dart';
|
||||
|
||||
class SearchBackendService {
|
||||
static Future<FlowyResult<void, FlowyError>> performSearch(
|
||||
static Future<FlowyResult<SearchResponseStream, FlowyError>> performSearch(
|
||||
String keyword, {
|
||||
String? workspaceId,
|
||||
String? channel,
|
||||
}) async {
|
||||
final searchId = nanoid(6);
|
||||
final stream = SearchResponseStream(searchId: searchId);
|
||||
|
||||
final filter = SearchFilterPB(workspaceId: workspaceId);
|
||||
final request = SearchQueryPB(
|
||||
search: keyword,
|
||||
filter: filter,
|
||||
channel: channel,
|
||||
searchId: searchId,
|
||||
streamPort: Int64(stream.nativePort),
|
||||
);
|
||||
|
||||
return SearchEventSearch(request).send();
|
||||
unawaited(SearchEventSearch(request).send());
|
||||
return FlowyResult.success(stream);
|
||||
}
|
||||
}
|
||||
|
||||
class SearchResponseStream {
|
||||
SearchResponseStream({required this.searchId}) {
|
||||
_port.handler = _controller.add;
|
||||
_subscription = _controller.stream.listen(
|
||||
(Uint8List data) => _onResultsChanged(data),
|
||||
);
|
||||
}
|
||||
|
||||
final String searchId;
|
||||
final RawReceivePort _port = RawReceivePort();
|
||||
final StreamController<Uint8List> _controller = StreamController.broadcast();
|
||||
late StreamSubscription<Uint8List> _subscription;
|
||||
void Function(
|
||||
List<SearchResponseItemPB> items,
|
||||
String searchId,
|
||||
bool isLoading,
|
||||
)? _onItems;
|
||||
void Function(
|
||||
List<SearchSummaryPB> summaries,
|
||||
String searchId,
|
||||
bool isLoading,
|
||||
)? _onSummaries;
|
||||
void Function(String searchId)? _onFinished;
|
||||
int get nativePort => _port.sendPort.nativePort;
|
||||
|
||||
Future<void> dispose() async {
|
||||
await _subscription.cancel();
|
||||
_port.close();
|
||||
}
|
||||
|
||||
void _onResultsChanged(Uint8List data) {
|
||||
final response = SearchResponsePB.fromBuffer(data);
|
||||
|
||||
if (response.hasResult()) {
|
||||
if (response.result.hasSearchResult()) {
|
||||
_onItems?.call(
|
||||
response.result.searchResult.items,
|
||||
searchId,
|
||||
response.isLoading,
|
||||
);
|
||||
}
|
||||
if (response.result.hasSearchSummary()) {
|
||||
_onSummaries?.call(
|
||||
response.result.searchSummary.items,
|
||||
searchId,
|
||||
response.isLoading,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
_onFinished?.call(searchId);
|
||||
}
|
||||
}
|
||||
|
||||
void listen({
|
||||
required void Function(
|
||||
List<SearchResponseItemPB> items,
|
||||
String searchId,
|
||||
bool isLoading,
|
||||
)? onItems,
|
||||
required void Function(
|
||||
List<SearchSummaryPB> summaries,
|
||||
String searchId,
|
||||
bool isLoading,
|
||||
)? onSummaries,
|
||||
required void Function(String searchId)? onFinished,
|
||||
}) {
|
||||
_onItems = onItems;
|
||||
_onSummaries = onSummaries;
|
||||
_onFinished = onFinished;
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,6 @@ import 'package:appflowy/workspace/presentation/command_palette/widgets/recent_v
|
||||
import 'package:appflowy/workspace/presentation/command_palette/widgets/search_field.dart';
|
||||
import 'package:appflowy/workspace/presentation/command_palette/widgets/search_results_list.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
@ -135,11 +134,15 @@ class CommandPaletteModal extends StatelessWidget {
|
||||
builder: (context, state) => FlowyDialog(
|
||||
alignment: Alignment.topCenter,
|
||||
insetPadding: const EdgeInsets.only(top: 100),
|
||||
constraints: const BoxConstraints(maxHeight: 420, maxWidth: 510),
|
||||
constraints: const BoxConstraints(
|
||||
maxHeight: 520,
|
||||
maxWidth: 510,
|
||||
minHeight: 420,
|
||||
),
|
||||
expandHeight: false,
|
||||
child: shortcutBuilder(
|
||||
// Change mainAxisSize to max so Expanded works correctly.
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SearchField(query: state.query, isLoading: state.isLoading),
|
||||
if (state.query?.isEmpty ?? true) ...[
|
||||
@ -150,23 +153,26 @@ class CommandPaletteModal extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
],
|
||||
if (state.results.isNotEmpty &&
|
||||
if (state.resultItems.isNotEmpty &&
|
||||
(state.query?.isNotEmpty ?? false)) ...[
|
||||
const Divider(height: 0),
|
||||
Flexible(
|
||||
child: SearchResultsList(
|
||||
child: SearchResultList(
|
||||
trash: state.trash,
|
||||
results: state.results,
|
||||
resultItems: state.resultItems,
|
||||
resultSummaries: state.resultSummaries,
|
||||
),
|
||||
),
|
||||
] else if ((state.query?.isNotEmpty ?? false) &&
|
||||
]
|
||||
// When there are no results and the query is not empty and not loading,
|
||||
// show the no results message, centered in the available space.
|
||||
else if ((state.query?.isNotEmpty ?? false) &&
|
||||
!state.isLoading) ...[
|
||||
const _NoResultsHint(),
|
||||
const Divider(height: 0),
|
||||
Expanded(
|
||||
child: const _NoResultsHint(),
|
||||
),
|
||||
],
|
||||
_CommandPaletteFooter(
|
||||
shouldShow: state.results.isNotEmpty &&
|
||||
(state.query?.isNotEmpty ?? false),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -175,57 +181,16 @@ class CommandPaletteModal extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// Updated _NoResultsHint now centers its content.
|
||||
class _NoResultsHint extends StatelessWidget {
|
||||
const _NoResultsHint();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Divider(height: 0),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: FlowyText.regular(
|
||||
LocaleKeys.commandPalette_noResultsHint.tr(),
|
||||
textAlign: TextAlign.left,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CommandPaletteFooter extends StatelessWidget {
|
||||
const _CommandPaletteFooter({required this.shouldShow});
|
||||
|
||||
final bool shouldShow;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!shouldShow) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).cardColor,
|
||||
border: Border(top: BorderSide(color: Theme.of(context).dividerColor)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 1),
|
||||
decoration: BoxDecoration(
|
||||
color: AFThemeExtension.of(context).lightGreyHover,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: const FlowyText.semibold('TAB', fontSize: 10),
|
||||
),
|
||||
const HSpace(4),
|
||||
FlowyText(LocaleKeys.commandPalette_navigateHint.tr(), fontSize: 11),
|
||||
],
|
||||
return Center(
|
||||
child: FlowyText.regular(
|
||||
LocaleKeys.commandPalette_noResultsHint.tr(),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -7,7 +7,6 @@ import 'package:appflowy/workspace/application/command_palette/command_palette_b
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/size.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text_field.dart';
|
||||
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||
@ -25,28 +24,31 @@ class SearchField extends StatefulWidget {
|
||||
|
||||
class _SearchFieldState extends State<SearchField> {
|
||||
late final FocusNode focusNode;
|
||||
late final controller = TextEditingController(text: widget.query);
|
||||
late final TextEditingController controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
focusNode = FocusNode(
|
||||
onKeyEvent: (node, event) {
|
||||
if (node.hasFocus &&
|
||||
event is KeyDownEvent &&
|
||||
event.logicalKey == LogicalKeyboardKey.arrowDown) {
|
||||
node.nextFocus();
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
|
||||
return KeyEventResult.ignored;
|
||||
},
|
||||
);
|
||||
controller = TextEditingController(text: widget.query);
|
||||
focusNode = FocusNode(onKeyEvent: _handleKeyEvent);
|
||||
focusNode.requestFocus();
|
||||
controller.selection = TextSelection(
|
||||
baseOffset: 0,
|
||||
extentOffset: controller.text.length,
|
||||
);
|
||||
// Update the text selection after the first frame
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
controller.selection = TextSelection(
|
||||
baseOffset: 0,
|
||||
extentOffset: controller.text.length,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
KeyEventResult _handleKeyEvent(FocusNode node, KeyEvent event) {
|
||||
if (node.hasFocus &&
|
||||
event is KeyDownEvent &&
|
||||
event.logicalKey == LogicalKeyboardKey.arrowDown) {
|
||||
node.nextFocus();
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
|
||||
@override
|
||||
@ -56,21 +58,83 @@ class _SearchFieldState extends State<SearchField> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Widget _buildSuffixIcon(BuildContext context) {
|
||||
return ValueListenableBuilder<TextEditingValue>(
|
||||
valueListenable: controller,
|
||||
builder: (context, value, _) {
|
||||
final hasText = value.text.trim().isNotEmpty;
|
||||
final clearIcon = Container(
|
||||
padding: const EdgeInsets.all(1),
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: AFThemeExtension.of(context).lightGreyHover,
|
||||
),
|
||||
child: const FlowySvg(
|
||||
FlowySvgs.close_s,
|
||||
size: Size.square(16),
|
||||
),
|
||||
);
|
||||
return AnimatedOpacity(
|
||||
opacity: hasText ? 1.0 : 0.0,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: hasText
|
||||
? FlowyTooltip(
|
||||
message: LocaleKeys.commandPalette_clearSearchTooltip.tr(),
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: GestureDetector(
|
||||
onTap: _clearSearch,
|
||||
child: clearIcon,
|
||||
),
|
||||
),
|
||||
)
|
||||
: clearIcon,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Cache theme and text styles
|
||||
final theme = Theme.of(context);
|
||||
final textStyle = theme.textTheme.bodySmall?.copyWith(fontSize: 14);
|
||||
final hintStyle = theme.textTheme.bodySmall?.copyWith(
|
||||
fontSize: 14,
|
||||
color: theme.hintColor,
|
||||
);
|
||||
|
||||
// Choose the leading icon based on loading state
|
||||
final Widget leadingIcon = widget.isLoading
|
||||
? FlowyTooltip(
|
||||
message: LocaleKeys.commandPalette_loadingTooltip.tr(),
|
||||
child: const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(3.0),
|
||||
child: CircularProgressIndicator(strokeWidth: 2.0),
|
||||
),
|
||||
),
|
||||
)
|
||||
: SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: FlowySvg(
|
||||
FlowySvgs.search_m,
|
||||
color: theme.hintColor,
|
||||
),
|
||||
);
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
const HSpace(12),
|
||||
FlowySvg(
|
||||
FlowySvgs.search_m,
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
leadingIcon,
|
||||
Expanded(
|
||||
child: FlowyTextField(
|
||||
focusNode: focusNode,
|
||||
controller: controller,
|
||||
textStyle:
|
||||
Theme.of(context).textTheme.bodySmall?.copyWith(fontSize: 14),
|
||||
textStyle: textStyle,
|
||||
decoration: InputDecoration(
|
||||
constraints: const BoxConstraints(maxHeight: 48),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
@ -80,72 +144,14 @@ class _SearchFieldState extends State<SearchField> {
|
||||
),
|
||||
isDense: false,
|
||||
hintText: LocaleKeys.commandPalette_placeholder.tr(),
|
||||
hintStyle: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
fontSize: 14,
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
errorStyle: Theme.of(context)
|
||||
.textTheme
|
||||
.bodySmall!
|
||||
.copyWith(color: Theme.of(context).colorScheme.error),
|
||||
hintStyle: hintStyle,
|
||||
errorStyle: theme.textTheme.bodySmall!
|
||||
.copyWith(color: theme.colorScheme.error),
|
||||
suffix: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
AnimatedOpacity(
|
||||
opacity: controller.text.trim().isNotEmpty ? 1 : 0,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
final icon = Container(
|
||||
padding: const EdgeInsets.all(1),
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: AFThemeExtension.of(context).lightGreyHover,
|
||||
),
|
||||
child: const FlowySvg(
|
||||
FlowySvgs.close_s,
|
||||
size: Size.square(16),
|
||||
),
|
||||
);
|
||||
if (controller.text.isEmpty) {
|
||||
return icon;
|
||||
}
|
||||
|
||||
return FlowyTooltip(
|
||||
message:
|
||||
LocaleKeys.commandPalette_clearSearchTooltip.tr(),
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: GestureDetector(
|
||||
onTap: controller.text.trim().isNotEmpty
|
||||
? _clearSearch
|
||||
: null,
|
||||
child: icon,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
_buildSuffixIcon(context),
|
||||
const HSpace(8),
|
||||
FlowyTooltip(
|
||||
message: LocaleKeys.commandPalette_betaTooltip.tr(),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 5,
|
||||
vertical: 2,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: AFThemeExtension.of(context).lightGreyHover,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: FlowyText.semibold(
|
||||
LocaleKeys.commandPalette_betaLabel.tr(),
|
||||
fontSize: 11,
|
||||
lineHeight: 1.2,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
counterText: "",
|
||||
@ -155,9 +161,7 @@ class _SearchFieldState extends State<SearchField> {
|
||||
),
|
||||
errorBorder: OutlineInputBorder(
|
||||
borderRadius: Corners.s8Border,
|
||||
borderSide: BorderSide(
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
borderSide: BorderSide(color: theme.colorScheme.error),
|
||||
),
|
||||
),
|
||||
onChanged: (value) => context
|
||||
@ -165,17 +169,6 @@ class _SearchFieldState extends State<SearchField> {
|
||||
.add(CommandPaletteEvent.searchChanged(search: value)),
|
||||
),
|
||||
),
|
||||
if (widget.isLoading) ...[
|
||||
FlowyTooltip(
|
||||
message: LocaleKeys.commandPalette_loadingTooltip.tr(),
|
||||
child: const SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(strokeWidth: 2.5),
|
||||
),
|
||||
),
|
||||
const HSpace(12),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
@ -16,12 +16,12 @@ import 'package:flutter/services.dart';
|
||||
class SearchResultTile extends StatefulWidget {
|
||||
const SearchResultTile({
|
||||
super.key,
|
||||
required this.result,
|
||||
required this.item,
|
||||
required this.onSelected,
|
||||
this.isTrashed = false,
|
||||
});
|
||||
|
||||
final SearchResultPB result;
|
||||
final SearchResponseItemPB item;
|
||||
final VoidCallback onSelected;
|
||||
final bool isTrashed;
|
||||
|
||||
@ -31,7 +31,6 @@ class SearchResultTile extends StatefulWidget {
|
||||
|
||||
class _SearchResultTileState extends State<SearchResultTile> {
|
||||
bool _hasFocus = false;
|
||||
|
||||
final focusNode = FocusNode();
|
||||
|
||||
@override
|
||||
@ -40,104 +39,132 @@ class _SearchResultTileState extends State<SearchResultTile> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// Helper to handle the selection action.
|
||||
void _handleSelection() {
|
||||
widget.onSelected();
|
||||
getIt<ActionNavigationBloc>().add(
|
||||
ActionNavigationEvent.performAction(
|
||||
action: NavigationAction(objectId: widget.item.viewId),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Helper to clean up preview text.
|
||||
String _cleanPreview(String preview) {
|
||||
return preview.replaceAll('\n', ' ').trim();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final title = widget.result.data.orDefault(
|
||||
final title = widget.item.data.orDefault(
|
||||
LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
|
||||
);
|
||||
final icon = widget.result.getIcon();
|
||||
final cleanedPreview = _cleanPreview(widget.result.preview);
|
||||
final icon = widget.item.getIcon();
|
||||
final cleanedPreview = _cleanPreview(widget.item.preview);
|
||||
final hasPreview = cleanedPreview.isNotEmpty;
|
||||
final trashHintText =
|
||||
widget.isTrashed ? LocaleKeys.commandPalette_fromTrashHint.tr() : null;
|
||||
|
||||
// Build the tile content based on preview availability.
|
||||
Widget tileContent;
|
||||
if (hasPreview) {
|
||||
tileContent = Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
if (icon != null) ...[
|
||||
SizedBox(width: 24, child: icon),
|
||||
const HSpace(6),
|
||||
],
|
||||
Flexible(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (widget.isTrashed)
|
||||
FlowyText(
|
||||
trashHintText!,
|
||||
color: AFThemeExtension.of(context)
|
||||
.textColor
|
||||
.withAlpha(175),
|
||||
fontSize: 10,
|
||||
),
|
||||
FlowyText(
|
||||
title,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const VSpace(4),
|
||||
_DocumentPreview(preview: cleanedPreview),
|
||||
],
|
||||
);
|
||||
} else {
|
||||
tileContent = Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (icon != null) ...[
|
||||
SizedBox(width: 24, child: icon),
|
||||
const HSpace(6),
|
||||
],
|
||||
Flexible(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (widget.isTrashed)
|
||||
FlowyText(
|
||||
trashHintText!,
|
||||
color:
|
||||
AFThemeExtension.of(context).textColor.withAlpha(175),
|
||||
fontSize: 10,
|
||||
),
|
||||
FlowyText(
|
||||
title,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () {
|
||||
widget.onSelected();
|
||||
|
||||
getIt<ActionNavigationBloc>().add(
|
||||
ActionNavigationEvent.performAction(
|
||||
action: NavigationAction(objectId: widget.result.viewId),
|
||||
),
|
||||
);
|
||||
},
|
||||
onTap: _handleSelection,
|
||||
child: Focus(
|
||||
focusNode: focusNode,
|
||||
onKeyEvent: (node, event) {
|
||||
if (event is! KeyDownEvent) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
|
||||
if (event is! KeyDownEvent) return KeyEventResult.ignored;
|
||||
if (event.logicalKey == LogicalKeyboardKey.enter) {
|
||||
widget.onSelected();
|
||||
|
||||
getIt<ActionNavigationBloc>().add(
|
||||
ActionNavigationEvent.performAction(
|
||||
action: NavigationAction(objectId: widget.result.viewId),
|
||||
),
|
||||
);
|
||||
_handleSelection();
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
|
||||
return KeyEventResult.ignored;
|
||||
},
|
||||
onFocusChange: (hasFocus) => setState(() => _hasFocus = hasFocus),
|
||||
child: FlowyHover(
|
||||
isSelected: () => _hasFocus,
|
||||
style: HoverStyle(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
hoverColor:
|
||||
Theme.of(context).colorScheme.primary.withValues(alpha: 0.1),
|
||||
foregroundColorOnHover: AFThemeExtension.of(context).textColor,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
// page icon
|
||||
if (icon != null) ...[
|
||||
SizedBox(width: 24, child: icon),
|
||||
const HSpace(6),
|
||||
],
|
||||
Flexible(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// if the result is trashed, show a hint
|
||||
if (widget.isTrashed) ...[
|
||||
FlowyText(
|
||||
LocaleKeys.commandPalette_fromTrashHint.tr(),
|
||||
color: AFThemeExtension.of(context)
|
||||
.textColor
|
||||
.withAlpha(175),
|
||||
fontSize: 10,
|
||||
),
|
||||
],
|
||||
// page title
|
||||
FlowyText(
|
||||
title,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
// content preview
|
||||
if (cleanedPreview.isNotEmpty) ...[
|
||||
const VSpace(4),
|
||||
_DocumentPreview(preview: cleanedPreview),
|
||||
],
|
||||
],
|
||||
padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 6),
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(minHeight: 30),
|
||||
child: tileContent,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _cleanPreview(String preview) {
|
||||
return preview.replaceAll('\n', ' ').trim();
|
||||
}
|
||||
}
|
||||
|
||||
class _DocumentPreview extends StatelessWidget {
|
||||
@ -147,9 +174,9 @@ class _DocumentPreview extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Combine the horizontal padding for clarity:
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16) +
|
||||
const EdgeInsets.only(left: 14),
|
||||
padding: const EdgeInsets.fromLTRB(30, 0, 16, 0),
|
||||
child: FlowyText.regular(
|
||||
preview,
|
||||
color: Theme.of(context).hintColor,
|
||||
|
@ -7,41 +7,99 @@ import 'package:appflowy_backend/protobuf/flowy-search/result.pb.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
|
||||
class SearchResultsList extends StatelessWidget {
|
||||
const SearchResultsList({
|
||||
super.key,
|
||||
class SearchResultList extends StatelessWidget {
|
||||
const SearchResultList({
|
||||
required this.trash,
|
||||
required this.results,
|
||||
required this.resultItems,
|
||||
required this.resultSummaries,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final List<TrashPB> trash;
|
||||
final List<SearchResultPB> results;
|
||||
final List<SearchResponseItemPB> resultItems;
|
||||
final List<SearchSummaryPB> resultSummaries;
|
||||
|
||||
Widget _buildSectionHeader(String title) => Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8) +
|
||||
const EdgeInsets.only(left: 8),
|
||||
child: Opacity(
|
||||
opacity: 0.6,
|
||||
child: FlowyText(title, fontSize: 12),
|
||||
),
|
||||
);
|
||||
|
||||
Widget _buildSummariesSection() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildSectionHeader(LocaleKeys.commandPalette_aiSummary.tr()),
|
||||
ListView.separated(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
shrinkWrap: true,
|
||||
itemCount: resultSummaries.length,
|
||||
separatorBuilder: (_, __) => const Divider(height: 0),
|
||||
itemBuilder: (_, index) => SearchSummaryTile(
|
||||
summary: resultSummaries[index],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildResultsSection(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 8),
|
||||
_buildSectionHeader(LocaleKeys.commandPalette_bestMatches.tr()),
|
||||
ListView.separated(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
shrinkWrap: true,
|
||||
itemCount: resultItems.length,
|
||||
separatorBuilder: (_, __) => const Divider(height: 0),
|
||||
itemBuilder: (_, index) {
|
||||
final item = resultItems[index];
|
||||
return SearchResultTile(
|
||||
item: item,
|
||||
onSelected: () => FlowyOverlay.pop(context),
|
||||
isTrashed: trash.any((t) => t.id == item.viewId),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListView.separated(
|
||||
shrinkWrap: true,
|
||||
physics: const ClampingScrollPhysics(),
|
||||
separatorBuilder: (_, __) => const Divider(height: 0),
|
||||
itemCount: results.length + 1,
|
||||
itemBuilder: (_, index) {
|
||||
if (index == 0) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8) +
|
||||
const EdgeInsets.only(left: 16),
|
||||
child: FlowyText(
|
||||
LocaleKeys.commandPalette_bestMatches.tr(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final result = results[index - 1];
|
||||
return SearchResultTile(
|
||||
result: result,
|
||||
onSelected: () => FlowyOverlay.pop(context),
|
||||
isTrashed: trash.any((t) => t.id == result.viewId),
|
||||
);
|
||||
},
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6),
|
||||
child: ListView(
|
||||
shrinkWrap: true,
|
||||
physics: const ClampingScrollPhysics(),
|
||||
children: [
|
||||
if (resultSummaries.isNotEmpty) _buildSummariesSection(),
|
||||
const VSpace(10),
|
||||
if (resultItems.isNotEmpty) _buildResultsSection(context),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SearchSummaryTile extends StatelessWidget {
|
||||
const SearchSummaryTile({required this.summary, super.key});
|
||||
|
||||
final SearchSummaryPB summary;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: FlowyText(
|
||||
summary.content,
|
||||
maxLines: 10,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -2693,6 +2693,7 @@
|
||||
"commandPalette": {
|
||||
"placeholder": "Search or ask a question...",
|
||||
"bestMatches": "Best matches",
|
||||
"aiSummary": "AI summary",
|
||||
"recentHistory": "Recent history",
|
||||
"navigateHint": "to navigate",
|
||||
"loadingTooltip": "We are looking for results...",
|
||||
|
334
frontend/rust-lib/Cargo.lock
generated
334
frontend/rust-lib/Cargo.lock
generated
@ -345,7 +345,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "af-local-ai"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=9d731d89ea2e0fd764da2effa8a456210c5a39c3#9d731d89ea2e0fd764da2effa8a456210c5a39c3"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=093d3f4916a7b924ac66b4f0a9d81cc5fcc72eaa#093d3f4916a7b924ac66b4f0a9d81cc5fcc72eaa"
|
||||
dependencies = [
|
||||
"af-plugin",
|
||||
"anyhow",
|
||||
@ -362,7 +362,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "af-mcp"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=9d731d89ea2e0fd764da2effa8a456210c5a39c3#9d731d89ea2e0fd764da2effa8a456210c5a39c3"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=093d3f4916a7b924ac66b4f0a9d81cc5fcc72eaa#093d3f4916a7b924ac66b4f0a9d81cc5fcc72eaa"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"futures-util",
|
||||
@ -376,7 +376,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "af-plugin"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=9d731d89ea2e0fd764da2effa8a456210c5a39c3#9d731d89ea2e0fd764da2effa8a456210c5a39c3"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=093d3f4916a7b924ac66b4f0a9d81cc5fcc72eaa#093d3f4916a7b924ac66b4f0a9d81cc5fcc72eaa"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"cfg-if",
|
||||
@ -493,7 +493,7 @@ checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f"
|
||||
[[package]]
|
||||
name = "app-error"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f300884#f300884dde348ce855712b8e5a3ff3066682d160"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c95dcc6bc7762d4b89d3d382e8ed2b6a68742ba6#c95dcc6bc7762d4b89d3d382e8ed2b6a68742ba6"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bincode",
|
||||
@ -513,7 +513,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "appflowy-ai-client"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f300884#f300884dde348ce855712b8e5a3ff3066682d160"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c95dcc6bc7762d4b89d3d382e8ed2b6a68742ba6#c95dcc6bc7762d4b89d3d382e8ed2b6a68742ba6"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
@ -604,7 +604,7 @@ dependencies = [
|
||||
"backoff",
|
||||
"base64 0.21.5",
|
||||
"bytes",
|
||||
"derive_builder",
|
||||
"derive_builder 0.12.0",
|
||||
"futures",
|
||||
"rand 0.8.5",
|
||||
"reqwest 0.11.27",
|
||||
@ -632,9 +632,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "async-stream"
|
||||
version = "0.3.5"
|
||||
version = "0.3.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51"
|
||||
checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476"
|
||||
dependencies = [
|
||||
"async-stream-impl",
|
||||
"futures-core",
|
||||
@ -643,9 +643,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "async-stream-impl"
|
||||
version = "0.3.5"
|
||||
version = "0.3.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193"
|
||||
checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@ -887,6 +887,31 @@ dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bon"
|
||||
version = "3.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92c5f8abc69af414cbd6f2103bb668b91e584072f2105e4b38bed79b6ad0975f"
|
||||
dependencies = [
|
||||
"bon-macros",
|
||||
"rustversion",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bon-macros"
|
||||
version = "3.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b69edf39b6f321cb2699a93fc20c256adb839719c42676d03f7aa975e4e5581d"
|
||||
dependencies = [
|
||||
"darling 0.20.11",
|
||||
"ident_case",
|
||||
"prettyplease",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustversion",
|
||||
"syn 2.0.94",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "borsh"
|
||||
version = "1.5.1"
|
||||
@ -1134,7 +1159,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "client-api"
|
||||
version = "0.2.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f300884#f300884dde348ce855712b8e5a3ff3066682d160"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c95dcc6bc7762d4b89d3d382e8ed2b6a68742ba6#c95dcc6bc7762d4b89d3d382e8ed2b6a68742ba6"
|
||||
dependencies = [
|
||||
"again",
|
||||
"anyhow",
|
||||
@ -1189,7 +1214,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "client-api-entity"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f300884#f300884dde348ce855712b8e5a3ff3066682d160"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c95dcc6bc7762d4b89d3d382e8ed2b6a68742ba6#c95dcc6bc7762d4b89d3d382e8ed2b6a68742ba6"
|
||||
dependencies = [
|
||||
"collab-entity",
|
||||
"collab-rt-entity",
|
||||
@ -1202,7 +1227,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "client-websocket"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f300884#f300884dde348ce855712b8e5a3ff3066682d160"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c95dcc6bc7762d4b89d3d382e8ed2b6a68742ba6#c95dcc6bc7762d4b89d3d382e8ed2b6a68742ba6"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-util",
|
||||
@ -1474,7 +1499,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-rt-entity"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f300884#f300884dde348ce855712b8e5a3ff3066682d160"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c95dcc6bc7762d4b89d3d382e8ed2b6a68742ba6#c95dcc6bc7762d4b89d3d382e8ed2b6a68742ba6"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bincode",
|
||||
@ -1496,7 +1521,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-rt-protocol"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f300884#f300884dde348ce855712b8e5a3ff3066682d160"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c95dcc6bc7762d4b89d3d382e8ed2b6a68742ba6#c95dcc6bc7762d4b89d3d382e8ed2b6a68742ba6"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@ -1761,7 +1786,7 @@ dependencies = [
|
||||
"cssparser-macros",
|
||||
"dtoa-short",
|
||||
"itoa",
|
||||
"phf 0.8.0",
|
||||
"phf 0.11.2",
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
@ -1817,12 +1842,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "darling"
|
||||
version = "0.20.8"
|
||||
version = "0.20.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "54e36fcd13ed84ffdfda6f5be89b31287cbb80c439841fe69e04841435464391"
|
||||
checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee"
|
||||
dependencies = [
|
||||
"darling_core 0.20.8",
|
||||
"darling_macro 0.20.8",
|
||||
"darling_core 0.20.11",
|
||||
"darling_macro 0.20.11",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1841,15 +1866,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "darling_core"
|
||||
version = "0.20.8"
|
||||
version = "0.20.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c2cf1c23a687a1feeb728783b993c4e1ad83d99f351801977dd809b48d0a70f"
|
||||
checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e"
|
||||
dependencies = [
|
||||
"fnv",
|
||||
"ident_case",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strsim 0.10.0",
|
||||
"strsim 0.11.1",
|
||||
"syn 2.0.94",
|
||||
]
|
||||
|
||||
@ -1866,11 +1891,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "darling_macro"
|
||||
version = "0.20.8"
|
||||
version = "0.20.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f"
|
||||
checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
|
||||
dependencies = [
|
||||
"darling_core 0.20.8",
|
||||
"darling_core 0.20.11",
|
||||
"quote",
|
||||
"syn 2.0.94",
|
||||
]
|
||||
@ -1944,7 +1969,7 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308"
|
||||
[[package]]
|
||||
name = "database-entity"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f300884#f300884dde348ce855712b8e5a3ff3066682d160"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c95dcc6bc7762d4b89d3d382e8ed2b6a68742ba6#c95dcc6bc7762d4b89d3d382e8ed2b6a68742ba6"
|
||||
dependencies = [
|
||||
"bincode",
|
||||
"bytes",
|
||||
@ -2027,7 +2052,16 @@ version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8d67778784b508018359cbc8696edb3db78160bab2c2a28ba7f56ef6932997f8"
|
||||
dependencies = [
|
||||
"derive_builder_macro",
|
||||
"derive_builder_macro 0.12.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_builder"
|
||||
version = "0.20.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947"
|
||||
dependencies = [
|
||||
"derive_builder_macro 0.20.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2042,16 +2076,38 @@ dependencies = [
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_builder_core"
|
||||
version = "0.20.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8"
|
||||
dependencies = [
|
||||
"darling 0.20.11",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.94",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_builder_macro"
|
||||
version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebcda35c7a396850a55ffeac740804b40ffec779b98fffbb1738f4033f0ee79e"
|
||||
dependencies = [
|
||||
"derive_builder_core",
|
||||
"derive_builder_core 0.12.0",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_builder_macro"
|
||||
version = "0.20.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c"
|
||||
dependencies = [
|
||||
"derive_builder_core 0.20.2",
|
||||
"syn 2.0.94",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_more"
|
||||
version = "0.99.17"
|
||||
@ -2162,9 +2218,9 @@ checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f"
|
||||
|
||||
[[package]]
|
||||
name = "downcast-rs"
|
||||
version = "1.2.0"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650"
|
||||
checksum = "ea8a8b81cacc08888170eef4d13b775126db426d0b348bee9d18c2c1eaf123cf"
|
||||
|
||||
[[package]]
|
||||
name = "dtoa"
|
||||
@ -2484,7 +2540,6 @@ dependencies = [
|
||||
"lib-dispatch",
|
||||
"lib-infra",
|
||||
"log",
|
||||
"md5",
|
||||
"notify",
|
||||
"pin-project",
|
||||
"protobuf",
|
||||
@ -2501,8 +2556,6 @@ dependencies = [
|
||||
"tracing-subscriber",
|
||||
"uuid",
|
||||
"validator 0.18.1",
|
||||
"zip 2.2.0",
|
||||
"zip-extensions",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2857,15 +2910,16 @@ dependencies = [
|
||||
name = "flowy-search"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"allo-isolate",
|
||||
"async-stream",
|
||||
"bytes",
|
||||
"collab",
|
||||
"collab-folder",
|
||||
"derive_builder 0.20.2",
|
||||
"flowy-codegen",
|
||||
"flowy-derive",
|
||||
"flowy-error",
|
||||
"flowy-folder",
|
||||
"flowy-notification",
|
||||
"flowy-search-pub",
|
||||
"flowy-user",
|
||||
"futures",
|
||||
@ -2874,11 +2928,11 @@ dependencies = [
|
||||
"protobuf",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"strsim 0.11.0",
|
||||
"strsim 0.11.1",
|
||||
"strum_macros 0.26.1",
|
||||
"tantivy",
|
||||
"tempfile",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"tracing",
|
||||
"uuid",
|
||||
]
|
||||
@ -3290,19 +3344,6 @@ dependencies = [
|
||||
"byteorder",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "generator"
|
||||
version = "0.7.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5cc16584ff22b460a382b7feec54b23d2908d858152e5739a120b949293bd74e"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"log",
|
||||
"rustversion",
|
||||
"windows 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.14.7"
|
||||
@ -3418,7 +3459,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "gotrue"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f300884#f300884dde348ce855712b8e5a3ff3066682d160"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c95dcc6bc7762d4b89d3d382e8ed2b6a68742ba6#c95dcc6bc7762d4b89d3d382e8ed2b6a68742ba6"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"getrandom 0.2.10",
|
||||
@ -3433,7 +3474,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "gotrue-entity"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f300884#f300884dde348ce855712b8e5a3ff3066682d160"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c95dcc6bc7762d4b89d3d382e8ed2b6a68742ba6#c95dcc6bc7762d4b89d3d382e8ed2b6a68742ba6"
|
||||
dependencies = [
|
||||
"app-error",
|
||||
"jsonwebtoken",
|
||||
@ -3527,12 +3568,6 @@ version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b"
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.5.0"
|
||||
@ -3802,6 +3837,15 @@ dependencies = [
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyperloglogplus"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "621debdf94dcac33e50475fdd76d34d5ea9c0362a834b9db08c3024696c1fbe3"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone"
|
||||
version = "0.1.61"
|
||||
@ -4054,7 +4098,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "infra"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f300884#f300884dde348ce855712b8e5a3ff3066682d160"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c95dcc6bc7762d4b89d3d382e8ed2b6a68742ba6#c95dcc6bc7762d4b89d3d382e8ed2b6a68742ba6"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
@ -4104,9 +4148,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -4121,7 +4162,7 @@ version = "0.4.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9"
|
||||
dependencies = [
|
||||
"hermit-abi 0.5.0",
|
||||
"hermit-abi",
|
||||
"libc",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
@ -4144,6 +4185,15 @@ dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.9"
|
||||
@ -4416,20 +4466,6 @@ version = "0.4.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
|
||||
|
||||
[[package]]
|
||||
name = "loom"
|
||||
version = "0.5.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff50ecb28bb86013e935fb6683ab1f6d3a20016f123c76fd4c27470076ac30f5"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"generator",
|
||||
"pin-utils",
|
||||
"scoped-tls",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lru"
|
||||
version = "0.12.3"
|
||||
@ -4608,11 +4644,10 @@ checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771"
|
||||
|
||||
[[package]]
|
||||
name = "measure_time"
|
||||
version = "0.8.2"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56220900f1a0923789ecd6bf25fbae8af3b2f1ff3e9e297fc9b6b8674dd4d852"
|
||||
checksum = "51c55d61e72fc3ab704396c5fa16f4c184db37978ae4e94ca8959693a235fc0e"
|
||||
dependencies = [
|
||||
"instant",
|
||||
"log",
|
||||
]
|
||||
|
||||
@ -4876,16 +4911,6 @@ dependencies = [
|
||||
"libm",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num_cpus"
|
||||
version = "1.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43"
|
||||
dependencies = [
|
||||
"hermit-abi 0.3.2",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "object"
|
||||
version = "0.32.1"
|
||||
@ -4903,12 +4928,9 @@ checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
|
||||
|
||||
[[package]]
|
||||
name = "oneshot"
|
||||
version = "0.1.6"
|
||||
version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6f6640c6bda7731b1fdbab747981a0f896dd1fedaf9f4a53fa237a04a84431f4"
|
||||
dependencies = [
|
||||
"loom",
|
||||
]
|
||||
checksum = "b4ce411919553d3f9fa53a0880544cda985a112117a0444d5ff1e870a893d6ea"
|
||||
|
||||
[[package]]
|
||||
name = "opaque-debug"
|
||||
@ -4988,9 +5010,9 @@ checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
|
||||
|
||||
[[package]]
|
||||
name = "ownedbytes"
|
||||
version = "0.7.0"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3a059efb063b8f425b948e042e6b9bd85edfe60e913630ed727b23e2dfcc558"
|
||||
checksum = "2fbd56f7631767e61784dc43f8580f403f4475bd4aaa4da003e6295e1bab4a7e"
|
||||
dependencies = [
|
||||
"stable_deref_trait",
|
||||
]
|
||||
@ -5167,7 +5189,7 @@ version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12"
|
||||
dependencies = [
|
||||
"phf_macros",
|
||||
"phf_macros 0.8.0",
|
||||
"phf_shared 0.8.0",
|
||||
"proc-macro-hack",
|
||||
]
|
||||
@ -5187,6 +5209,7 @@ version = "0.11.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc"
|
||||
dependencies = [
|
||||
"phf_macros 0.11.3",
|
||||
"phf_shared 0.11.2",
|
||||
]
|
||||
|
||||
@ -5254,6 +5277,19 @@ dependencies = [
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_macros"
|
||||
version = "0.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216"
|
||||
dependencies = [
|
||||
"phf_generator 0.11.2",
|
||||
"phf_shared 0.11.2",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.94",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_shared"
|
||||
version = "0.8.0"
|
||||
@ -6482,12 +6518,6 @@ dependencies = [
|
||||
"parking_lot 0.12.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scoped-tls"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
|
||||
|
||||
[[package]]
|
||||
name = "scopeguard"
|
||||
version = "1.2.0"
|
||||
@ -6606,18 +6636,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.210"
|
||||
version = "1.0.219"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a"
|
||||
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.210"
|
||||
version = "1.0.219"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f"
|
||||
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@ -6754,7 +6784,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "shared-entity"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=f300884#f300884dde348ce855712b8e5a3ff3066682d160"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c95dcc6bc7762d4b89d3d382e8ed2b6a68742ba6#c95dcc6bc7762d4b89d3d382e8ed2b6a68742ba6"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"app-error",
|
||||
@ -6844,9 +6874,9 @@ checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d"
|
||||
|
||||
[[package]]
|
||||
name = "sketches-ddsketch"
|
||||
version = "0.2.2"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85636c14b73d81f541e525f585c0a2109e6744e1565b5c1668e31c70c10ed65c"
|
||||
checksum = "c1e9a774a6c28142ac54bb25d25562e6bcf957493a184f15ad4eebccb23e410a"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
@ -6967,9 +6997,9 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.0"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "strum"
|
||||
@ -7093,7 +7123,7 @@ dependencies = [
|
||||
"ntapi",
|
||||
"once_cell",
|
||||
"rayon",
|
||||
"windows 0.52.0",
|
||||
"windows",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -7146,14 +7176,15 @@ checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417"
|
||||
|
||||
[[package]]
|
||||
name = "tantivy"
|
||||
version = "0.22.0"
|
||||
version = "0.24.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8d0582f186c0a6d55655d24543f15e43607299425c5ad8352c242b914b31856"
|
||||
checksum = "b21ad8b222d71c57aa979353ed702f0bc6d97e66d368962cbded57fbd19eedd7"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"arc-swap",
|
||||
"base64 0.22.1",
|
||||
"bitpacking",
|
||||
"bon",
|
||||
"byteorder",
|
||||
"census",
|
||||
"crc32fast",
|
||||
@ -7163,20 +7194,20 @@ dependencies = [
|
||||
"fnv",
|
||||
"fs4",
|
||||
"htmlescape",
|
||||
"itertools 0.12.1",
|
||||
"hyperloglogplus",
|
||||
"itertools 0.14.0",
|
||||
"levenshtein_automata",
|
||||
"log",
|
||||
"lru",
|
||||
"lz4_flex",
|
||||
"measure_time",
|
||||
"memmap2",
|
||||
"num_cpus",
|
||||
"once_cell",
|
||||
"oneshot",
|
||||
"rayon",
|
||||
"regex",
|
||||
"rust-stemmers",
|
||||
"rustc-hash 1.1.0",
|
||||
"rustc-hash 2.1.0",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sketches-ddsketch",
|
||||
@ -7189,7 +7220,7 @@ dependencies = [
|
||||
"tantivy-stacker",
|
||||
"tantivy-tokenizer-api",
|
||||
"tempfile",
|
||||
"thiserror 1.0.64",
|
||||
"thiserror 2.0.9",
|
||||
"time",
|
||||
"uuid",
|
||||
"winapi",
|
||||
@ -7197,22 +7228,22 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tantivy-bitpacker"
|
||||
version = "0.6.0"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "284899c2325d6832203ac6ff5891b297fc5239c3dc754c5bc1977855b23c10df"
|
||||
checksum = "1adc286a39e089ae9938935cd488d7d34f14502544a36607effd2239ff0e2494"
|
||||
dependencies = [
|
||||
"bitpacking",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tantivy-columnar"
|
||||
version = "0.3.0"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "12722224ffbe346c7fec3275c699e508fd0d4710e629e933d5736ec524a1f44e"
|
||||
checksum = "6300428e0c104c4f7db6f95b466a6f5c1b9aece094ec57cdd365337908dc7344"
|
||||
dependencies = [
|
||||
"downcast-rs",
|
||||
"fastdivide",
|
||||
"itertools 0.12.1",
|
||||
"itertools 0.14.0",
|
||||
"serde",
|
||||
"tantivy-bitpacker",
|
||||
"tantivy-common",
|
||||
@ -7222,9 +7253,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tantivy-common"
|
||||
version = "0.7.0"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8019e3cabcfd20a1380b491e13ff42f57bb38bf97c3d5fa5c07e50816e0621f4"
|
||||
checksum = "e91b6ea6090ce03dc72c27d0619e77185d26cc3b20775966c346c6d4f7e99d7f"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"byteorder",
|
||||
@ -7246,19 +7277,23 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tantivy-query-grammar"
|
||||
version = "0.22.0"
|
||||
version = "0.24.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "847434d4af57b32e309f4ab1b4f1707a6c566656264caa427ff4285c4d9d0b82"
|
||||
checksum = "e810cdeeebca57fc3f7bfec5f85fdbea9031b2ac9b990eb5ff49b371d52bbe6a"
|
||||
dependencies = [
|
||||
"nom",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tantivy-sstable"
|
||||
version = "0.3.0"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c69578242e8e9fc989119f522ba5b49a38ac20f576fc778035b96cc94f41f98e"
|
||||
checksum = "709f22c08a4c90e1b36711c1c6cad5ae21b20b093e535b69b18783dd2cb99416"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"itertools 0.14.0",
|
||||
"tantivy-bitpacker",
|
||||
"tantivy-common",
|
||||
"tantivy-fst",
|
||||
@ -7267,9 +7302,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tantivy-stacker"
|
||||
version = "0.3.0"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c56d6ff5591fc332739b3ce7035b57995a3ce29a93ffd6012660e0949c956ea8"
|
||||
checksum = "2bcdebb267671311d1e8891fd9d1301803fdb8ad21ba22e0a30d0cab49ba59c1"
|
||||
dependencies = [
|
||||
"murmurhash32",
|
||||
"rand_distr",
|
||||
@ -7278,9 +7313,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tantivy-tokenizer-api"
|
||||
version = "0.3.0"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a0dcade25819a89cfe6f17d932c9cedff11989936bf6dd4f336d50392053b04"
|
||||
checksum = "dfa942fcee81e213e09715bbce8734ae2180070b97b33839a795ba1de201547d"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
@ -7303,14 +7338,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.10.0"
|
||||
version = "3.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a365e8cd18e44762ef95d87f284f4b5cd04107fec2ff3052bd6a3e6069669e67"
|
||||
checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"fastrand",
|
||||
"once_cell",
|
||||
"rustix",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -8213,7 +8249,7 @@ version = "0.18.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df0bcf92720c40105ac4b2dda2a4ea3aa717d4d6a862cc217da653a4bd5c6b10"
|
||||
dependencies = [
|
||||
"darling 0.20.8",
|
||||
"darling 0.20.11",
|
||||
"once_cell",
|
||||
"proc-macro-error",
|
||||
"proc-macro2",
|
||||
@ -8227,7 +8263,7 @@ version = "0.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bac855a2ce6f843beb229757e6e570a42e837bcb15e5f449dd48d5747d41bf77"
|
||||
dependencies = [
|
||||
"darling 0.20.8",
|
||||
"darling 0.20.11",
|
||||
"once_cell",
|
||||
"proc-macro-error2",
|
||||
"proc-macro2",
|
||||
@ -8489,15 +8525,6 @@ version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.48.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f"
|
||||
dependencies = [
|
||||
"windows-targets 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.52.0"
|
||||
@ -9008,15 +9035,6 @@ dependencies = [
|
||||
"zstd 0.13.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zip-extensions"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eb0a99499b3497d765525c5d05e3ade9ca4a731c184365c19472c3fd6ba86341"
|
||||
dependencies = [
|
||||
"zip 2.2.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zopfli"
|
||||
version = "0.8.1"
|
||||
|
@ -97,14 +97,16 @@ validator = { version = "0.18", features = ["derive"] }
|
||||
tokio-util = "0.7.11"
|
||||
zip = "2.2.0"
|
||||
dashmap = "6.0.1"
|
||||
derive_builder = "0.20.2"
|
||||
tantivy = { version = "0.24.0" }
|
||||
|
||||
# Please using the following command to update the revision id
|
||||
# Current directory: frontend
|
||||
# Run the script.add_workspace_members:
|
||||
# scripts/tool/update_client_api_rev.sh new_rev_id
|
||||
# ⚠️⚠️⚠️️
|
||||
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "f300884" }
|
||||
client-api-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "f300884" }
|
||||
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "c95dcc6bc7762d4b89d3d382e8ed2b6a68742ba6" }
|
||||
client-api-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "c95dcc6bc7762d4b89d3d382e8ed2b6a68742ba6" }
|
||||
|
||||
[profile.dev]
|
||||
opt-level = 0
|
||||
@ -152,6 +154,6 @@ collab-importer = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFl
|
||||
# To update the commit ID, run:
|
||||
# scripts/tool/update_local_ai_rev.sh new_rev_id
|
||||
# ⚠️⚠️⚠️️
|
||||
af-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "9d731d89ea2e0fd764da2effa8a456210c5a39c3" }
|
||||
af-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "9d731d89ea2e0fd764da2effa8a456210c5a39c3" }
|
||||
af-mcp = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "9d731d89ea2e0fd764da2effa8a456210c5a39c3" }
|
||||
af-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "093d3f4916a7b924ac66b4f0a9d81cc5fcc72eaa" }
|
||||
af-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "093d3f4916a7b924ac66b4f0a9d81cc5fcc72eaa" }
|
||||
af-mcp = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "093d3f4916a7b924ac66b4f0a9d81cc5fcc72eaa" }
|
||||
|
@ -41,9 +41,6 @@ reqwest = { version = "0.11.27", features = ["json"] }
|
||||
sha2 = "0.10.7"
|
||||
base64 = "0.21.5"
|
||||
futures-util = "0.3.30"
|
||||
md5 = "0.7.0"
|
||||
zip = { workspace = true, features = ["deflate"] }
|
||||
zip-extensions = "0.8.0"
|
||||
pin-project = "1.1.5"
|
||||
flowy-storage-pub = { workspace = true }
|
||||
collab-integrate.workspace = true
|
||||
|
@ -1,6 +1,6 @@
|
||||
use crate::server_layer::{Server, ServerProvider};
|
||||
use client_api::collab_sync::{SinkConfig, SyncObject, SyncPlugin};
|
||||
use client_api::entity::ai_dto::RepeatedRelatedQuestion;
|
||||
use client_api::entity::search_dto::SearchDocumentResponseItem;
|
||||
use client_api::entity::workspace_dto::PublishInfoView;
|
||||
use client_api::entity::PublishInfo;
|
||||
use collab::core::origin::{CollabClient, CollabOrigin};
|
||||
@ -10,6 +10,9 @@ use collab_entity::CollabType;
|
||||
use collab_integrate::collab_builder::{
|
||||
CollabCloudPluginProvider, CollabPluginProviderContext, CollabPluginProviderType,
|
||||
};
|
||||
use flowy_ai_pub::cloud::search_dto::{
|
||||
SearchDocumentResponseItem, SearchResult, SearchSummaryResult,
|
||||
};
|
||||
use flowy_ai_pub::cloud::{
|
||||
AIModel, ChatCloudService, ChatMessage, ChatMessageMetadata, ChatMessageType, ChatSettings,
|
||||
CompleteTextParams, LocalAIConfig, MessageCursor, ModelList, RepeatedChatMessage, ResponseFormat,
|
||||
@ -46,8 +49,6 @@ use tracing::log::error;
|
||||
use tracing::{debug, info};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::server_layer::{Server, ServerProvider};
|
||||
|
||||
#[async_trait]
|
||||
impl StorageCloudService for ServerProvider {
|
||||
async fn get_object_url(&self, object_id: ObjectIdentity) -> Result<String, FlowyError> {
|
||||
@ -874,4 +875,21 @@ impl SearchCloudService for ServerProvider {
|
||||
None => Err(FlowyError::internal().with_context("SearchCloudService not found")),
|
||||
}
|
||||
}
|
||||
|
||||
async fn generate_search_summary(
|
||||
&self,
|
||||
workspace_id: &Uuid,
|
||||
query: String,
|
||||
search_results: Vec<SearchResult>,
|
||||
) -> Result<SearchSummaryResult, FlowyError> {
|
||||
let server = self.get_server()?;
|
||||
match server.search_service() {
|
||||
Some(search_service) => {
|
||||
search_service
|
||||
.generate_search_summary(workspace_id, query, search_results)
|
||||
.await
|
||||
},
|
||||
None => Err(FlowyError::internal().with_context("SearchCloudService not found")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -82,12 +82,13 @@ impl UserWorkspaceService for UserWorkspaceServiceImpl {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn did_delete_workspace(&self, workspace_id: &Uuid) -> FlowyResult<()> {
|
||||
async fn did_delete_workspace(&self, workspace_id: &Uuid) -> FlowyResult<()> {
|
||||
// The remove_indices_for_workspace should not block the deletion of the workspace
|
||||
// Log the error and continue
|
||||
if let Err(err) = self
|
||||
.folder_manager
|
||||
.remove_indices_for_workspace(workspace_id)
|
||||
.await
|
||||
{
|
||||
info!("Error removing indices for workspace: {}", err);
|
||||
}
|
||||
|
@ -165,9 +165,9 @@ impl AppFlowyCore {
|
||||
collab_builder
|
||||
.set_snapshot_persistence(Arc::new(SnapshotDBImpl(Arc::downgrade(&authenticate_user))));
|
||||
|
||||
let folder_indexer = Arc::new(FolderIndexManagerImpl::new(Some(Arc::downgrade(
|
||||
let folder_indexer = Arc::new(FolderIndexManagerImpl::new(Arc::downgrade(
|
||||
&authenticate_user,
|
||||
))));
|
||||
)));
|
||||
|
||||
let folder_manager = FolderDepsResolver::resolve(
|
||||
Arc::downgrade(&authenticate_user),
|
||||
|
@ -33,7 +33,7 @@ collab-document = { workspace = true, optional = true }
|
||||
collab-plugins = { workspace = true, optional = true }
|
||||
collab-folder = { workspace = true, optional = true }
|
||||
client-api = { workspace = true, optional = true }
|
||||
tantivy = { version = "0.22.0", optional = true }
|
||||
tantivy = { workspace = true, optional = true }
|
||||
uuid.workspace = true
|
||||
|
||||
[features]
|
||||
|
@ -2051,10 +2051,11 @@ impl FolderManager {
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn remove_indices_for_workspace(&self, workspace_id: &Uuid) -> FlowyResult<()> {
|
||||
pub async fn remove_indices_for_workspace(&self, workspace_id: &Uuid) -> FlowyResult<()> {
|
||||
self
|
||||
.folder_indexer
|
||||
.remove_indices_for_workspace(*workspace_id)?;
|
||||
.remove_indices_for_workspace(*workspace_id)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -115,8 +115,9 @@ impl FolderManager {
|
||||
let index_content_rx = folder.subscribe_index_content();
|
||||
self
|
||||
.folder_indexer
|
||||
.set_index_content_receiver(index_content_rx, *workspace_id);
|
||||
self.handle_index_folder(*workspace_id, &folder);
|
||||
.set_index_content_receiver(index_content_rx, *workspace_id)
|
||||
.await;
|
||||
self.handle_index_folder(*workspace_id, &folder).await;
|
||||
folder_state_rx
|
||||
};
|
||||
|
||||
@ -137,6 +138,8 @@ impl FolderManager {
|
||||
Arc::downgrade(&self.user),
|
||||
);
|
||||
|
||||
self.folder_indexer.initialize().await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -166,7 +169,7 @@ impl FolderManager {
|
||||
Ok(folder)
|
||||
}
|
||||
|
||||
fn handle_index_folder(&self, workspace_id: Uuid, folder: &Folder) {
|
||||
async fn handle_index_folder(&self, workspace_id: Uuid, folder: &Folder) {
|
||||
let mut index_all = true;
|
||||
|
||||
let encoded_collab = self
|
||||
@ -191,12 +194,11 @@ impl FolderManager {
|
||||
if index_all {
|
||||
let views = folder.get_all_views();
|
||||
let folder_indexer = self.folder_indexer.clone();
|
||||
let _ = folder_indexer
|
||||
.remove_indices_for_workspace(workspace_id)
|
||||
.await;
|
||||
// We spawn a blocking task to index all views in the folder
|
||||
spawn_blocking(move || {
|
||||
// We remove old indexes just in case
|
||||
let _ = folder_indexer.remove_indices_for_workspace(workspace_id);
|
||||
|
||||
// We index all views from the workspace
|
||||
folder_indexer.index_all_views(views, workspace_id);
|
||||
});
|
||||
}
|
||||
|
@ -1,4 +1,6 @@
|
||||
use client_api::entity::search_dto::SearchDocumentResponseItem;
|
||||
pub use client_api::entity::search_dto::{
|
||||
SearchDocumentResponseItem, SearchResult, SearchSummaryResult,
|
||||
};
|
||||
use flowy_error::FlowyError;
|
||||
use lib_infra::async_trait::async_trait;
|
||||
use uuid::Uuid;
|
||||
@ -10,4 +12,11 @@ pub trait SearchCloudService: Send + Sync + 'static {
|
||||
workspace_id: &Uuid,
|
||||
query: String,
|
||||
) -> Result<Vec<SearchDocumentResponseItem>, FlowyError>;
|
||||
|
||||
async fn generate_search_summary(
|
||||
&self,
|
||||
workspace_id: &Uuid,
|
||||
query: String,
|
||||
search_results: Vec<SearchResult>,
|
||||
) -> Result<SearchSummaryResult, FlowyError>;
|
||||
}
|
||||
|
@ -1,9 +1,9 @@
|
||||
use std::any::Any;
|
||||
use std::sync::Arc;
|
||||
|
||||
use collab::core::collab::IndexContentReceiver;
|
||||
use collab_folder::{folder_diff::FolderViewChange, View, ViewIcon, ViewLayout};
|
||||
use flowy_error::FlowyError;
|
||||
use lib_infra::async_trait::async_trait;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub struct IndexableData {
|
||||
@ -26,19 +26,22 @@ impl IndexableData {
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait IndexManager: Send + Sync {
|
||||
fn set_index_content_receiver(&self, rx: IndexContentReceiver, workspace_id: Uuid);
|
||||
fn add_index(&self, data: IndexableData) -> Result<(), FlowyError>;
|
||||
fn update_index(&self, data: IndexableData) -> Result<(), FlowyError>;
|
||||
fn remove_indices(&self, ids: Vec<String>) -> Result<(), FlowyError>;
|
||||
fn remove_indices_for_workspace(&self, workspace_id: Uuid) -> Result<(), FlowyError>;
|
||||
fn is_indexed(&self) -> bool;
|
||||
|
||||
fn as_any(&self) -> &dyn Any;
|
||||
async fn set_index_content_receiver(&self, rx: IndexContentReceiver, workspace_id: Uuid);
|
||||
async fn add_index(&self, data: IndexableData) -> Result<(), FlowyError>;
|
||||
async fn update_index(&self, data: IndexableData) -> Result<(), FlowyError>;
|
||||
async fn remove_indices(&self, ids: Vec<String>) -> Result<(), FlowyError>;
|
||||
async fn remove_indices_for_workspace(&self, workspace_id: Uuid) -> Result<(), FlowyError>;
|
||||
async fn is_indexed(&self) -> bool;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait FolderIndexManager: IndexManager {
|
||||
async fn initialize(&self);
|
||||
|
||||
fn index_all_views(&self, views: Vec<Arc<View>>, workspace_id: Uuid);
|
||||
|
||||
fn index_view_changes(
|
||||
&self,
|
||||
views: Vec<Arc<View>>,
|
||||
|
@ -17,13 +17,10 @@ flowy-error = { workspace = true, features = [
|
||||
"impl_from_tantivy",
|
||||
"impl_from_serde",
|
||||
] }
|
||||
flowy-notification.workspace = true
|
||||
flowy-user.workspace = true
|
||||
flowy-search-pub.workspace = true
|
||||
flowy-folder = { workspace = true }
|
||||
|
||||
bytes.workspace = true
|
||||
futures.workspace = true
|
||||
lib-dispatch.workspace = true
|
||||
lib-infra = { workspace = true }
|
||||
protobuf.workspace = true
|
||||
@ -31,18 +28,18 @@ serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
tokio = { workspace = true, features = ["full", "rt-multi-thread", "tracing"] }
|
||||
tracing.workspace = true
|
||||
|
||||
async-stream = "0.3.4"
|
||||
derive_builder.workspace = true
|
||||
strsim = "0.11.0"
|
||||
strum_macros = "0.26.1"
|
||||
tantivy = { version = "0.22.0" }
|
||||
tantivy.workspace = true
|
||||
uuid.workspace = true
|
||||
allo-isolate = { version = "^0.1", features = ["catch-unwind"] }
|
||||
futures.workspace = true
|
||||
tokio-stream.workspace = true
|
||||
async-stream = "0.3.6"
|
||||
|
||||
[build-dependencies]
|
||||
flowy-codegen.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.10.0"
|
||||
|
||||
[features]
|
||||
dart = ["flowy-codegen/dart"]
|
||||
|
@ -1,16 +1,22 @@
|
||||
use flowy_error::FlowyResult;
|
||||
use flowy_folder::{manager::FolderManager, ViewLayout};
|
||||
use flowy_search_pub::cloud::SearchCloudService;
|
||||
use lib_infra::async_trait::async_trait;
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
use tracing::{trace, warn};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::entities::{
|
||||
CreateSearchResultPBArgs, RepeatedSearchResponseItemPB, RepeatedSearchSummaryPB, SearchResultPB,
|
||||
SearchSourcePB, SearchSummaryPB,
|
||||
};
|
||||
use crate::{
|
||||
entities::{IndexTypePB, ResultIconPB, ResultIconTypePB, SearchFilterPB, SearchResultPB},
|
||||
entities::{IndexTypePB, ResultIconPB, ResultIconTypePB, SearchFilterPB, SearchResponseItemPB},
|
||||
services::manager::{SearchHandler, SearchType},
|
||||
};
|
||||
use async_stream::stream;
|
||||
use flowy_error::FlowyResult;
|
||||
use flowy_folder::{manager::FolderManager, ViewLayout};
|
||||
use flowy_search_pub::cloud::{SearchCloudService, SearchResult};
|
||||
use lib_infra::async_trait::async_trait;
|
||||
use std::pin::Pin;
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
use tokio_stream::{self, Stream};
|
||||
use tracing::{trace, warn};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub struct DocumentSearchHandler {
|
||||
pub cloud_service: Arc<dyn SearchCloudService>,
|
||||
@ -39,65 +45,122 @@ impl SearchHandler for DocumentSearchHandler {
|
||||
&self,
|
||||
query: String,
|
||||
filter: Option<SearchFilterPB>,
|
||||
) -> FlowyResult<Vec<SearchResultPB>> {
|
||||
let filter = match filter {
|
||||
Some(filter) => filter,
|
||||
None => return Ok(vec![]),
|
||||
};
|
||||
) -> Pin<Box<dyn Stream<Item = FlowyResult<SearchResultPB>> + Send + 'static>> {
|
||||
let cloud_service = self.cloud_service.clone();
|
||||
let folder_manager = self.folder_manager.clone();
|
||||
|
||||
let workspace_id = match filter.workspace_id {
|
||||
Some(workspace_id) => workspace_id,
|
||||
None => return Ok(vec![]),
|
||||
};
|
||||
Box::pin(stream! {
|
||||
let filter = match filter {
|
||||
Some(f) => f,
|
||||
None => {
|
||||
yield Ok(CreateSearchResultPBArgs::default().build().unwrap());
|
||||
return;
|
||||
},
|
||||
};
|
||||
|
||||
let workspace_id = Uuid::from_str(&workspace_id)?;
|
||||
let results = self
|
||||
.cloud_service
|
||||
.document_search(&workspace_id, query)
|
||||
.await?;
|
||||
trace!("[Search] remote search results: {:?}", results);
|
||||
let workspace_id = match Uuid::from_str(&filter.workspace_id) {
|
||||
Ok(id) => id,
|
||||
Err(e) => {
|
||||
yield Err(e.into());
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Grab all views from folder cache
|
||||
// Notice that `get_all_view_pb` returns Views that don't include trashed and private views
|
||||
let views = self.folder_manager.get_all_views_pb().await?;
|
||||
let mut search_results: Vec<SearchResultPB> = vec![];
|
||||
let views = match folder_manager.get_all_views_pb().await {
|
||||
Ok(views) => views,
|
||||
Err(e) => {
|
||||
yield Err(e);
|
||||
return;
|
||||
},
|
||||
};
|
||||
|
||||
for result in results {
|
||||
if let Some(view) = views.iter().find(|v| v.id == result.object_id.to_string()) {
|
||||
// If there is no View for the result, we don't add it to the results
|
||||
// If possible we will extract the icon to display for the result
|
||||
let icon: Option<ResultIconPB> = match view.icon.clone() {
|
||||
Some(view_icon) => Some(ResultIconPB::from(view_icon)),
|
||||
None => {
|
||||
let view_layout_ty: i64 = ViewLayout::from(view.layout.clone()).into();
|
||||
Some(ResultIconPB {
|
||||
ty: ResultIconTypePB::Icon,
|
||||
value: view_layout_ty.to_string(),
|
||||
})
|
||||
},
|
||||
};
|
||||
let result_items = match cloud_service.document_search(&workspace_id, query.clone()).await {
|
||||
Ok(items) => items,
|
||||
Err(e) => {
|
||||
yield Err(e);
|
||||
return;
|
||||
},
|
||||
};
|
||||
|
||||
search_results.push(SearchResultPB {
|
||||
index_type: IndexTypePB::Document,
|
||||
view_id: result.object_id.to_string(),
|
||||
id: result.object_id.to_string(),
|
||||
data: view.name.clone(),
|
||||
icon,
|
||||
score: result.score,
|
||||
workspace_id: result.workspace_id.to_string(),
|
||||
preview: result.preview,
|
||||
});
|
||||
} else {
|
||||
warn!("No view found for search result: {:?}", result);
|
||||
trace!("[Search] search results: {:?}", result_items);
|
||||
let summary_input = result_items
|
||||
.iter()
|
||||
.map(|v| SearchResult {
|
||||
object_id: v.object_id,
|
||||
content: v.content.clone(),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut items: Vec<SearchResponseItemPB> = Vec::new();
|
||||
for item in result_items.iter() {
|
||||
if let Some(view) = views.iter().find(|v| v.id == item.object_id.to_string()) {
|
||||
let icon: Option<ResultIconPB> = match view.icon.clone() {
|
||||
Some(view_icon) => Some(ResultIconPB::from(view_icon)),
|
||||
None => {
|
||||
let view_layout_ty: i64 = ViewLayout::from(view.layout.clone()).into();
|
||||
Some(ResultIconPB {
|
||||
ty: ResultIconTypePB::Icon,
|
||||
value: view_layout_ty.to_string(),
|
||||
})
|
||||
},
|
||||
};
|
||||
|
||||
items.push(SearchResponseItemPB {
|
||||
index_type: IndexTypePB::Document,
|
||||
view_id: item.object_id.to_string(),
|
||||
id: item.object_id.to_string(),
|
||||
data: view.name.clone(),
|
||||
icon,
|
||||
score: item.score,
|
||||
workspace_id: item.workspace_id.to_string(),
|
||||
preview: item.preview.clone(),
|
||||
});
|
||||
} else {
|
||||
warn!("No view found for search result: {:?}", item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
trace!("[Search] showing results: {:?}", search_results);
|
||||
Ok(search_results)
|
||||
}
|
||||
let search_result = RepeatedSearchResponseItemPB {
|
||||
items,
|
||||
};
|
||||
yield Ok(
|
||||
CreateSearchResultPBArgs::default()
|
||||
.search_result(Some(search_result))
|
||||
.build()
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
/// Ignore for [DocumentSearchHandler]
|
||||
fn index_count(&self) -> u64 {
|
||||
0
|
||||
// Search summary generation.
|
||||
match cloud_service.generate_search_summary(&workspace_id, query.clone(), summary_input).await {
|
||||
Ok(summary_result) => {
|
||||
trace!("[Search] search summary: {:?}", summary_result);
|
||||
let summaries: Vec<SearchSummaryPB> = summary_result
|
||||
.summaries
|
||||
.into_iter()
|
||||
.filter_map(|v| {
|
||||
v.metadata.as_object().and_then(|object| {
|
||||
let id = object.get("id")?.as_str()?.to_string();
|
||||
let source = object.get("source")?.as_str()?.to_string();
|
||||
let metadata = SearchSourcePB {id, source };
|
||||
Some(SearchSummaryPB { content: v.content, metadata: Some(metadata) })
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
let summary_result = RepeatedSearchSummaryPB {
|
||||
items: summaries,
|
||||
};
|
||||
yield Ok(
|
||||
CreateSearchResultPBArgs::default()
|
||||
.search_summary(Some(summary_result))
|
||||
.build()
|
||||
.unwrap(),
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Failed to generate search summary: {:?}", e);
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -1,20 +1,16 @@
|
||||
use super::SearchResultPB;
|
||||
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
|
||||
|
||||
use super::SearchResultPB;
|
||||
|
||||
#[derive(ProtoBuf, Default, Debug, Clone)]
|
||||
pub struct SearchResultNotificationPB {
|
||||
#[pb(index = 1)]
|
||||
pub items: Vec<SearchResultPB>,
|
||||
pub struct SearchResponsePB {
|
||||
#[pb(index = 1, one_of)]
|
||||
pub result: Option<SearchResultPB>,
|
||||
|
||||
#[pb(index = 2)]
|
||||
pub sends: u64,
|
||||
pub search_id: String,
|
||||
|
||||
#[pb(index = 3, one_of)]
|
||||
pub channel: Option<String>,
|
||||
|
||||
#[pb(index = 4)]
|
||||
pub query: String,
|
||||
#[pb(index = 3)]
|
||||
pub is_loading: bool,
|
||||
}
|
||||
|
||||
#[derive(ProtoBuf_Enum, Debug, Default)]
|
||||
|
@ -13,13 +13,9 @@ pub struct SearchQueryPB {
|
||||
#[pb(index = 3, one_of)]
|
||||
pub filter: Option<SearchFilterPB>,
|
||||
|
||||
/// Used to identify the channel of the search
|
||||
///
|
||||
/// This can be used to have multiple search notification listeners in place.
|
||||
/// It is up to the client to decide how to handle this.
|
||||
///
|
||||
/// If not set, then no channel is used.
|
||||
///
|
||||
#[pb(index = 4, one_of)]
|
||||
pub channel: Option<String>,
|
||||
#[pb(index = 4)]
|
||||
pub search_id: String,
|
||||
|
||||
#[pb(index = 5)]
|
||||
pub stream_port: i64,
|
||||
}
|
||||
|
@ -1,17 +1,54 @@
|
||||
use super::IndexTypePB;
|
||||
use collab_folder::{IconType, ViewIcon};
|
||||
use derive_builder::Builder;
|
||||
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
|
||||
use flowy_folder::entities::ViewIconPB;
|
||||
|
||||
use super::IndexTypePB;
|
||||
#[derive(Debug, Default, ProtoBuf, Builder, Clone)]
|
||||
#[builder(name = "CreateSearchResultPBArgs")]
|
||||
#[builder(pattern = "mutable")]
|
||||
pub struct SearchResultPB {
|
||||
#[pb(index = 1, one_of)]
|
||||
#[builder(default)]
|
||||
pub search_result: Option<RepeatedSearchResponseItemPB>,
|
||||
|
||||
#[derive(Debug, Default, ProtoBuf, Clone)]
|
||||
pub struct RepeatedSearchResultPB {
|
||||
#[pb(index = 1)]
|
||||
pub items: Vec<SearchResultPB>,
|
||||
#[pb(index = 2, one_of)]
|
||||
#[builder(default)]
|
||||
pub search_summary: Option<RepeatedSearchSummaryPB>,
|
||||
}
|
||||
|
||||
#[derive(ProtoBuf, Default, Debug, Clone)]
|
||||
pub struct SearchResultPB {
|
||||
pub struct RepeatedSearchSummaryPB {
|
||||
#[pb(index = 1)]
|
||||
pub items: Vec<SearchSummaryPB>,
|
||||
}
|
||||
|
||||
#[derive(ProtoBuf, Default, Debug, Clone)]
|
||||
pub struct SearchSummaryPB {
|
||||
#[pb(index = 1)]
|
||||
pub content: String,
|
||||
|
||||
#[pb(index = 2, one_of)]
|
||||
pub metadata: Option<SearchSourcePB>,
|
||||
}
|
||||
|
||||
#[derive(ProtoBuf, Default, Debug, Clone)]
|
||||
pub struct SearchSourcePB {
|
||||
#[pb(index = 1)]
|
||||
pub id: String,
|
||||
|
||||
#[pb(index = 2)]
|
||||
pub source: String,
|
||||
}
|
||||
|
||||
#[derive(ProtoBuf, Default, Debug, Clone)]
|
||||
pub struct RepeatedSearchResponseItemPB {
|
||||
#[pb(index = 1)]
|
||||
pub items: Vec<SearchResponseItemPB>,
|
||||
}
|
||||
|
||||
#[derive(ProtoBuf, Default, Debug, Clone)]
|
||||
pub struct SearchResponseItemPB {
|
||||
#[pb(index = 1)]
|
||||
pub index_type: IndexTypePB,
|
||||
|
||||
@ -37,9 +74,9 @@ pub struct SearchResultPB {
|
||||
pub preview: Option<String>,
|
||||
}
|
||||
|
||||
impl SearchResultPB {
|
||||
impl SearchResponseItemPB {
|
||||
pub fn with_score(&self, score: f64) -> Self {
|
||||
SearchResultPB {
|
||||
SearchResponseItemPB {
|
||||
index_type: self.index_type.clone(),
|
||||
view_id: self.view_id.clone(),
|
||||
id: self.id.clone(),
|
||||
|
@ -2,6 +2,6 @@ use flowy_derive::ProtoBuf;
|
||||
|
||||
#[derive(Eq, PartialEq, ProtoBuf, Default, Debug, Clone)]
|
||||
pub struct SearchFilterPB {
|
||||
#[pb(index = 1, one_of)]
|
||||
pub workspace_id: Option<String>,
|
||||
#[pb(index = 1)]
|
||||
pub workspace_id: String,
|
||||
}
|
||||
|
@ -21,7 +21,14 @@ pub(crate) async fn search_handler(
|
||||
) -> Result<(), FlowyError> {
|
||||
let query = data.into_inner();
|
||||
let manager = upgrade_manager(manager)?;
|
||||
manager.perform_search(query.search, query.filter, query.channel);
|
||||
manager
|
||||
.perform_search(
|
||||
query.search,
|
||||
query.stream_port,
|
||||
query.filter,
|
||||
query.search_id,
|
||||
)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::entities::{IndexTypePB, ResultIconPB, SearchResultPB};
|
||||
use crate::entities::{IndexTypePB, ResultIconPB, SearchResponseItemPB};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct FolderIndexData {
|
||||
@ -11,7 +11,7 @@ pub struct FolderIndexData {
|
||||
pub workspace_id: String,
|
||||
}
|
||||
|
||||
impl From<FolderIndexData> for SearchResultPB {
|
||||
impl From<FolderIndexData> for SearchResponseItemPB {
|
||||
fn from(data: FolderIndexData) -> Self {
|
||||
let icon = if data.icon.is_empty() {
|
||||
None
|
||||
|
@ -1,12 +1,14 @@
|
||||
use crate::{
|
||||
entities::{SearchFilterPB, SearchResultPB},
|
||||
services::manager::{SearchHandler, SearchType},
|
||||
use super::indexer::FolderIndexManagerImpl;
|
||||
use crate::entities::{
|
||||
CreateSearchResultPBArgs, RepeatedSearchResponseItemPB, SearchFilterPB, SearchResultPB,
|
||||
};
|
||||
use crate::services::manager::{SearchHandler, SearchType};
|
||||
use async_stream::stream;
|
||||
use flowy_error::FlowyResult;
|
||||
use lib_infra::async_trait::async_trait;
|
||||
use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::indexer::FolderIndexManagerImpl;
|
||||
use tokio_stream::{self, Stream};
|
||||
|
||||
pub struct FolderSearchHandler {
|
||||
pub index_manager: Arc<FolderIndexManagerImpl>,
|
||||
@ -28,19 +30,26 @@ impl SearchHandler for FolderSearchHandler {
|
||||
&self,
|
||||
query: String,
|
||||
filter: Option<SearchFilterPB>,
|
||||
) -> FlowyResult<Vec<SearchResultPB>> {
|
||||
let mut results = self.index_manager.search(query, filter.clone())?;
|
||||
if let Some(filter) = filter {
|
||||
if let Some(workspace_id) = filter.workspace_id {
|
||||
// Filter results by workspace ID
|
||||
results.retain(|result| result.workspace_id == workspace_id);
|
||||
}
|
||||
}
|
||||
) -> Pin<Box<dyn Stream<Item = FlowyResult<SearchResultPB>> + Send + 'static>> {
|
||||
let index_manager = self.index_manager.clone();
|
||||
|
||||
Ok(results)
|
||||
}
|
||||
Box::pin(stream! {
|
||||
// Perform search (if search() returns a Result)
|
||||
let mut items = match index_manager.search(query).await {
|
||||
Ok(items) => items,
|
||||
Err(err) => {
|
||||
yield Err(err);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
fn index_count(&self) -> u64 {
|
||||
self.index_manager.num_docs()
|
||||
if let Some(filter) = filter {
|
||||
items.retain(|result| result.workspace_id == filter.workspace_id);
|
||||
}
|
||||
|
||||
// Build the search result.
|
||||
let search_result = RepeatedSearchResponseItemPB {items};
|
||||
yield Ok(CreateSearchResultPBArgs::default().search_result(Some(search_result)).build().unwrap())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -1,14 +1,5 @@
|
||||
use std::{
|
||||
any::Any,
|
||||
collections::HashMap,
|
||||
fs,
|
||||
ops::Deref,
|
||||
path::Path,
|
||||
sync::{Arc, Mutex, MutexGuard, Weak},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
entities::{ResultIconTypePB, SearchFilterPB, SearchResultPB},
|
||||
entities::SearchResponseItemPB,
|
||||
folder::schema::{
|
||||
FolderSchema, FOLDER_ICON_FIELD_NAME, FOLDER_ICON_TY_FIELD_NAME, FOLDER_ID_FIELD_NAME,
|
||||
FOLDER_TITLE_FIELD_NAME, FOLDER_WORKSPACE_ID_FIELD_NAME,
|
||||
@ -19,172 +10,88 @@ use collab_folder::{folder_diff::FolderViewChange, View, ViewIcon, ViewIndexCont
|
||||
use flowy_error::{FlowyError, FlowyResult};
|
||||
use flowy_search_pub::entities::{FolderIndexManager, IndexManager, IndexableData};
|
||||
use flowy_user::services::authenticate_user::AuthenticateUser;
|
||||
use std::sync::{Arc, Weak};
|
||||
use std::{collections::HashMap, fs};
|
||||
|
||||
use super::entities::FolderIndexData;
|
||||
use crate::entities::ResultIconTypePB;
|
||||
use lib_infra::async_trait::async_trait;
|
||||
use strsim::levenshtein;
|
||||
use tantivy::{
|
||||
collector::TopDocs, directory::MmapDirectory, doc, query::QueryParser, schema::Field, Document,
|
||||
Index, IndexReader, IndexWriter, TantivyDocument, Term,
|
||||
};
|
||||
use tokio::sync::RwLock;
|
||||
use tracing::{error, info};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct FolderIndexManagerImpl {
|
||||
folder_schema: Option<FolderSchema>,
|
||||
index: Option<Index>,
|
||||
index_reader: Option<IndexReader>,
|
||||
index_writer: Option<Arc<Mutex<IndexWriter>>>,
|
||||
pub struct TantivyState {
|
||||
pub index: Index,
|
||||
pub folder_schema: FolderSchema,
|
||||
pub index_reader: IndexReader,
|
||||
pub index_writer: IndexWriter,
|
||||
}
|
||||
|
||||
const FOLDER_INDEX_DIR: &str = "folder_index";
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct FolderIndexManagerImpl {
|
||||
auth_user: Weak<AuthenticateUser>,
|
||||
state: Arc<RwLock<Option<TantivyState>>>,
|
||||
}
|
||||
|
||||
impl FolderIndexManagerImpl {
|
||||
pub fn new(auth_user: Option<Weak<AuthenticateUser>>) -> Self {
|
||||
let auth_user = match auth_user {
|
||||
Some(auth_user) => auth_user,
|
||||
None => {
|
||||
return FolderIndexManagerImpl::empty();
|
||||
},
|
||||
};
|
||||
|
||||
// AuthenticateUser is required to get the index path
|
||||
let authenticate_user = auth_user.upgrade();
|
||||
|
||||
// Storage path is the users data path with an index directory
|
||||
// Eg. /usr/flowy-data/indexes
|
||||
let storage_path = match authenticate_user {
|
||||
Some(auth_user) => auth_user.get_index_path(),
|
||||
None => {
|
||||
tracing::error!("FolderIndexManager: AuthenticateUser is not available");
|
||||
return FolderIndexManagerImpl::empty();
|
||||
},
|
||||
};
|
||||
|
||||
// We check if the `folder_index` directory exists, if not we create it
|
||||
let index_path = storage_path.join(Path::new(FOLDER_INDEX_DIR));
|
||||
if !index_path.exists() {
|
||||
let res = fs::create_dir_all(&index_path);
|
||||
if let Err(e) = res {
|
||||
tracing::error!(
|
||||
"FolderIndexManager failed to create index directory: {:?}",
|
||||
e
|
||||
);
|
||||
return FolderIndexManagerImpl::empty();
|
||||
}
|
||||
}
|
||||
|
||||
// The folder schema is used to define the fields of the index along
|
||||
// with how they are stored and if the field is indexed
|
||||
let folder_schema = FolderSchema::new();
|
||||
|
||||
// We open the existing or newly created folder_index directory
|
||||
// This is required by the Tantivy Index, as it will use it to store
|
||||
// and read index data
|
||||
let index = match MmapDirectory::open(index_path) {
|
||||
// We open or create an index that takes the directory r/w and the schema.
|
||||
Ok(dir) => match Index::open_or_create(dir, folder_schema.schema.clone()) {
|
||||
Ok(index) => index,
|
||||
Err(e) => {
|
||||
tracing::error!("FolderIndexManager failed to open index: {:?}", e);
|
||||
return FolderIndexManagerImpl::empty();
|
||||
},
|
||||
},
|
||||
Err(e) => {
|
||||
tracing::error!("FolderIndexManager failed to open index directory: {:?}", e);
|
||||
return FolderIndexManagerImpl::empty();
|
||||
},
|
||||
};
|
||||
|
||||
// We only need one IndexReader per index
|
||||
let index_reader = index.reader();
|
||||
let index_writer = index.writer(50_000_000);
|
||||
|
||||
let (index_reader, index_writer) = match (index_reader, index_writer) {
|
||||
(Ok(reader), Ok(writer)) => (reader, writer),
|
||||
_ => {
|
||||
tracing::error!("FolderIndexManager failed to instantiate index writer and/or reader");
|
||||
return FolderIndexManagerImpl::empty();
|
||||
},
|
||||
};
|
||||
|
||||
pub fn new(auth_user: Weak<AuthenticateUser>) -> Self {
|
||||
Self {
|
||||
folder_schema: Some(folder_schema),
|
||||
index: Some(index),
|
||||
index_reader: Some(index_reader),
|
||||
index_writer: Some(Arc::new(Mutex::new(index_writer))),
|
||||
auth_user,
|
||||
state: Arc::new(RwLock::new(None)),
|
||||
}
|
||||
}
|
||||
|
||||
fn index_all(&self, indexes: Vec<IndexableData>) -> Result<(), FlowyError> {
|
||||
if indexes.is_empty() {
|
||||
return Ok(());
|
||||
async fn with_writer<F, R>(&self, f: F) -> FlowyResult<R>
|
||||
where
|
||||
F: FnOnce(&mut IndexWriter, &FolderSchema) -> FlowyResult<R>,
|
||||
{
|
||||
let mut lock = self.state.write().await;
|
||||
if let Some(ref mut state) = *lock {
|
||||
f(&mut state.index_writer, &state.folder_schema)
|
||||
} else {
|
||||
Err(FlowyError::internal().with_context("Index not initialized. Call initialize first"))
|
||||
}
|
||||
}
|
||||
|
||||
/// Initializes the state using the workspace directory.
|
||||
async fn initialize_with_workspace(&self) -> FlowyResult<()> {
|
||||
let auth_user = self
|
||||
.auth_user
|
||||
.upgrade()
|
||||
.ok_or_else(|| FlowyError::internal().with_context("AuthenticateUser is not available"))?;
|
||||
|
||||
let index_path = auth_user.get_index_path()?.join(FOLDER_INDEX_DIR);
|
||||
if !index_path.exists() {
|
||||
fs::create_dir_all(&index_path).map_err(|e| {
|
||||
error!("Failed to create folder index directory: {:?}", e);
|
||||
FlowyError::internal().with_context("Failed to create folder index")
|
||||
})?;
|
||||
}
|
||||
|
||||
let mut index_writer = self.get_index_writer()?;
|
||||
let folder_schema = self.get_folder_schema()?;
|
||||
info!("Folder indexer initialized at: {:?}", index_path);
|
||||
let folder_schema = FolderSchema::new();
|
||||
let dir = MmapDirectory::open(index_path)?;
|
||||
let index = Index::open_or_create(dir, folder_schema.schema.clone())?;
|
||||
let index_reader = index.reader()?;
|
||||
let index_writer = index.writer(50_000_000)?;
|
||||
|
||||
let id_field = folder_schema.schema.get_field(FOLDER_ID_FIELD_NAME)?;
|
||||
let title_field = folder_schema.schema.get_field(FOLDER_TITLE_FIELD_NAME)?;
|
||||
let icon_field = folder_schema.schema.get_field(FOLDER_ICON_FIELD_NAME)?;
|
||||
let icon_ty_field = folder_schema.schema.get_field(FOLDER_ICON_TY_FIELD_NAME)?;
|
||||
let workspace_id_field = folder_schema
|
||||
.schema
|
||||
.get_field(FOLDER_WORKSPACE_ID_FIELD_NAME)?;
|
||||
|
||||
for data in indexes {
|
||||
let (icon, icon_ty) = self.extract_icon(data.icon, data.layout);
|
||||
|
||||
let _ = index_writer.add_document(doc![
|
||||
id_field => data.id.clone(),
|
||||
title_field => data.data.clone(),
|
||||
icon_field => icon.unwrap_or_default(),
|
||||
icon_ty_field => icon_ty,
|
||||
workspace_id_field => data.workspace_id.to_string(),
|
||||
]);
|
||||
}
|
||||
|
||||
index_writer.commit()?;
|
||||
*self.state.write().await = Some(TantivyState {
|
||||
index,
|
||||
folder_schema,
|
||||
index_reader,
|
||||
index_writer,
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn num_docs(&self) -> u64 {
|
||||
self
|
||||
.index_reader
|
||||
.clone()
|
||||
.map(|reader| reader.searcher().num_docs())
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
fn empty() -> Self {
|
||||
Self {
|
||||
folder_schema: None,
|
||||
index: None,
|
||||
index_reader: None,
|
||||
index_writer: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_index_writer(&self) -> FlowyResult<MutexGuard<IndexWriter>> {
|
||||
match &self.index_writer {
|
||||
Some(index_writer) => match index_writer.deref().lock() {
|
||||
Ok(writer) => Ok(writer),
|
||||
Err(e) => {
|
||||
tracing::error!("FolderIndexManager failed to lock index writer: {:?}", e);
|
||||
Err(FlowyError::folder_index_manager_unavailable())
|
||||
},
|
||||
},
|
||||
None => Err(FlowyError::folder_index_manager_unavailable()),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_folder_schema(&self) -> FlowyResult<FolderSchema> {
|
||||
match &self.folder_schema {
|
||||
Some(folder_schema) => Ok(folder_schema.clone()),
|
||||
None => Err(FlowyError::folder_index_manager_unavailable()),
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_icon(
|
||||
&self,
|
||||
view_icon: Option<ViewIcon>,
|
||||
@ -200,100 +107,68 @@ impl FolderIndexManagerImpl {
|
||||
icon = Some(view_icon.value);
|
||||
} else {
|
||||
icon_ty = ResultIconTypePB::Icon.into();
|
||||
let layout_ty: i64 = view_layout.into();
|
||||
let layout_ty = view_layout as i64;
|
||||
icon = Some(layout_ty.to_string());
|
||||
}
|
||||
|
||||
(icon, icon_ty)
|
||||
}
|
||||
|
||||
pub fn search(
|
||||
&self,
|
||||
query: String,
|
||||
_filter: Option<SearchFilterPB>,
|
||||
) -> Result<Vec<SearchResultPB>, FlowyError> {
|
||||
let folder_schema = self.get_folder_schema()?;
|
||||
|
||||
let (index, index_reader) = self
|
||||
.index
|
||||
.as_ref()
|
||||
.zip(self.index_reader.as_ref())
|
||||
.ok_or_else(FlowyError::folder_index_manager_unavailable)?;
|
||||
|
||||
let title_field = folder_schema.schema.get_field(FOLDER_TITLE_FIELD_NAME)?;
|
||||
|
||||
let length = query.len();
|
||||
let distance: u8 = if length >= 2 { 2 } else { 1 };
|
||||
|
||||
let mut query_parser = QueryParser::for_index(&index.clone(), vec![title_field]);
|
||||
query_parser.set_field_fuzzy(title_field, true, distance, true);
|
||||
let built_query = query_parser.parse_query(&query.clone())?;
|
||||
|
||||
let searcher = index_reader.searcher();
|
||||
let mut search_results: Vec<SearchResultPB> = vec![];
|
||||
let top_docs = searcher.search(&built_query, &TopDocs::with_limit(10))?;
|
||||
for (_score, doc_address) in top_docs {
|
||||
let retrieved_doc: TantivyDocument = searcher.doc(doc_address)?;
|
||||
|
||||
let mut content = HashMap::new();
|
||||
let named_doc = retrieved_doc.to_named_doc(&folder_schema.schema);
|
||||
for (k, v) in named_doc.0 {
|
||||
content.insert(k, v[0].clone());
|
||||
}
|
||||
|
||||
if content.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let s = serde_json::to_string(&content)?;
|
||||
let result: SearchResultPB = serde_json::from_str::<FolderIndexData>(&s)?.into();
|
||||
let score = self.score_result(&query, &result.data);
|
||||
search_results.push(result.with_score(score));
|
||||
}
|
||||
|
||||
Ok(search_results)
|
||||
}
|
||||
|
||||
// Score result by distance
|
||||
fn score_result(&self, query: &str, term: &str) -> f64 {
|
||||
let distance = levenshtein(query, term) as f64;
|
||||
1.0 / (distance + 1.0)
|
||||
}
|
||||
|
||||
fn get_schema_fields(&self) -> Result<(Field, Field, Field, Field, Field), FlowyError> {
|
||||
let folder_schema = match self.folder_schema.clone() {
|
||||
Some(schema) => schema,
|
||||
_ => return Err(FlowyError::folder_index_manager_unavailable()),
|
||||
};
|
||||
/// Simple implementation to index all given data by spawning async tasks.
|
||||
fn index_all(&self, data_vec: Vec<IndexableData>) -> Result<(), FlowyError> {
|
||||
for data in data_vec {
|
||||
let indexer = self.clone();
|
||||
tokio::spawn(async move {
|
||||
let _ = indexer.add_index(data).await;
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
let id_field = folder_schema.schema.get_field(FOLDER_ID_FIELD_NAME)?;
|
||||
let title_field = folder_schema.schema.get_field(FOLDER_TITLE_FIELD_NAME)?;
|
||||
let icon_field = folder_schema.schema.get_field(FOLDER_ICON_FIELD_NAME)?;
|
||||
let icon_ty_field = folder_schema.schema.get_field(FOLDER_ICON_TY_FIELD_NAME)?;
|
||||
let workspace_id_field = folder_schema
|
||||
.schema
|
||||
.get_field(FOLDER_WORKSPACE_ID_FIELD_NAME)?;
|
||||
/// Searches the index using the given query string.
|
||||
pub async fn search(&self, query: String) -> Result<Vec<SearchResponseItemPB>, FlowyError> {
|
||||
let lock = self.state.read().await;
|
||||
let state = lock
|
||||
.as_ref()
|
||||
.ok_or_else(FlowyError::folder_index_manager_unavailable)?;
|
||||
let schema = &state.folder_schema;
|
||||
let index = &state.index;
|
||||
let reader = &state.index_reader;
|
||||
|
||||
Ok((
|
||||
id_field,
|
||||
title_field,
|
||||
icon_field,
|
||||
icon_ty_field,
|
||||
workspace_id_field,
|
||||
))
|
||||
let title_field = schema.schema.get_field(FOLDER_TITLE_FIELD_NAME)?;
|
||||
let mut parser = QueryParser::for_index(index, vec![title_field]);
|
||||
parser.set_field_fuzzy(title_field, true, 2, true);
|
||||
|
||||
let built_query = parser.parse_query(&query)?;
|
||||
let searcher = reader.searcher();
|
||||
let top_docs = searcher.search(&built_query, &TopDocs::with_limit(10))?;
|
||||
|
||||
let mut results = Vec::new();
|
||||
for (_score, doc_address) in top_docs {
|
||||
let doc: TantivyDocument = searcher.doc(doc_address)?;
|
||||
let named_doc = doc.to_named_doc(&schema.schema);
|
||||
let mut content = HashMap::new();
|
||||
for (k, v) in named_doc.0 {
|
||||
content.insert(k, v[0].clone());
|
||||
}
|
||||
if !content.is_empty() {
|
||||
let s = serde_json::to_string(&content)?;
|
||||
let result: SearchResponseItemPB = serde_json::from_str::<FolderIndexData>(&s)?.into();
|
||||
results.push(result.with_score(self.score_result(&query, &result.data) as f64));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(results)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl IndexManager for FolderIndexManagerImpl {
|
||||
fn is_indexed(&self) -> bool {
|
||||
self
|
||||
.index_reader
|
||||
.clone()
|
||||
.map(|reader| reader.searcher().num_docs() > 0)
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn set_index_content_receiver(&self, mut rx: IndexContentReceiver, workspace_id: Uuid) {
|
||||
async fn set_index_content_receiver(&self, mut rx: IndexContentReceiver, workspace_id: Uuid) {
|
||||
let indexer = self.clone();
|
||||
let wid = workspace_id;
|
||||
tokio::spawn(async move {
|
||||
@ -301,31 +176,35 @@ impl IndexManager for FolderIndexManagerImpl {
|
||||
match msg {
|
||||
IndexContent::Create(value) => match serde_json::from_value::<ViewIndexContent>(value) {
|
||||
Ok(view) => {
|
||||
let _ = indexer.add_index(IndexableData {
|
||||
id: view.id,
|
||||
data: view.name,
|
||||
icon: view.icon,
|
||||
layout: view.layout,
|
||||
workspace_id: wid,
|
||||
});
|
||||
let _ = indexer
|
||||
.add_index(IndexableData {
|
||||
id: view.id,
|
||||
data: view.name,
|
||||
icon: view.icon,
|
||||
layout: view.layout,
|
||||
workspace_id: wid,
|
||||
})
|
||||
.await;
|
||||
},
|
||||
Err(err) => tracing::error!("FolderIndexManager error deserialize: {:?}", err),
|
||||
Err(err) => tracing::error!("FolderIndexManager error deserialize (create): {:?}", err),
|
||||
},
|
||||
IndexContent::Update(value) => match serde_json::from_value::<ViewIndexContent>(value) {
|
||||
Ok(view) => {
|
||||
let _ = indexer.update_index(IndexableData {
|
||||
id: view.id,
|
||||
data: view.name,
|
||||
icon: view.icon,
|
||||
layout: view.layout,
|
||||
workspace_id: wid,
|
||||
});
|
||||
let _ = indexer
|
||||
.update_index(IndexableData {
|
||||
id: view.id,
|
||||
data: view.name,
|
||||
icon: view.icon,
|
||||
layout: view.layout,
|
||||
workspace_id: wid,
|
||||
})
|
||||
.await;
|
||||
},
|
||||
Err(err) => tracing::error!("FolderIndexManager error deserialize: {:?}", err),
|
||||
Err(err) => tracing::error!("FolderIndexManager error deserialize (update): {:?}", err),
|
||||
},
|
||||
IndexContent::Delete(ids) => {
|
||||
if let Err(e) = indexer.remove_indices(ids) {
|
||||
tracing::error!("FolderIndexManager error deserialize: {:?}", e);
|
||||
if let Err(e) = indexer.remove_indices(ids).await {
|
||||
tracing::error!("FolderIndexManager error (delete): {:?}", e);
|
||||
}
|
||||
},
|
||||
}
|
||||
@ -333,100 +212,108 @@ impl IndexManager for FolderIndexManagerImpl {
|
||||
});
|
||||
}
|
||||
|
||||
fn update_index(&self, data: IndexableData) -> Result<(), FlowyError> {
|
||||
let mut index_writer = self.get_index_writer()?;
|
||||
|
||||
let (id_field, title_field, icon_field, icon_ty_field, workspace_id_field) =
|
||||
self.get_schema_fields()?;
|
||||
|
||||
let delete_term = Term::from_field_text(id_field, &data.id.clone());
|
||||
|
||||
// Remove old index
|
||||
index_writer.delete_term(delete_term);
|
||||
|
||||
async fn add_index(&self, data: IndexableData) -> Result<(), FlowyError> {
|
||||
let (icon, icon_ty) = self.extract_icon(data.icon, data.layout);
|
||||
|
||||
// Add new index
|
||||
let _ = index_writer.add_document(doc![
|
||||
id_field => data.id.clone(),
|
||||
title_field => data.data,
|
||||
icon_field => icon.unwrap_or_default(),
|
||||
icon_ty_field => icon_ty,
|
||||
workspace_id_field => data.workspace_id.to_string(),
|
||||
]);
|
||||
|
||||
index_writer.commit()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn remove_indices(&self, ids: Vec<String>) -> Result<(), FlowyError> {
|
||||
let mut index_writer = self.get_index_writer()?;
|
||||
|
||||
let folder_schema = self.get_folder_schema()?;
|
||||
let id_field = folder_schema.schema.get_field(FOLDER_ID_FIELD_NAME)?;
|
||||
for id in ids {
|
||||
let delete_term = Term::from_field_text(id_field, &id);
|
||||
index_writer.delete_term(delete_term);
|
||||
}
|
||||
|
||||
index_writer.commit()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn add_index(&self, data: IndexableData) -> Result<(), FlowyError> {
|
||||
let mut index_writer = self.get_index_writer()?;
|
||||
|
||||
let (id_field, title_field, icon_field, icon_ty_field, workspace_id_field) =
|
||||
self.get_schema_fields()?;
|
||||
|
||||
let (icon, icon_ty) = self.extract_icon(data.icon, data.layout);
|
||||
|
||||
// Add new index
|
||||
let _ = index_writer.add_document(doc![
|
||||
id_field => data.id,
|
||||
title_field => data.data,
|
||||
icon_field => icon.unwrap_or_default(),
|
||||
icon_ty_field => icon_ty,
|
||||
workspace_id_field => data.workspace_id.to_string(),
|
||||
]);
|
||||
|
||||
index_writer.commit()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Removes all indexes that are related by workspace id. This is useful
|
||||
/// for cleaning indexes when eg. removing/leaving a workspace.
|
||||
///
|
||||
fn remove_indices_for_workspace(&self, workspace_id: Uuid) -> Result<(), FlowyError> {
|
||||
let mut index_writer = self.get_index_writer()?;
|
||||
|
||||
let folder_schema = self.get_folder_schema()?;
|
||||
let id_field = folder_schema
|
||||
.schema
|
||||
.get_field(FOLDER_WORKSPACE_ID_FIELD_NAME)?;
|
||||
let delete_term = Term::from_field_text(id_field, &workspace_id.to_string());
|
||||
index_writer.delete_term(delete_term);
|
||||
|
||||
index_writer.commit()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
.with_writer(|index_writer, folder_schema| {
|
||||
let (id_field, title_field, icon_field, icon_ty_field, workspace_id_field) =
|
||||
get_schema_fields(folder_schema)?;
|
||||
let _ = index_writer.add_document(doc![
|
||||
id_field => data.id,
|
||||
title_field => data.data,
|
||||
icon_field => icon.unwrap_or_default(),
|
||||
icon_ty_field => icon_ty,
|
||||
workspace_id_field => data.workspace_id.to_string(),
|
||||
]);
|
||||
index_writer.commit()?;
|
||||
Ok(())
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn update_index(&self, data: IndexableData) -> Result<(), FlowyError> {
|
||||
self
|
||||
.with_writer(|index_writer, folder_schema| {
|
||||
let (id_field, title_field, icon_field, icon_ty_field, workspace_id_field) =
|
||||
get_schema_fields(folder_schema)?;
|
||||
let delete_term = Term::from_field_text(id_field, &data.id);
|
||||
index_writer.delete_term(delete_term);
|
||||
|
||||
let (icon, icon_ty) = self.extract_icon(data.icon, data.layout);
|
||||
let _ = index_writer.add_document(doc![
|
||||
id_field => data.id,
|
||||
title_field => data.data,
|
||||
icon_field => icon.unwrap_or_default(),
|
||||
icon_ty_field => icon_ty,
|
||||
workspace_id_field => data.workspace_id.to_string(),
|
||||
]);
|
||||
|
||||
index_writer.commit()?;
|
||||
Ok(())
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn remove_indices(&self, ids: Vec<String>) -> Result<(), FlowyError> {
|
||||
self
|
||||
.with_writer(|index_writer, folder_schema| {
|
||||
let id_field = folder_schema.schema.get_field(FOLDER_ID_FIELD_NAME)?;
|
||||
for id in ids {
|
||||
let delete_term = Term::from_field_text(id_field, &id);
|
||||
index_writer.delete_term(delete_term);
|
||||
}
|
||||
|
||||
index_writer.commit()?;
|
||||
Ok(())
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn remove_indices_for_workspace(&self, workspace_id: Uuid) -> Result<(), FlowyError> {
|
||||
self
|
||||
.with_writer(|index_writer, folder_schema| {
|
||||
let id_field = folder_schema
|
||||
.schema
|
||||
.get_field(FOLDER_WORKSPACE_ID_FIELD_NAME)?;
|
||||
|
||||
let delete_term = Term::from_field_text(id_field, &workspace_id.to_string());
|
||||
index_writer.delete_term(delete_term);
|
||||
index_writer.commit()?;
|
||||
Ok(())
|
||||
})
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn is_indexed(&self) -> bool {
|
||||
let lock = self.state.read().await;
|
||||
if let Some(ref state) = *lock {
|
||||
state.index_reader.searcher().num_docs() > 0
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl FolderIndexManager for FolderIndexManagerImpl {
|
||||
async fn initialize(&self) {
|
||||
if let Err(e) = self.initialize_with_workspace().await {
|
||||
error!("Failed to initialize FolderIndexManager: {:?}", e);
|
||||
}
|
||||
}
|
||||
|
||||
fn index_all_views(&self, views: Vec<Arc<View>>, workspace_id: Uuid) {
|
||||
let indexable_data = views
|
||||
.into_iter()
|
||||
.map(|view| IndexableData::from_view(view, workspace_id))
|
||||
.collect();
|
||||
|
||||
let _ = self.index_all(indexable_data);
|
||||
}
|
||||
|
||||
@ -440,23 +327,50 @@ impl FolderIndexManager for FolderIndexManagerImpl {
|
||||
for change in changes {
|
||||
match change {
|
||||
FolderViewChange::Inserted { view_id } => {
|
||||
let view = views_iter.find(|view| view.id == view_id);
|
||||
if let Some(view) = view {
|
||||
if let Some(view) = views_iter.find(|view| view.id == view_id) {
|
||||
let indexable_data = IndexableData::from_view(view, workspace_id);
|
||||
let _ = self.add_index(indexable_data);
|
||||
let f = self.clone();
|
||||
tokio::spawn(async move {
|
||||
let _ = f.add_index(indexable_data).await;
|
||||
});
|
||||
}
|
||||
},
|
||||
FolderViewChange::Updated { view_id } => {
|
||||
let view = views_iter.find(|view| view.id == view_id);
|
||||
if let Some(view) = view {
|
||||
if let Some(view) = views_iter.find(|view| view.id == view_id) {
|
||||
let indexable_data = IndexableData::from_view(view, workspace_id);
|
||||
let _ = self.update_index(indexable_data);
|
||||
let f = self.clone();
|
||||
tokio::spawn(async move {
|
||||
let _ = f.update_index(indexable_data).await;
|
||||
});
|
||||
}
|
||||
},
|
||||
FolderViewChange::Deleted { view_ids } => {
|
||||
let _ = self.remove_indices(view_ids);
|
||||
let f = self.clone();
|
||||
tokio::spawn(async move {
|
||||
let _ = f.remove_indices(view_ids).await;
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_schema_fields(
|
||||
folder_schema: &FolderSchema,
|
||||
) -> Result<(Field, Field, Field, Field, Field), FlowyError> {
|
||||
let id_field = folder_schema.schema.get_field(FOLDER_ID_FIELD_NAME)?;
|
||||
let title_field = folder_schema.schema.get_field(FOLDER_TITLE_FIELD_NAME)?;
|
||||
let icon_field = folder_schema.schema.get_field(FOLDER_ICON_FIELD_NAME)?;
|
||||
let icon_ty_field = folder_schema.schema.get_field(FOLDER_ICON_TY_FIELD_NAME)?;
|
||||
let workspace_id_field = folder_schema
|
||||
.schema
|
||||
.get_field(FOLDER_WORKSPACE_ID_FIELD_NAME)?;
|
||||
|
||||
Ok((
|
||||
id_field,
|
||||
title_field,
|
||||
icon_field,
|
||||
icon_ty_field,
|
||||
workspace_id_field,
|
||||
))
|
||||
}
|
||||
|
@ -1,12 +1,13 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::notifier::{SearchNotifier, SearchResultChanged, SearchResultReceiverRunner};
|
||||
use crate::entities::{SearchFilterPB, SearchResultNotificationPB, SearchResultPB};
|
||||
use crate::entities::{SearchFilterPB, SearchResponsePB, SearchResultPB};
|
||||
use allo_isolate::Isolate;
|
||||
use flowy_error::FlowyResult;
|
||||
|
||||
use lib_infra::async_trait::async_trait;
|
||||
use tokio::sync::broadcast;
|
||||
use lib_infra::isolate_stream::{IsolateSink, SinkExt};
|
||||
use std::collections::HashMap;
|
||||
use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
use tokio_stream::{self, Stream, StreamExt};
|
||||
use tracing::{error, trace};
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
pub enum SearchType {
|
||||
@ -19,15 +20,12 @@ pub trait SearchHandler: Send + Sync + 'static {
|
||||
/// returns the type of search this handler is responsible for
|
||||
fn search_type(&self) -> SearchType;
|
||||
|
||||
/// performs a search and returns the results
|
||||
/// performs a search and returns a stream of results
|
||||
async fn perform_search(
|
||||
&self,
|
||||
query: String,
|
||||
filter: Option<SearchFilterPB>,
|
||||
) -> FlowyResult<Vec<SearchResultPB>>;
|
||||
|
||||
/// returns the number of indexed objects
|
||||
fn index_count(&self) -> u64;
|
||||
) -> Pin<Box<dyn Stream<Item = FlowyResult<SearchResultPB>> + Send + 'static>>;
|
||||
}
|
||||
|
||||
/// The [SearchManager] is used to inject multiple [SearchHandler]'s
|
||||
@ -36,7 +34,7 @@ pub trait SearchHandler: Send + Sync + 'static {
|
||||
///
|
||||
pub struct SearchManager {
|
||||
pub handlers: HashMap<SearchType, Arc<dyn SearchHandler>>,
|
||||
notifier: SearchNotifier,
|
||||
current_search: Arc<tokio::sync::Mutex<Option<String>>>, // Track current search
|
||||
}
|
||||
|
||||
impl SearchManager {
|
||||
@ -46,45 +44,88 @@ impl SearchManager {
|
||||
.map(|handler| (handler.search_type(), handler))
|
||||
.collect();
|
||||
|
||||
// Initialize Search Notifier
|
||||
let (notifier, _) = broadcast::channel(100);
|
||||
tokio::spawn(SearchResultReceiverRunner(Some(notifier.subscribe())).run());
|
||||
|
||||
Self { handlers, notifier }
|
||||
Self {
|
||||
handlers,
|
||||
current_search: Arc::new(tokio::sync::Mutex::new(None)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_handler(&self, search_type: SearchType) -> Option<&Arc<dyn SearchHandler>> {
|
||||
self.handlers.get(&search_type)
|
||||
}
|
||||
|
||||
pub fn perform_search(
|
||||
pub async fn perform_search(
|
||||
&self,
|
||||
query: String,
|
||||
stream_port: i64,
|
||||
filter: Option<SearchFilterPB>,
|
||||
channel: Option<String>,
|
||||
search_id: String,
|
||||
) {
|
||||
let max: usize = self.handlers.len();
|
||||
let handlers = self.handlers.clone();
|
||||
for (_, handler) in handlers {
|
||||
let q = query.clone();
|
||||
let f = filter.clone();
|
||||
let ch = channel.clone();
|
||||
let notifier = self.notifier.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
let res = handler.perform_search(q.clone(), f).await;
|
||||
|
||||
let items = res.unwrap_or_default();
|
||||
|
||||
let notification = SearchResultNotificationPB {
|
||||
items,
|
||||
sends: max as u64,
|
||||
channel: ch,
|
||||
query: q,
|
||||
};
|
||||
|
||||
let _ = notifier.send(SearchResultChanged::SearchResultUpdate(notification));
|
||||
});
|
||||
// Cancel previous search by updating current_search
|
||||
{
|
||||
let mut current = self.current_search.lock().await;
|
||||
*current = Some(search_id.clone());
|
||||
}
|
||||
|
||||
let handlers = self.handlers.clone();
|
||||
let sink = IsolateSink::new(Isolate::new(stream_port));
|
||||
let mut join_handles = vec![];
|
||||
let current_search = self.current_search.clone();
|
||||
|
||||
for (_, handler) in handlers {
|
||||
let mut clone_sink = sink.clone();
|
||||
let query = query.clone();
|
||||
let filter = filter.clone();
|
||||
let search_id = search_id.clone();
|
||||
let current_search = current_search.clone();
|
||||
|
||||
let handle = tokio::spawn(async move {
|
||||
let mut stream = handler.perform_search(query.clone(), filter).await;
|
||||
while let Some(result) = stream.next().await {
|
||||
if !is_current_search(¤t_search, &search_id).await {
|
||||
trace!("[Search] search changed, cancel search: {}", query);
|
||||
break;
|
||||
}
|
||||
|
||||
if let Ok(result) = result {
|
||||
let resp = SearchResponsePB {
|
||||
result: Some(result),
|
||||
search_id: search_id.clone(),
|
||||
is_loading: true,
|
||||
};
|
||||
if let Ok::<Vec<u8>, _>(data) = resp.try_into() {
|
||||
if let Err(err) = clone_sink.send(data).await {
|
||||
error!("Failed to send search result: {}", err);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !is_current_search(¤t_search, &search_id).await {
|
||||
trace!("[Search] search changed, cancel search: {}", query);
|
||||
return;
|
||||
}
|
||||
|
||||
let resp = SearchResponsePB {
|
||||
result: None,
|
||||
search_id: search_id.clone(),
|
||||
is_loading: true,
|
||||
};
|
||||
if let Ok::<Vec<u8>, _>(data) = resp.try_into() {
|
||||
let _ = clone_sink.send(data).await;
|
||||
}
|
||||
});
|
||||
join_handles.push(handle);
|
||||
}
|
||||
futures::future::join_all(join_handles).await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn is_current_search(
|
||||
current_search: &Arc<tokio::sync::Mutex<Option<String>>>,
|
||||
search_id: &str,
|
||||
) -> bool {
|
||||
let current = current_search.lock().await;
|
||||
current.as_ref().map_or(false, |id| id == search_id)
|
||||
}
|
||||
|
@ -1,2 +1 @@
|
||||
pub mod manager;
|
||||
pub mod notifier;
|
||||
|
@ -1,61 +0,0 @@
|
||||
use async_stream::stream;
|
||||
use flowy_notification::NotificationBuilder;
|
||||
use futures::stream::StreamExt;
|
||||
use tokio::sync::broadcast;
|
||||
|
||||
use crate::entities::{SearchNotification, SearchResultNotificationPB};
|
||||
|
||||
const SEARCH_OBSERVABLE_SOURCE: &str = "Search";
|
||||
const SEARCH_ID: &str = "SEARCH_IDENTIFIER";
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum SearchResultChanged {
|
||||
SearchResultUpdate(SearchResultNotificationPB),
|
||||
}
|
||||
|
||||
pub type SearchNotifier = broadcast::Sender<SearchResultChanged>;
|
||||
|
||||
pub(crate) struct SearchResultReceiverRunner(
|
||||
pub(crate) Option<broadcast::Receiver<SearchResultChanged>>,
|
||||
);
|
||||
|
||||
impl SearchResultReceiverRunner {
|
||||
pub(crate) async fn run(mut self) {
|
||||
let mut receiver = self.0.take().expect("Only take once");
|
||||
let stream = stream! {
|
||||
while let Ok(changed) = receiver.recv().await {
|
||||
yield changed;
|
||||
}
|
||||
};
|
||||
stream
|
||||
.for_each(|changed| async {
|
||||
match changed {
|
||||
SearchResultChanged::SearchResultUpdate(notification) => {
|
||||
send_notification(
|
||||
SEARCH_ID,
|
||||
SearchNotification::DidUpdateResults,
|
||||
notification.channel.clone(),
|
||||
)
|
||||
.payload(notification)
|
||||
.send();
|
||||
},
|
||||
}
|
||||
})
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "trace")]
|
||||
pub fn send_notification(
|
||||
id: &str,
|
||||
ty: SearchNotification,
|
||||
channel: Option<String>,
|
||||
) -> NotificationBuilder {
|
||||
let observable_source = &format!(
|
||||
"{}{}",
|
||||
SEARCH_OBSERVABLE_SOURCE,
|
||||
channel.unwrap_or_default()
|
||||
);
|
||||
|
||||
NotificationBuilder::new(id, ty, observable_source)
|
||||
}
|
@ -1,19 +1,16 @@
|
||||
use client_api::entity::search_dto::SearchDocumentResponseItem;
|
||||
use crate::af_cloud::AFServer;
|
||||
use flowy_ai_pub::cloud::search_dto::{
|
||||
SearchDocumentResponseItem, SearchResult, SearchSummaryResult,
|
||||
};
|
||||
use flowy_error::FlowyError;
|
||||
use flowy_search_pub::cloud::SearchCloudService;
|
||||
use lib_infra::async_trait::async_trait;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::af_cloud::AFServer;
|
||||
|
||||
pub(crate) struct AFCloudSearchCloudServiceImpl<T> {
|
||||
pub inner: T,
|
||||
}
|
||||
|
||||
// The limit of what the score should be for results, used to
|
||||
// filter out irrelevant results.
|
||||
// https://community.openai.com/t/rule-of-thumb-cosine-similarity-thresholds/693670/5
|
||||
const SCORE_LIMIT: f64 = 0.3;
|
||||
const DEFAULT_PREVIEW: u32 = 80;
|
||||
|
||||
#[async_trait]
|
||||
@ -28,14 +25,22 @@ where
|
||||
) -> Result<Vec<SearchDocumentResponseItem>, FlowyError> {
|
||||
let client = self.inner.try_get_client()?;
|
||||
let result = client
|
||||
.search_documents(workspace_id, &query, 10, DEFAULT_PREVIEW)
|
||||
.search_documents(workspace_id, &query, 10, DEFAULT_PREVIEW, None)
|
||||
.await?;
|
||||
|
||||
// Filter out irrelevant results
|
||||
let result = result
|
||||
.into_iter()
|
||||
.filter(|r| r.score > SCORE_LIMIT)
|
||||
.collect();
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
async fn generate_search_summary(
|
||||
&self,
|
||||
workspace_id: &Uuid,
|
||||
query: String,
|
||||
search_results: Vec<SearchResult>,
|
||||
) -> Result<SearchSummaryResult, FlowyError> {
|
||||
let client = self.inner.try_get_client()?;
|
||||
let result = client
|
||||
.generate_search_summary(workspace_id, &query, search_results)
|
||||
.await?;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
@ -127,7 +127,7 @@ where
|
||||
let try_get_client = self.server.try_get_client();
|
||||
let client = try_get_client?;
|
||||
let response = client.sign_in_password(&email, &password).await?;
|
||||
Ok(response)
|
||||
Ok(response.gotrue_response)
|
||||
}
|
||||
|
||||
async fn sign_in_with_magic_link(
|
||||
|
@ -20,5 +20,5 @@ pub trait UserWorkspaceService: Send + Sync {
|
||||
) -> FlowyResult<()>;
|
||||
|
||||
/// Removes local indexes when a workspace is left/deleted
|
||||
fn did_delete_workspace(&self, workspace_id: &Uuid) -> FlowyResult<()>;
|
||||
async fn did_delete_workspace(&self, workspace_id: &Uuid) -> FlowyResult<()>;
|
||||
}
|
||||
|
@ -93,9 +93,9 @@ impl AuthenticateUser {
|
||||
self.database.get_connection(uid)
|
||||
}
|
||||
|
||||
pub fn get_index_path(&self) -> PathBuf {
|
||||
let uid = self.user_id().unwrap_or(0);
|
||||
PathBuf::from(self.user_paths.user_data_dir(uid)).join("indexes")
|
||||
pub fn get_index_path(&self) -> FlowyResult<PathBuf> {
|
||||
let uid = self.user_id()?;
|
||||
Ok(PathBuf::from(self.user_paths.user_data_dir(uid)).join("indexes"))
|
||||
}
|
||||
|
||||
pub fn get_user_data_dir(&self) -> FlowyResult<PathBuf> {
|
||||
|
@ -279,6 +279,7 @@ impl UserManager {
|
||||
self
|
||||
.user_workspace_service
|
||||
.did_delete_workspace(workspace_id)
|
||||
.await
|
||||
}
|
||||
|
||||
#[instrument(level = "info", skip(self), err)]
|
||||
@ -295,7 +296,8 @@ impl UserManager {
|
||||
|
||||
self
|
||||
.user_workspace_service
|
||||
.did_delete_workspace(workspace_id)?;
|
||||
.did_delete_workspace(workspace_id)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ use std::pin::Pin;
|
||||
use std::task::{Context, Poll};
|
||||
|
||||
#[pin_project]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct IsolateSink {
|
||||
isolate: Isolate,
|
||||
}
|
||||
|
Reference in New Issue
Block a user