refactored chat archive and added search

This commit is contained in:
Fabian Freund
2024-06-16 13:07:16 +02:00
parent 9daca26261
commit d7a65a5770
18 changed files with 1150 additions and 53 deletions
@@ -1,4 +1,4 @@
import 'package:bang_navigator/features/chat_archive/data/repositories/chat_archive_file.dart';
import 'package:bang_navigator/features/chat_archive/data/services/file.dart';
import 'package:bang_navigator/features/chat_archive/domain/entities/chat_entity.dart';
import 'package:bang_navigator/features/kagi/data/services/chat.dart';
import 'package:exceptions/exceptions.dart';
@@ -6,16 +6,15 @@ import 'package:path/path.dart' as path;
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:rxdart/rxdart.dart';
part 'chat_archive.g.dart';
part 'archive.g.dart';
@Riverpod()
class ChatArchiveRepository extends _$ChatArchiveRepository {
Future<List<ChatEntity>> _listArchivedChats() async {
Future<List<ChatEntity>> listArchivedChats() async {
final files =
await ref.read(chatArchiveFileRepositoryProvider.notifier).list();
await ref.read(chatArchiveFileServiceProvider.notifier).list();
return files
.where((file) => path.extension(file.path) == '.md')
.map((file) => ChatEntity.fromFileName(path.basename(file.path)))
.toList();
}
@@ -26,14 +25,14 @@ class ChatArchiveRepository extends _$ChatArchiveRepository {
return contentsResult.flatMapAsync(
(contents) => ref
.read(chatArchiveFileRepositoryProvider.notifier)
.read(chatArchiveFileServiceProvider.notifier)
.write(fileName, contents),
);
}
Future<Result<String>> readChat(String fileName) async {
final contentsResult = await Result.fromAsync(
() => ref.read(chatArchiveFileRepositoryProvider.notifier).read(fileName),
() => ref.read(chatArchiveFileServiceProvider.notifier).read(fileName),
);
return contentsResult.fold(
@@ -51,12 +50,12 @@ class ChatArchiveRepository extends _$ChatArchiveRepository {
@override
Stream<List<ChatEntity>> build() async* {
final fileRepository = ref.watch(chatArchiveFileRepositoryProvider);
final fileRepository = ref.watch(chatArchiveFileServiceProvider);
yield* ConcatStream([
_listArchivedChats().asStream(),
listArchivedChats().asStream(),
fileRepository.asyncMap(
(_) => _listArchivedChats(),
(_) => listArchivedChats(),
),
]);
}
@@ -1,6 +1,6 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'chat_archive.dart';
part of 'archive.dart';
// **************************************************************************
// RiverpodGenerator
@@ -156,7 +156,7 @@ class _ReadArchivedChatProviderElement
}
String _$chatArchiveRepositoryHash() =>
r'c53b0d2704953a205c12357642237e15e8ffd3b1';
r'1b84b6f26a7c5f6c27874904b893a979a76e1fec';
/// See also [ChatArchiveRepository].
@ProviderFor(ChatArchiveRepository)
@@ -0,0 +1,120 @@
import 'dart:async';
import 'package:bang_navigator/core/logger.dart';
import 'package:bang_navigator/features/chat_archive/data/database/database.dart';
import 'package:bang_navigator/features/chat_archive/data/providers.dart';
import 'package:bang_navigator/features/chat_archive/data/services/file.dart';
import 'package:bang_navigator/features/chat_archive/domain/entities/chat_entity.dart';
import 'package:bang_navigator/features/chat_archive/domain/repositories/archive.dart';
import 'package:bang_navigator/features/chat_archive/utils/markdown_to_text.dart';
import 'package:collection/collection.dart';
import 'package:path/path.dart' as path;
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:watcher/watcher.dart';
part 'search.g.dart';
@Riverpod()
class ChatArchiveSearchRepository extends _$ChatArchiveSearchRepository {
late Completer<void> _populatedCompleter;
late StreamController<List<ChatQueryResult>> _streamController;
late ChatSearchDatabase _searchDatabase;
Future<ChatCompanion?> _readChat(ChatEntity chat) async {
final contentResult = await ref
.read(chatArchiveRepositoryProvider.notifier)
.readChat(chat.fileName);
return contentResult.fold(
(content) => ChatCompanion.insert(
fileName: chat.fileName,
title: chat.name!,
content: markdownToText(content),
),
onFailure: (errorMessage) {
logger.e(
'Error reading file "${chat.fileName}": ${errorMessage.message}',
error: errorMessage.details,
stackTrace: errorMessage.stackTrace,
);
return null;
},
);
}
Future<Iterable<ChatCompanion>> _availableChats() async {
final availableChats = await ref
.read(chatArchiveRepositoryProvider.notifier)
.listArchivedChats()
.then(
(chats) => Future.wait(
chats.where((chat) => chat.name != null).map(_readChat),
),
);
return availableChats.whereNotNull();
}
Future<void> search(
String input, {
int snippetLength = 120,
String matchPrefix = '***',
String matchSuffix = '***',
String ellipsis = '',
}) async {
if (input.isNotEmpty) {
await _populatedCompleter.future;
await _searchDatabase.searchDao
.queryChats(
searchString: input,
snippetLength: snippetLength,
matchPrefix: matchPrefix,
matchSuffix: matchSuffix,
ellipsis: ellipsis,
)
.get()
.then(_streamController.add);
}
}
@override
Stream<List<ChatQueryResult>> build() async* {
_populatedCompleter = Completer();
_streamController = StreamController();
_searchDatabase = ref.watch(chatSearchDatabaseProvider);
ref.onDispose(() async {
await _streamController.close();
});
// populate with initial chats
await _searchDatabase.searchDao.deleteAllChats();
await _searchDatabase.searchDao.indexChats(await _availableChats());
_populatedCompleter.complete();
final changeStreamSubscription =
ref.watch(chatArchiveFileServiceProvider).listen((event) async {
final chat = ChatEntity.fromFileName(path.basename(event.path));
switch (event.type) {
case ChangeType.ADD:
case ChangeType.MODIFY:
if (chat.name != null) {
final companion = await _readChat(chat);
if (companion != null) {
await _searchDatabase.searchDao.upsertChat(companion);
}
}
case ChangeType.REMOVE:
await _searchDatabase.searchDao.deleteChat(chat.fileName);
}
});
ref.onDispose(() async {
await changeStreamSubscription.cancel();
});
yield* _streamController.stream;
}
}
@@ -0,0 +1,28 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'search.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$chatArchiveSearchRepositoryHash() =>
r'6cb31ab2c4a37561ec17a60dae3bb4ecd3893744';
/// See also [ChatArchiveSearchRepository].
@ProviderFor(ChatArchiveSearchRepository)
final chatArchiveSearchRepositoryProvider = AutoDisposeStreamNotifierProvider<
ChatArchiveSearchRepository, List<ChatQueryResult>>.internal(
ChatArchiveSearchRepository.new,
name: r'chatArchiveSearchRepositoryProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$chatArchiveSearchRepositoryHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$ChatArchiveSearchRepository
= AutoDisposeStreamNotifier<List<ChatQueryResult>>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member