intermediate
This commit is contained in:
@@ -0,0 +1,42 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:langchain_openai/langchain_openai.dart';
|
||||||
|
import 'package:riverpod/riverpod.dart';
|
||||||
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
|
||||||
|
part 'models.g.dart';
|
||||||
|
|
||||||
|
// @Riverpod(keepAlive: true)
|
||||||
|
// ChatOpenAI summarizerModel(Ref ref) {
|
||||||
|
// final openaiApiKey = Platform.environment['OPENAI_API_KEY'];
|
||||||
|
// return ChatOpenAI(
|
||||||
|
// apiKey: openaiApiKey,
|
||||||
|
// baseUrl: 'https://api.together.xyz/v1',
|
||||||
|
// defaultOptions: const ChatOpenAIOptions(model: 'deepseek-ai/DeepSeek-V3'),
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
@Riverpod(keepAlive: true)
|
||||||
|
ChatOpenAI chatModel(Ref ref) {
|
||||||
|
final openaiApiKey = 'FfTBKW1t5dOOXVoiaF2hbXPAHiz3fvfy';
|
||||||
|
return ChatOpenAI(
|
||||||
|
apiKey: openaiApiKey,
|
||||||
|
baseUrl: 'https://api.deepinfra.com/v1/openai',
|
||||||
|
defaultOptions: const ChatOpenAIOptions(
|
||||||
|
model: 'meta-llama/Llama-3.3-70B-Instruct-Turbo',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Riverpod(keepAlive: true)
|
||||||
|
OpenAIEmbeddings embeddingModel(Ref ref) {
|
||||||
|
final openaiApiKey = 'FfTBKW1t5dOOXVoiaF2hbXPAHiz3fvfy';
|
||||||
|
return OpenAIEmbeddings(
|
||||||
|
apiKey: openaiApiKey,
|
||||||
|
baseUrl: 'https://api.deepinfra.com/v1/openai',
|
||||||
|
model: 'BAAI/bge-m3',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Riverpod(keepAlive: true)
|
||||||
|
int embeddingDimensions(Ref ref) => 1024;
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'models.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// RiverpodGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
String _$chatModelHash() => r'e7538b9f0dd43ea64953164c5402e76fde96f5e9';
|
||||||
|
|
||||||
|
/// See also [chatModel].
|
||||||
|
@ProviderFor(chatModel)
|
||||||
|
final chatModelProvider = Provider<ChatOpenAI>.internal(
|
||||||
|
chatModel,
|
||||||
|
name: r'chatModelProvider',
|
||||||
|
debugGetCreateSourceHash:
|
||||||
|
const bool.fromEnvironment('dart.vm.product') ? null : _$chatModelHash,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
|
// ignore: unused_element
|
||||||
|
typedef ChatModelRef = ProviderRef<ChatOpenAI>;
|
||||||
|
String _$embeddingModelHash() => r'5d65c75df195a26adefac5a75f0e8f6b0f1b8590';
|
||||||
|
|
||||||
|
/// See also [embeddingModel].
|
||||||
|
@ProviderFor(embeddingModel)
|
||||||
|
final embeddingModelProvider = Provider<OpenAIEmbeddings>.internal(
|
||||||
|
embeddingModel,
|
||||||
|
name: r'embeddingModelProvider',
|
||||||
|
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||||
|
? null
|
||||||
|
: _$embeddingModelHash,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
|
// ignore: unused_element
|
||||||
|
typedef EmbeddingModelRef = ProviderRef<OpenAIEmbeddings>;
|
||||||
|
String _$embeddingDimensionsHash() =>
|
||||||
|
r'e34f544a18a7e2bd6a479b21f2016b0c0e2ba96c';
|
||||||
|
|
||||||
|
/// See also [embeddingDimensions].
|
||||||
|
@ProviderFor(embeddingDimensions)
|
||||||
|
final embeddingDimensionsProvider = Provider<int>.internal(
|
||||||
|
embeddingDimensions,
|
||||||
|
name: r'embeddingDimensionsProvider',
|
||||||
|
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||||
|
? null
|
||||||
|
: _$embeddingDimensionsHash,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
|
// ignore: unused_element
|
||||||
|
typedef EmbeddingDimensionsRef = ProviderRef<int>;
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
||||||
@@ -6,7 +6,9 @@ import 'package:riverpod_annotation/riverpod_annotation.dart';
|
|||||||
part 'providers.g.dart';
|
part 'providers.g.dart';
|
||||||
|
|
||||||
@Riverpod(keepAlive: true)
|
@Riverpod(keepAlive: true)
|
||||||
GoRouter router(Ref ref) => GoRouter(
|
GoRouter router(Ref ref) {
|
||||||
debugLogDiagnostics: true,
|
return GoRouter(
|
||||||
routes: $appRoutes,
|
debugLogDiagnostics: true,
|
||||||
);
|
routes: $appRoutes,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ part of 'providers.dart';
|
|||||||
// RiverpodGenerator
|
// RiverpodGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$routerHash() => r'dcafae2924a2ad3d5d836c6756d2e4a4ead36b4e';
|
String _$routerHash() => r'25fc899b87c080600f6e1d87d510db333378bdfd';
|
||||||
|
|
||||||
/// See also [router].
|
/// See also [router].
|
||||||
@ProviderFor(router)
|
@ProviderFor(router)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:lensai/core/routing/dialog_page.dart';
|
import 'package:lensai/core/routing/dialog_page.dart';
|
||||||
|
import 'package:lensai/data/models/web_page_info.dart';
|
||||||
import 'package:lensai/features/about/presentation/screens/about.dart';
|
import 'package:lensai/features/about/presentation/screens/about.dart';
|
||||||
import 'package:lensai/features/bangs/presentation/screens/categories.dart';
|
import 'package:lensai/features/bangs/presentation/screens/categories.dart';
|
||||||
import 'package:lensai/features/bangs/presentation/screens/list.dart';
|
import 'package:lensai/features/bangs/presentation/screens/list.dart';
|
||||||
@@ -8,10 +9,12 @@ import 'package:lensai/features/bangs/presentation/screens/search.dart';
|
|||||||
import 'package:lensai/features/chat_archive/presentation/screens/detail.dart';
|
import 'package:lensai/features/chat_archive/presentation/screens/detail.dart';
|
||||||
import 'package:lensai/features/chat_archive/presentation/screens/list.dart';
|
import 'package:lensai/features/chat_archive/presentation/screens/list.dart';
|
||||||
import 'package:lensai/features/chat_archive/presentation/screens/search.dart';
|
import 'package:lensai/features/chat_archive/presentation/screens/search.dart';
|
||||||
|
import 'package:lensai/features/geckoview/features/browser/presentation/dialogs/web_page_dialog.dart';
|
||||||
import 'package:lensai/features/geckoview/features/browser/presentation/screens/browser.dart';
|
import 'package:lensai/features/geckoview/features/browser/presentation/screens/browser.dart';
|
||||||
import 'package:lensai/features/geckoview/features/tabs/presentation/screens/container_list.dart';
|
import 'package:lensai/features/geckoview/features/tabs/presentation/screens/container_list.dart';
|
||||||
import 'package:lensai/features/search/presentation/screens/search.dart';
|
import 'package:lensai/features/search/presentation/screens/search.dart';
|
||||||
import 'package:lensai/features/settings/presentation/screens/settings.dart';
|
import 'package:lensai/features/settings/presentation/screens/settings.dart';
|
||||||
|
import 'package:lensai/features/user/presentation/screens/auth.dart';
|
||||||
|
|
||||||
part 'routes.g.dart';
|
part 'routes.g.dart';
|
||||||
|
|
||||||
@@ -19,10 +22,18 @@ part 'routes.g.dart';
|
|||||||
name: 'BrowserRoute',
|
name: 'BrowserRoute',
|
||||||
path: '/',
|
path: '/',
|
||||||
routes: [
|
routes: [
|
||||||
|
TypedGoRoute<WebPageRoute>(
|
||||||
|
name: 'WebPageRoute',
|
||||||
|
path: 'page/:url',
|
||||||
|
),
|
||||||
TypedGoRoute<AboutRoute>(
|
TypedGoRoute<AboutRoute>(
|
||||||
name: 'AboutRoute',
|
name: 'AboutRoute',
|
||||||
path: 'about',
|
path: 'about',
|
||||||
),
|
),
|
||||||
|
TypedGoRoute<UserAuthRoute>(
|
||||||
|
name: 'UserAuthRoute',
|
||||||
|
path: 'userAuth',
|
||||||
|
),
|
||||||
TypedGoRoute<SearchRoute>(
|
TypedGoRoute<SearchRoute>(
|
||||||
name: 'SearchRoute',
|
name: 'SearchRoute',
|
||||||
path: 'search/:searchText',
|
path: 'search/:searchText',
|
||||||
@@ -60,6 +71,22 @@ class BrowserRoute extends GoRouteData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class WebPageRoute extends GoRouteData {
|
||||||
|
final String url;
|
||||||
|
|
||||||
|
const WebPageRoute({required this.url});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Page<void> buildPage(BuildContext context, GoRouterState state) {
|
||||||
|
return DialogPage(
|
||||||
|
builder: (_) => WebPageDialog(
|
||||||
|
url: Uri.parse(url),
|
||||||
|
precachedInfo: state.extra as WebPageInfo?,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class AboutRoute extends GoRouteData {
|
class AboutRoute extends GoRouteData {
|
||||||
@override
|
@override
|
||||||
Page<void> buildPage(BuildContext context, GoRouterState state) {
|
Page<void> buildPage(BuildContext context, GoRouterState state) {
|
||||||
@@ -67,6 +94,13 @@ class AboutRoute extends GoRouteData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class UserAuthRoute extends GoRouteData {
|
||||||
|
@override
|
||||||
|
Page<void> buildPage(BuildContext context, GoRouterState state) {
|
||||||
|
return DialogPage(builder: (_) => const UserAuthScreen());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class SearchRoute extends GoRouteData {
|
class SearchRoute extends GoRouteData {
|
||||||
static const String emptySearchText = ' ';
|
static const String emptySearchText = ' ';
|
||||||
|
|
||||||
|
|||||||
@@ -17,11 +17,21 @@ RouteBase get $browserRoute => GoRouteData.$route(
|
|||||||
name: 'BrowserRoute',
|
name: 'BrowserRoute',
|
||||||
factory: $BrowserRouteExtension._fromState,
|
factory: $BrowserRouteExtension._fromState,
|
||||||
routes: [
|
routes: [
|
||||||
|
GoRouteData.$route(
|
||||||
|
path: 'page/:url',
|
||||||
|
name: 'WebPageRoute',
|
||||||
|
factory: $WebPageRouteExtension._fromState,
|
||||||
|
),
|
||||||
GoRouteData.$route(
|
GoRouteData.$route(
|
||||||
path: 'about',
|
path: 'about',
|
||||||
name: 'AboutRoute',
|
name: 'AboutRoute',
|
||||||
factory: $AboutRouteExtension._fromState,
|
factory: $AboutRouteExtension._fromState,
|
||||||
),
|
),
|
||||||
|
GoRouteData.$route(
|
||||||
|
path: 'userAuth',
|
||||||
|
name: 'UserAuthRoute',
|
||||||
|
factory: $UserAuthRouteExtension._fromState,
|
||||||
|
),
|
||||||
GoRouteData.$route(
|
GoRouteData.$route(
|
||||||
path: 'search/:searchText',
|
path: 'search/:searchText',
|
||||||
name: 'SearchRoute',
|
name: 'SearchRoute',
|
||||||
@@ -76,6 +86,25 @@ extension $BrowserRouteExtension on BrowserRoute {
|
|||||||
void replace(BuildContext context) => context.replace(location);
|
void replace(BuildContext context) => context.replace(location);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension $WebPageRouteExtension on WebPageRoute {
|
||||||
|
static WebPageRoute _fromState(GoRouterState state) => WebPageRoute(
|
||||||
|
url: state.pathParameters['url']!,
|
||||||
|
);
|
||||||
|
|
||||||
|
String get location => GoRouteData.$location(
|
||||||
|
'/page/${Uri.encodeComponent(url)}',
|
||||||
|
);
|
||||||
|
|
||||||
|
void go(BuildContext context) => context.go(location);
|
||||||
|
|
||||||
|
Future<T?> push<T>(BuildContext context) => context.push<T>(location);
|
||||||
|
|
||||||
|
void pushReplacement(BuildContext context) =>
|
||||||
|
context.pushReplacement(location);
|
||||||
|
|
||||||
|
void replace(BuildContext context) => context.replace(location);
|
||||||
|
}
|
||||||
|
|
||||||
extension $AboutRouteExtension on AboutRoute {
|
extension $AboutRouteExtension on AboutRoute {
|
||||||
static AboutRoute _fromState(GoRouterState state) => AboutRoute();
|
static AboutRoute _fromState(GoRouterState state) => AboutRoute();
|
||||||
|
|
||||||
@@ -93,6 +122,23 @@ extension $AboutRouteExtension on AboutRoute {
|
|||||||
void replace(BuildContext context) => context.replace(location);
|
void replace(BuildContext context) => context.replace(location);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension $UserAuthRouteExtension on UserAuthRoute {
|
||||||
|
static UserAuthRoute _fromState(GoRouterState state) => UserAuthRoute();
|
||||||
|
|
||||||
|
String get location => GoRouteData.$location(
|
||||||
|
'/userAuth',
|
||||||
|
);
|
||||||
|
|
||||||
|
void go(BuildContext context) => context.go(location);
|
||||||
|
|
||||||
|
Future<T?> push<T>(BuildContext context) => context.push<T>(location);
|
||||||
|
|
||||||
|
void pushReplacement(BuildContext context) =>
|
||||||
|
context.pushReplacement(location);
|
||||||
|
|
||||||
|
void replace(BuildContext context) => context.replace(location);
|
||||||
|
}
|
||||||
|
|
||||||
extension $SearchRouteExtension on SearchRoute {
|
extension $SearchRouteExtension on SearchRoute {
|
||||||
static SearchRoute _fromState(GoRouterState state) => SearchRoute(
|
static SearchRoute _fromState(GoRouterState state) => SearchRoute(
|
||||||
searchText:
|
searchText:
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
extension StringExtension on String {
|
||||||
|
String toCapitalized() =>
|
||||||
|
isEmpty ? this : '${this[0].toUpperCase()}${substring(1)}';
|
||||||
|
}
|
||||||
@@ -91,7 +91,17 @@ class BangDao extends DatabaseAccessor<BangDatabase> with _$BangDaoMixin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Selectable<BangData> queryBangs(String searchString) {
|
Selectable<BangData> queryBangs(String searchString) {
|
||||||
return db.bangQuery(query: db.buildFtsQuery(searchString));
|
final ftsQuery = db.buildFtsQuery(searchString);
|
||||||
|
|
||||||
|
if (ftsQuery.isNotEmpty) {
|
||||||
|
return db.queryBangs(
|
||||||
|
query: ftsQuery,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return db.queryBangsBasic(
|
||||||
|
query: db.buildLikeQuery(searchString),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<int> addSearchEntry(String trigger, String searchQuery) {
|
Future<int> addSearchEntry(String trigger, String searchQuery) {
|
||||||
|
|||||||
@@ -72,20 +72,56 @@ END;
|
|||||||
optimizeFtsIndex:
|
optimizeFtsIndex:
|
||||||
INSERT INTO bang_fts(bang_fts) VALUES ('optimize');
|
INSERT INTO bang_fts(bang_fts) VALUES ('optimize');
|
||||||
|
|
||||||
bangQuery WITH BangData:
|
queryBangs WITH BangData:
|
||||||
SELECT
|
WITH weights AS (
|
||||||
|
SELECT
|
||||||
|
-- Customize these weights (higher = more important)
|
||||||
|
10.0 AS "trigger", -- Title matches are most important
|
||||||
|
5.0 AS website_name -- URL matches are quite important
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
b.*,
|
b.*,
|
||||||
bf.frequency,
|
bf.frequency,
|
||||||
bf.last_used
|
bf.last_used,
|
||||||
FROM
|
bm25(bang_fts, weights."trigger", weights.website_name) AS weighted_rank
|
||||||
|
FROM
|
||||||
bang_fts(:query) fts
|
bang_fts(:query) fts
|
||||||
INNER JOIN
|
INNER JOIN
|
||||||
bang b ON b.rowid = fts.rowid
|
bang b ON b.rowid = fts.rowid
|
||||||
LEFT JOIN
|
LEFT JOIN
|
||||||
bang_frequency bf ON b."trigger" = bf."trigger"
|
bang_frequency bf ON b."trigger" = bf."trigger"
|
||||||
ORDER BY
|
CROSS JOIN
|
||||||
RANK,
|
weights
|
||||||
bf.frequency NULLS LAST;
|
ORDER BY
|
||||||
|
weighted_rank ASC,
|
||||||
|
bf.frequency NULLS LAST;
|
||||||
|
|
||||||
|
queryBangsBasic WITH BangData:
|
||||||
|
WITH weights AS (
|
||||||
|
SELECT
|
||||||
|
-- Customize these weights (higher = more important)
|
||||||
|
10.0 AS "trigger", -- Title matches are most important
|
||||||
|
5.0 AS website_name -- URL matches are quite important
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
b.*,
|
||||||
|
bf.frequency,
|
||||||
|
bf.last_used,
|
||||||
|
bm25(bang_fts, weights."trigger", weights.website_name) AS weighted_rank
|
||||||
|
FROM
|
||||||
|
bang_fts fts
|
||||||
|
INNER JOIN
|
||||||
|
bang b ON b.rowid = fts.rowid
|
||||||
|
LEFT JOIN
|
||||||
|
bang_frequency bf ON b."trigger" = bf."trigger"
|
||||||
|
CROSS JOIN
|
||||||
|
weights
|
||||||
|
WHERE
|
||||||
|
fts."trigger" LIKE :query OR
|
||||||
|
fts.website_name LIKE :query
|
||||||
|
ORDER BY
|
||||||
|
weighted_rank ASC,
|
||||||
|
bf.frequency NULLS LAST;
|
||||||
|
|
||||||
categoriesJson:
|
categoriesJson:
|
||||||
WITH categories AS (
|
WITH categories AS (
|
||||||
|
|||||||
@@ -1095,16 +1095,40 @@ abstract class _$BangDatabase extends GeneratedDatabase {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Selectable<BangData> bangQuery({required String query}) {
|
Selectable<BangData> queryBangs({required String query}) {
|
||||||
return customSelect(
|
return customSelect(
|
||||||
'SELECT b.*, bf.frequency, bf.last_used FROM bang_fts(?1)AS fts INNER JOIN bang AS b ON b."rowid" = fts."rowid" LEFT JOIN bang_frequency AS bf ON b."trigger" = bf."trigger" ORDER BY RANK, bf.frequency NULLS LAST',
|
'WITH weights AS (SELECT 10.0 AS "trigger", 5.0 AS website_name) SELECT b.*, bf.frequency, bf.last_used, bm25(bang_fts, weights."trigger", weights.website_name) AS weighted_rank FROM bang_fts(?1)AS fts INNER JOIN bang AS b ON b."rowid" = fts."rowid" LEFT JOIN bang_frequency AS bf ON b."trigger" = bf."trigger" CROSS JOIN weights ORDER BY weighted_rank ASC, bf.frequency NULLS LAST',
|
||||||
variables: [
|
variables: [
|
||||||
Variable<String>(query)
|
Variable<String>(query)
|
||||||
],
|
],
|
||||||
readsFrom: {
|
readsFrom: {
|
||||||
bangFrequency,
|
bangFrequency,
|
||||||
bang,
|
|
||||||
bangFts,
|
bangFts,
|
||||||
|
bang,
|
||||||
|
}).map((QueryRow row) => BangData(
|
||||||
|
websiteName: row.read<String>('website_name'),
|
||||||
|
domain: row.read<String>('domain'),
|
||||||
|
trigger: row.read<String>('trigger'),
|
||||||
|
urlTemplate: row.read<String>('url_template'),
|
||||||
|
category: row.readNullable<String>('category'),
|
||||||
|
subCategory: row.readNullable<String>('sub_category'),
|
||||||
|
format: BangTable.$converterformat
|
||||||
|
.fromSql(row.readNullable<String>('format')),
|
||||||
|
frequency: row.readNullable<int>('frequency'),
|
||||||
|
lastUsed: row.readNullable<DateTime>('last_used'),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Selectable<BangData> queryBangsBasic({required String query}) {
|
||||||
|
return customSelect(
|
||||||
|
'WITH weights AS (SELECT 10.0 AS "trigger", 5.0 AS website_name) SELECT b.*, bf.frequency, bf.last_used, bm25(bang_fts, weights."trigger", weights.website_name) AS weighted_rank FROM bang_fts AS fts INNER JOIN bang AS b ON b."rowid" = fts."rowid" LEFT JOIN bang_frequency AS bf ON b."trigger" = bf."trigger" CROSS JOIN weights WHERE fts."trigger" LIKE ?1 OR fts.website_name LIKE ?1 ORDER BY weighted_rank ASC, bf.frequency NULLS LAST',
|
||||||
|
variables: [
|
||||||
|
Variable<String>(query)
|
||||||
|
],
|
||||||
|
readsFrom: {
|
||||||
|
bangFrequency,
|
||||||
|
bangFts,
|
||||||
|
bang,
|
||||||
}).map((QueryRow row) => BangData(
|
}).map((QueryRow row) => BangData(
|
||||||
websiteName: row.read<String>('website_name'),
|
websiteName: row.read<String>('website_name'),
|
||||||
domain: row.read<String>('domain'),
|
domain: row.read<String>('domain'),
|
||||||
@@ -1236,8 +1260,8 @@ final class $BangTableReferences
|
|||||||
db.bang.trigger, db.bangFrequency.trigger));
|
db.bang.trigger, db.bangFrequency.trigger));
|
||||||
|
|
||||||
$BangFrequencyProcessedTableManager get bangFrequencyRefs {
|
$BangFrequencyProcessedTableManager get bangFrequencyRefs {
|
||||||
final manager = $BangFrequencyTableManager($_db, $_db.bangFrequency)
|
final manager = $BangFrequencyTableManager($_db, $_db.bangFrequency).filter(
|
||||||
.filter((f) => f.trigger.trigger($_item.trigger));
|
(f) => f.trigger.trigger.sqlEquals($_itemColumn<String>('trigger')!));
|
||||||
|
|
||||||
final cache = $_typedResult.readTableOrNull(_bangFrequencyRefsTable($_db));
|
final cache = $_typedResult.readTableOrNull(_bangFrequencyRefsTable($_db));
|
||||||
return ProcessedTableManager(
|
return ProcessedTableManager(
|
||||||
@@ -1251,8 +1275,8 @@ final class $BangTableReferences
|
|||||||
$_aliasNameGenerator(db.bang.trigger, db.bangHistory.trigger));
|
$_aliasNameGenerator(db.bang.trigger, db.bangHistory.trigger));
|
||||||
|
|
||||||
$BangHistoryProcessedTableManager get bangHistoryRefs {
|
$BangHistoryProcessedTableManager get bangHistoryRefs {
|
||||||
final manager = $BangHistoryTableManager($_db, $_db.bangHistory)
|
final manager = $BangHistoryTableManager($_db, $_db.bangHistory).filter(
|
||||||
.filter((f) => f.trigger.trigger($_item.trigger));
|
(f) => f.trigger.trigger.sqlEquals($_itemColumn<String>('trigger')!));
|
||||||
|
|
||||||
final cache = $_typedResult.readTableOrNull(_bangHistoryRefsTable($_db));
|
final cache = $_typedResult.readTableOrNull(_bangHistoryRefsTable($_db));
|
||||||
return ProcessedTableManager(
|
return ProcessedTableManager(
|
||||||
@@ -1704,8 +1728,10 @@ final class $BangFrequencyReferences
|
|||||||
$_aliasNameGenerator(db.bangFrequency.trigger, db.bang.trigger));
|
$_aliasNameGenerator(db.bangFrequency.trigger, db.bang.trigger));
|
||||||
|
|
||||||
$BangTableProcessedTableManager get trigger {
|
$BangTableProcessedTableManager get trigger {
|
||||||
|
final $_column = $_itemColumn<String>('trigger')!;
|
||||||
|
|
||||||
final manager = $BangTableTableManager($_db, $_db.bang)
|
final manager = $BangTableTableManager($_db, $_db.bang)
|
||||||
.filter((f) => f.trigger($_item.trigger!));
|
.filter((f) => f.trigger.sqlEquals($_column));
|
||||||
final item = $_typedResult.readTableOrNull(_triggerTable($_db));
|
final item = $_typedResult.readTableOrNull(_triggerTable($_db));
|
||||||
if (item == null) return manager;
|
if (item == null) return manager;
|
||||||
return ProcessedTableManager(
|
return ProcessedTableManager(
|
||||||
@@ -1941,8 +1967,10 @@ final class $BangHistoryReferences
|
|||||||
$_aliasNameGenerator(db.bangHistory.trigger, db.bang.trigger));
|
$_aliasNameGenerator(db.bangHistory.trigger, db.bang.trigger));
|
||||||
|
|
||||||
$BangTableProcessedTableManager get trigger {
|
$BangTableProcessedTableManager get trigger {
|
||||||
|
final $_column = $_itemColumn<String>('trigger')!;
|
||||||
|
|
||||||
final manager = $BangTableTableManager($_db, $_db.bang)
|
final manager = $BangTableTableManager($_db, $_db.bang)
|
||||||
.filter((f) => f.trigger($_item.trigger!));
|
.filter((f) => f.trigger.sqlEquals($_column));
|
||||||
final item = $_typedResult.readTableOrNull(_triggerTable($_db));
|
final item = $_typedResult.readTableOrNull(_triggerTable($_db));
|
||||||
if (item == null) return manager;
|
if (item == null) return manager;
|
||||||
return ProcessedTableManager(
|
return ProcessedTableManager(
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:drift/native.dart';
|
import 'package:drift/native.dart';
|
||||||
import 'package:lensai/features/bangs/data/database/database.dart';
|
import 'package:lensai/features/bangs/data/database/database.dart';
|
||||||
@@ -13,7 +15,7 @@ part 'providers.g.dart';
|
|||||||
|
|
||||||
@Riverpod(keepAlive: true)
|
@Riverpod(keepAlive: true)
|
||||||
BangDatabase bangDatabase(Ref ref) {
|
BangDatabase bangDatabase(Ref ref) {
|
||||||
return BangDatabase(
|
final db = BangDatabase(
|
||||||
LazyDatabase(() async {
|
LazyDatabase(() async {
|
||||||
// put the database file, called db.sqlite here, into the documents folder
|
// put the database file, called db.sqlite here, into the documents folder
|
||||||
// for your app.
|
// for your app.
|
||||||
@@ -35,4 +37,10 @@ BangDatabase bangDatabase(Ref ref) {
|
|||||||
return NativeDatabase.createInBackground(file);
|
return NativeDatabase.createInBackground(file);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
ref.onDispose(() {
|
||||||
|
unawaited(db.close());
|
||||||
|
});
|
||||||
|
|
||||||
|
return db;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ part of 'providers.dart';
|
|||||||
// RiverpodGenerator
|
// RiverpodGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$bangDatabaseHash() => r'd7964e9cdbb6e60fb6d2a78f07bfd4fa34bcb236';
|
String _$bangDatabaseHash() => r'1ff3fb8bfa29269f6e515f3b22c216998769f578';
|
||||||
|
|
||||||
/// See also [bangDatabase].
|
/// See also [bangDatabase].
|
||||||
@ProviderFor(bangDatabase)
|
@ProviderFor(bangDatabase)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import 'package:lensai/features/bangs/domain/providers/bangs.dart';
|
|||||||
import 'package:lensai/features/bangs/presentation/widgets/bang_details.dart';
|
import 'package:lensai/features/bangs/presentation/widgets/bang_details.dart';
|
||||||
import 'package:lensai/features/geckoview/features/browser/domain/entities/sheet.dart';
|
import 'package:lensai/features/geckoview/features/browser/domain/entities/sheet.dart';
|
||||||
import 'package:lensai/features/geckoview/features/browser/domain/providers.dart';
|
import 'package:lensai/features/geckoview/features/browser/domain/providers.dart';
|
||||||
import 'package:lensai/features/geckoview/features/controllers/bottom_sheet.dart';
|
import 'package:lensai/features/geckoview/domain/controllers/bottom_sheet.dart';
|
||||||
import 'package:lensai/features/kagi/data/entities/modes.dart';
|
import 'package:lensai/features/kagi/data/entities/modes.dart';
|
||||||
import 'package:lensai/presentation/widgets/failure_widget.dart';
|
import 'package:lensai/presentation/widgets/failure_widget.dart';
|
||||||
|
|
||||||
|
|||||||
@@ -1,205 +0,0 @@
|
|||||||
// The default Material-style Autocomplete options.
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
||||||
import 'package:lensai/features/bangs/data/models/bang_data.dart';
|
|
||||||
import 'package:lensai/features/bangs/presentation/widgets/bang_icon.dart';
|
|
||||||
import 'package:lensai/features/geckoview/features/browser/presentation/widgets/speech_to_text_button.dart';
|
|
||||||
import 'package:lensai/features/search/domain/providers/search_suggestions.dart';
|
|
||||||
import 'package:lensai/features/user/domain/repositories/settings.dart';
|
|
||||||
import 'package:lensai/presentation/widgets/autocomplete.dart';
|
|
||||||
|
|
||||||
class SearchField extends HookConsumerWidget {
|
|
||||||
static const defaultMaxOptionsHeight = 158.0;
|
|
||||||
|
|
||||||
final TextEditingController textController;
|
|
||||||
final BangData? activeBang;
|
|
||||||
final OptionsViewOpenDirection openDirection;
|
|
||||||
final double maxOptionsHeight;
|
|
||||||
final void Function(String)? onFieldSubmitted;
|
|
||||||
|
|
||||||
const SearchField({
|
|
||||||
required this.textController,
|
|
||||||
required this.activeBang,
|
|
||||||
this.openDirection = OptionsViewOpenDirection.up,
|
|
||||||
this.maxOptionsHeight = defaultMaxOptionsHeight,
|
|
||||||
this.onFieldSubmitted,
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
final incognitoEnabled = ref.watch(
|
|
||||||
settingsRepositoryProvider.select((value) => value.incognitoMode),
|
|
||||||
);
|
|
||||||
|
|
||||||
final optionsStream = ref.watch(searchSuggestionsProvider());
|
|
||||||
|
|
||||||
final focusNode = useFocusNode();
|
|
||||||
|
|
||||||
// final quickAnswer = useListenableSelector(
|
|
||||||
// textController,
|
|
||||||
// () => textController.text.endsWith('?'),
|
|
||||||
// );
|
|
||||||
|
|
||||||
return Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
ExternalResultsAutocomplete<String>(
|
|
||||||
textEditingController: textController,
|
|
||||||
optionsStream: optionsStream,
|
|
||||||
focusNode: focusNode,
|
|
||||||
optionsViewOpenDirection: openDirection,
|
|
||||||
displayStringForOption:
|
|
||||||
// ignore: avoid_redundant_argument_values
|
|
||||||
RawAutocomplete.defaultStringForOption,
|
|
||||||
optionsViewBuilder: (context, onSelected, options) {
|
|
||||||
return _AutocompleteOptions(
|
|
||||||
//Must match RawAutocomplete parent
|
|
||||||
displayStringForOption: RawAutocomplete.defaultStringForOption,
|
|
||||||
onSelected: onSelected,
|
|
||||||
options: options,
|
|
||||||
//Must match RawAutocomplete parent
|
|
||||||
openDirection: openDirection,
|
|
||||||
maxOptionsHeight: maxOptionsHeight,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
onTextChanged: (textEditingValue) {
|
|
||||||
ref
|
|
||||||
.read(searchSuggestionsProvider().notifier)
|
|
||||||
.addQuery(textEditingValue.text);
|
|
||||||
},
|
|
||||||
fieldViewBuilder: (
|
|
||||||
context,
|
|
||||||
textEditingController,
|
|
||||||
focusNode,
|
|
||||||
onFieldSubmitted,
|
|
||||||
) {
|
|
||||||
return TextFormField(
|
|
||||||
controller: textEditingController,
|
|
||||||
enableIMEPersonalizedLearning: !incognitoEnabled,
|
|
||||||
focusNode: focusNode,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
prefixIcon: (activeBang != null)
|
|
||||||
? Padding(
|
|
||||||
padding: const EdgeInsetsDirectional.all(12.0),
|
|
||||||
child: BangIcon(activeBang!, iconSize: 24.0),
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
// label: const Text('Query'),
|
|
||||||
hintText: 'Ask anything...',
|
|
||||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
|
||||||
suffixIcon: SpeechToTextButton(
|
|
||||||
onTextReceived: (data) {
|
|
||||||
textEditingController.text = data.toString();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
|
||||||
validator: (value) {
|
|
||||||
if (value?.isEmpty ?? true) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
onTapOutside: (event) {
|
|
||||||
focusNode.unfocus();
|
|
||||||
},
|
|
||||||
onFieldSubmitted: this.onFieldSubmitted,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
// if (activeBang?.domain.endsWith('kagi.com') == true)
|
|
||||||
// SwitchListTile(
|
|
||||||
// value: quickAnswer,
|
|
||||||
// onChanged: (_) {
|
|
||||||
// if (textController.text.endsWith('?')) {
|
|
||||||
// textController.text = textController.text
|
|
||||||
// .substring(0, textController.text.length - 1);
|
|
||||||
// } else {
|
|
||||||
// textController.text = '${textController.text}?';
|
|
||||||
// }
|
|
||||||
// },
|
|
||||||
// contentPadding: EdgeInsets.zero,
|
|
||||||
// title: const Text('Quick Answer'),
|
|
||||||
// secondary: const Icon(MdiIcons.lightningBolt),
|
|
||||||
// ),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _AutocompleteOptions<T extends Object> extends StatelessWidget {
|
|
||||||
const _AutocompleteOptions({
|
|
||||||
super.key,
|
|
||||||
required this.displayStringForOption,
|
|
||||||
required this.onSelected,
|
|
||||||
required this.openDirection,
|
|
||||||
required this.options,
|
|
||||||
required this.maxOptionsHeight,
|
|
||||||
});
|
|
||||||
|
|
||||||
final AutocompleteOptionToString<T> displayStringForOption;
|
|
||||||
|
|
||||||
final AutocompleteOnSelected<T> onSelected;
|
|
||||||
final OptionsViewOpenDirection openDirection;
|
|
||||||
|
|
||||||
final Iterable<T> options;
|
|
||||||
final double maxOptionsHeight;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final AlignmentDirectional optionsAlignment = switch (openDirection) {
|
|
||||||
OptionsViewOpenDirection.up => AlignmentDirectional.bottomStart,
|
|
||||||
OptionsViewOpenDirection.down => AlignmentDirectional.topStart,
|
|
||||||
};
|
|
||||||
return Align(
|
|
||||||
alignment: optionsAlignment,
|
|
||||||
child: Material(
|
|
||||||
elevation: 4.0,
|
|
||||||
child: ConstrainedBox(
|
|
||||||
constraints: BoxConstraints(maxHeight: maxOptionsHeight),
|
|
||||||
child: ListView.builder(
|
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
shrinkWrap: true,
|
|
||||||
reverse: switch (openDirection) {
|
|
||||||
OptionsViewOpenDirection.up => true,
|
|
||||||
OptionsViewOpenDirection.down => false,
|
|
||||||
},
|
|
||||||
itemCount: options.length,
|
|
||||||
itemBuilder: (BuildContext context, int index) {
|
|
||||||
final T option = options.elementAt(index);
|
|
||||||
return InkWell(
|
|
||||||
onTap: () {
|
|
||||||
onSelected(option);
|
|
||||||
},
|
|
||||||
child: Builder(
|
|
||||||
builder: (BuildContext context) {
|
|
||||||
final bool highlight =
|
|
||||||
AutocompleteHighlightedOption.of(context) == index;
|
|
||||||
// if (highlight) {
|
|
||||||
// SchedulerBinding.instance.addPostFrameCallback(
|
|
||||||
// (Duration timeStamp) async {
|
|
||||||
// await Scrollable.ensureVisible(
|
|
||||||
// context,
|
|
||||||
// alignment: 0.5,
|
|
||||||
// );
|
|
||||||
// },
|
|
||||||
// debugLabel: 'AutocompleteOptions.ensureVisible',
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
return Container(
|
|
||||||
color: highlight ? Theme.of(context).focusColor : null,
|
|
||||||
padding: const EdgeInsets.all(16.0),
|
|
||||||
child: Text(displayStringForOption(option)),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,14 +1,18 @@
|
|||||||
|
import 'package:fading_scroll/fading_scroll.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:lensai/features/bangs/data/models/bang_data.dart';
|
import 'package:lensai/features/bangs/data/models/bang_data.dart';
|
||||||
import 'package:lensai/features/bangs/domain/providers/search.dart';
|
import 'package:lensai/features/bangs/domain/providers/search.dart';
|
||||||
import 'package:lensai/features/bangs/presentation/widgets/bang_icon.dart';
|
import 'package:lensai/features/bangs/presentation/widgets/bang_icon.dart';
|
||||||
import 'package:lensai/features/bangs/presentation/widgets/search_field.dart';
|
import 'package:lensai/features/geckoview/domain/controllers/overlay_dialog.dart';
|
||||||
import 'package:lensai/features/geckoview/domain/repositories/tab.dart';
|
import 'package:lensai/features/geckoview/domain/repositories/tab.dart';
|
||||||
import 'package:lensai/features/geckoview/features/browser/domain/providers.dart';
|
import 'package:lensai/features/geckoview/features/browser/domain/providers.dart';
|
||||||
import 'package:lensai/features/geckoview/features/controllers/overlay_dialog.dart';
|
import 'package:lensai/features/search/domain/providers/search_suggestions.dart';
|
||||||
|
import 'package:lensai/features/search/presentation/widgets/search_field.dart';
|
||||||
|
import 'package:lensai/features/search/presentation/widgets/search_suggestion_list.dart';
|
||||||
|
import 'package:lensai/presentation/hooks/listenable_callback.dart';
|
||||||
import 'package:lensai/presentation/widgets/selectable_chips.dart';
|
import 'package:lensai/presentation/widgets/selectable_chips.dart';
|
||||||
|
|
||||||
class SiteSearch extends HookConsumerWidget {
|
class SiteSearch extends HookConsumerWidget {
|
||||||
@@ -25,6 +29,17 @@ class SiteSearch extends HookConsumerWidget {
|
|||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final formKey = useMemoized(() => GlobalKey<FormState>());
|
final formKey = useMemoized(() => GlobalKey<FormState>());
|
||||||
|
|
||||||
|
final searchTextController = useTextEditingController();
|
||||||
|
|
||||||
|
useListenableCallback(
|
||||||
|
searchTextController,
|
||||||
|
() async {
|
||||||
|
ref
|
||||||
|
.read(searchSuggestionsProvider().notifier)
|
||||||
|
.addQuery(searchTextController.text);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
final selectedBang = ref.watch(
|
final selectedBang = ref.watch(
|
||||||
selectedBangDataProvider(domain: domain)
|
selectedBangDataProvider(domain: domain)
|
||||||
.select((value) => value.valueOrNull),
|
.select((value) => value.valueOrNull),
|
||||||
@@ -32,17 +47,17 @@ class SiteSearch extends HookConsumerWidget {
|
|||||||
|
|
||||||
final activeBang = selectedBang ?? availableBangs.firstOrNull;
|
final activeBang = selectedBang ?? availableBangs.firstOrNull;
|
||||||
|
|
||||||
final textController = useTextEditingController();
|
Future<void> submitSearch(String query) async {
|
||||||
|
|
||||||
Future<void> submitSearch() async {
|
|
||||||
if (activeBang != null && (formKey.currentState?.validate() == true)) {
|
if (activeBang != null && (formKey.currentState?.validate() == true)) {
|
||||||
final searchUri = await ref.read(
|
final searchUri = await ref.read(
|
||||||
triggerBangSearchProvider(activeBang, textController.text).future,
|
triggerBangSearchProvider(activeBang, query).future,
|
||||||
);
|
);
|
||||||
|
|
||||||
await ref.read(tabRepositoryProvider.notifier).addTab(url: searchUri);
|
await ref.read(tabRepositoryProvider.notifier).addTab(url: searchUri);
|
||||||
|
|
||||||
ref.read(overlayDialogControllerProvider.notifier).dismiss();
|
if (context.mounted) {
|
||||||
|
context.pop();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,21 +97,29 @@ class SiteSearch extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
SearchField(
|
SearchField(
|
||||||
textController: textController,
|
textEditingController: searchTextController,
|
||||||
activeBang: activeBang,
|
activeBang: activeBang,
|
||||||
onFieldSubmitted: (_) async {
|
onFieldSubmitted: (_) async {
|
||||||
await submitSearch();
|
await submitSearch(searchTextController.text);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(
|
ConstrainedBox(
|
||||||
height: 12,
|
constraints: const BoxConstraints(maxHeight: 150),
|
||||||
),
|
child: FadingScroll(
|
||||||
SizedBox(
|
fadingSize: 25,
|
||||||
width: double.infinity,
|
builder: (context, controller) {
|
||||||
child: FilledButton.icon(
|
return CustomScrollView(
|
||||||
onPressed: submitSearch,
|
shrinkWrap: true,
|
||||||
label: const Text('Search on Site'),
|
controller: controller,
|
||||||
icon: const Icon(MdiIcons.cloudSearch),
|
slivers: [
|
||||||
|
SearchSuggestionList(
|
||||||
|
searchTextController: searchTextController,
|
||||||
|
submitSearch: submitSearch,
|
||||||
|
showHistory: false,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:lensai/features/chat/features/chat_store/data/models/chat_metadata.dart';
|
||||||
|
import 'package:drift/drift.dart';
|
||||||
|
|
||||||
|
class ChatMetadataConverter extends TypeConverter<ChatMetadata, String> {
|
||||||
|
const ChatMetadataConverter();
|
||||||
|
|
||||||
|
@override
|
||||||
|
ChatMetadata fromSql(String fromDb) {
|
||||||
|
return ChatMetadata.fromJson(jsonDecode(fromDb) as Map<String, dynamic>);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toSql(ChatMetadata value) {
|
||||||
|
return jsonEncode(value.toJson());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:lensai/features/chat/features/chat_store/data/database/database.dart';
|
||||||
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:flutter_chat_core/flutter_chat_core.dart';
|
||||||
|
import 'package:lexo_rank/lexo_rank.dart';
|
||||||
|
|
||||||
|
part 'messages.g.dart';
|
||||||
|
|
||||||
|
@DriftAccessor()
|
||||||
|
class MessagesDao extends DatabaseAccessor<ChatDatabase>
|
||||||
|
with _$MessagesDaoMixin {
|
||||||
|
MessagesDao(super.db);
|
||||||
|
|
||||||
|
Selectable<int> messageCount({required String chatId}) {
|
||||||
|
return db.chatMessage.count(
|
||||||
|
where: (row) => row.chatId.equals(chatId),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Selectable<ChatMessageData> chatMessages({required String chatId}) {
|
||||||
|
return db.chatMessage.select()
|
||||||
|
..where((t) => t.chatId.equals(chatId))
|
||||||
|
..orderBy([(row) => OrderingTerm(expression: row.orderKey)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Selectable<bool> messageExists(String messageId) {
|
||||||
|
final existsExpression = existsQuery(
|
||||||
|
db.chatMessage.select()..where((row) => row.messageId.equals(messageId)),
|
||||||
|
);
|
||||||
|
|
||||||
|
return db.selectExpressions([existsExpression]).map(
|
||||||
|
(row) => row.read(existsExpression)!,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
SingleOrNullSelectable<int> messageIndex({
|
||||||
|
required String chatId,
|
||||||
|
required String messageId,
|
||||||
|
}) {
|
||||||
|
return db
|
||||||
|
.messageIndex(chatId: chatId, messageId: messageId)
|
||||||
|
.map((rowId) => rowId - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<int?> insertMessage({
|
||||||
|
required String chatId,
|
||||||
|
required Message message,
|
||||||
|
int? index,
|
||||||
|
}) async {
|
||||||
|
if (await messageExists(message.id).getSingle()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return db.transaction(
|
||||||
|
() async {
|
||||||
|
final String orderKey;
|
||||||
|
if (index != null) {
|
||||||
|
if (index <= 0) {
|
||||||
|
orderKey =
|
||||||
|
await db.leadingOrderKey(bucket: 0, chatId: chatId).getSingle();
|
||||||
|
} else {
|
||||||
|
orderKey = await db
|
||||||
|
.orderKeyBeforeIndex(chatId: chatId, rowIdx: index + 1)
|
||||||
|
.getSingle();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
orderKey =
|
||||||
|
await db.trailingOrderKey(bucket: 0, chatId: chatId).getSingle();
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.chatMessage.insertOne(
|
||||||
|
ChatMessageCompanion.insert(
|
||||||
|
messageId: message.id,
|
||||||
|
chatId: chatId,
|
||||||
|
orderKey: orderKey,
|
||||||
|
messageJson: jsonEncode(message.toJson()),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final rowIdx = await db
|
||||||
|
.messageIndex(chatId: chatId, messageId: message.id)
|
||||||
|
.getSingle();
|
||||||
|
|
||||||
|
return rowIdx - 1;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<int> removeMessage({required String messageId}) {
|
||||||
|
return db.chatMessage.deleteWhere((row) => row.messageId.equals(messageId));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<int> updateMessage({
|
||||||
|
required String messageId,
|
||||||
|
required Message message,
|
||||||
|
}) {
|
||||||
|
return (db.chatMessage.update()
|
||||||
|
..where((row) => row.messageId.equals(messageId)))
|
||||||
|
.write(
|
||||||
|
ChatMessageCompanion(
|
||||||
|
messageJson: Value(jsonEncode(message.toJson())),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<int> deleteChatMessages({required String chatId}) {
|
||||||
|
return (db.chatMessage.delete()..where((t) => t.chatId.equals(chatId)))
|
||||||
|
.go();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> replaceChatMessages({
|
||||||
|
required String chatId,
|
||||||
|
required List<Message> messages,
|
||||||
|
}) async {
|
||||||
|
return db.transaction(() async {
|
||||||
|
await deleteChatMessages(chatId: chatId);
|
||||||
|
|
||||||
|
var currentOrderKey =
|
||||||
|
await db.leadingOrderKey(bucket: 0, chatId: chatId).getSingle();
|
||||||
|
|
||||||
|
await db.chatMessage.insertAll(
|
||||||
|
messages.map(
|
||||||
|
(message) {
|
||||||
|
final insertable = ChatMessageCompanion.insert(
|
||||||
|
messageId: message.id,
|
||||||
|
chatId: chatId,
|
||||||
|
messageJson: jsonEncode(message.toJson()),
|
||||||
|
orderKey: currentOrderKey,
|
||||||
|
// timestamp: DateTime.now(),
|
||||||
|
);
|
||||||
|
|
||||||
|
currentOrderKey = LexoRank.parse(currentOrderKey).genPrev().value;
|
||||||
|
|
||||||
|
return insertable;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'messages.dart';
|
||||||
|
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
mixin _$MessagesDaoMixin on DatabaseAccessor<ChatDatabase> {}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import 'package:lensai/features/chat/features/chat_store/data/database/database.dart';
|
||||||
|
import 'package:lensai/features/chat/features/chat_store/data/models/chat_metadata.dart';
|
||||||
|
import 'package:drift/drift.dart';
|
||||||
|
|
||||||
|
part 'metadata.g.dart';
|
||||||
|
|
||||||
|
@DriftAccessor()
|
||||||
|
class MetadataDao extends DatabaseAccessor<ChatDatabase>
|
||||||
|
with _$MetadataDaoMixin {
|
||||||
|
MetadataDao(super.db);
|
||||||
|
|
||||||
|
SingleOrNullSelectable<ChatMetadata> chatMetadata(String chatId) {
|
||||||
|
final statement = db.chatData.selectOnly()
|
||||||
|
..addColumns([db.chatData.metadata])
|
||||||
|
..where(db.chatData.chatId.equals(chatId));
|
||||||
|
|
||||||
|
return statement.map((row) => row.readWithConverter(db.chatData.metadata)!);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> updateChatMetadata(String chatId, ChatMetadata metadata) {
|
||||||
|
return db.chatData.insertOne(
|
||||||
|
ChatDataCompanion.insert(chatId: chatId, metadata: metadata),
|
||||||
|
mode: InsertMode.insertOrReplace,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'metadata.dart';
|
||||||
|
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
mixin _$MetadataDaoMixin on DatabaseAccessor<ChatDatabase> {}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import 'package:lensai/features/chat/features/chat_store/data/database/converters/chat_metadata_converter.dart';
|
||||||
|
import 'package:lensai/features/chat/features/chat_store/data/database/daos/messages.dart';
|
||||||
|
import 'package:lensai/features/chat/features/chat_store/data/database/daos/metadata.dart';
|
||||||
|
import 'package:lensai/features/chat/features/chat_store/data/models/chat_metadata.dart';
|
||||||
|
|
||||||
|
import 'package:drift/drift.dart';
|
||||||
|
|
||||||
|
part 'database.g.dart';
|
||||||
|
|
||||||
|
@DriftDatabase(include: {'database.drift'}, daos: [MessagesDao, MetadataDao])
|
||||||
|
class ChatDatabase extends _$ChatDatabase {
|
||||||
|
ChatDatabase(super.e);
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get schemaVersion => 1;
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import 'package:lensai/features/chat/features/chat_store/data/database/converters/chat_metadata_converter.dart';
|
||||||
|
|
||||||
|
CREATE TABLE chat_data (
|
||||||
|
chat_id TEXT PRIMARY KEY NOT NULL,
|
||||||
|
metadata TEXT NOT NULL MAPPED BY `const ChatMetadataConverter()`
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE chat_message (
|
||||||
|
message_id TEXT PRIMARY KEY NOT NULL,
|
||||||
|
chat_id TEXT NOT NULL,
|
||||||
|
order_key TEXT NOT NULL,
|
||||||
|
message_json TEXT NOT NULL
|
||||||
|
);
|
||||||
|
CREATE UNIQUE INDEX group_order_idx on chat_message (chat_id, order_key);
|
||||||
|
|
||||||
|
leadingOrderKey:
|
||||||
|
SELECT lexo_rank_previous(
|
||||||
|
:bucket,
|
||||||
|
(
|
||||||
|
SELECT order_key
|
||||||
|
FROM chat_message
|
||||||
|
WHERE chat_id = :chat_id
|
||||||
|
ORDER BY order_key
|
||||||
|
LIMIT 1
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
trailingOrderKey:
|
||||||
|
SELECT lexo_rank_next(
|
||||||
|
:bucket,
|
||||||
|
(
|
||||||
|
SELECT order_key
|
||||||
|
FROM chat_message
|
||||||
|
WHERE chat_id = :chat_id
|
||||||
|
ORDER BY order_key DESC
|
||||||
|
LIMIT 1
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
orderKeyBeforeIndex:
|
||||||
|
WITH ordered_table AS
|
||||||
|
(
|
||||||
|
SELECT message_id,
|
||||||
|
order_key,
|
||||||
|
LEAD(order_key) OVER (ORDER BY order_key) AS next_order_key,
|
||||||
|
ROW_NUMBER() OVER (ORDER BY order_key) AS row_idx,
|
||||||
|
COUNT(*) OVER () as total_rows
|
||||||
|
FROM chat_message
|
||||||
|
WHERE chat_id = :chat_id
|
||||||
|
)
|
||||||
|
SELECT lexo_rank_reorder_after(order_key, next_order_key)
|
||||||
|
FROM ordered_table
|
||||||
|
WHERE row_idx = MIN(:row_idx, total_rows);
|
||||||
|
|
||||||
|
messageIndex:
|
||||||
|
WITH ordered_table AS
|
||||||
|
(
|
||||||
|
SELECT message_id,
|
||||||
|
ROW_NUMBER() OVER (ORDER BY order_key) AS row_idx
|
||||||
|
FROM chat_message
|
||||||
|
WHERE chat_id = :chat_id
|
||||||
|
)
|
||||||
|
SELECT row_idx
|
||||||
|
FROM ordered_table
|
||||||
|
WHERE message_id = :message_id;
|
||||||
@@ -0,0 +1,764 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'database.dart';
|
||||||
|
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
class ChatData extends Table with TableInfo<ChatData, ChatDataData> {
|
||||||
|
@override
|
||||||
|
final GeneratedDatabase attachedDatabase;
|
||||||
|
final String? _alias;
|
||||||
|
ChatData(this.attachedDatabase, [this._alias]);
|
||||||
|
late final GeneratedColumn<String> chatId = GeneratedColumn<String>(
|
||||||
|
'chat_id', aliasedName, false,
|
||||||
|
type: DriftSqlType.string,
|
||||||
|
requiredDuringInsert: true,
|
||||||
|
$customConstraints: 'PRIMARY KEY NOT NULL');
|
||||||
|
late final GeneratedColumnWithTypeConverter<ChatMetadata, String> metadata =
|
||||||
|
GeneratedColumn<String>('metadata', aliasedName, false,
|
||||||
|
type: DriftSqlType.string,
|
||||||
|
requiredDuringInsert: true,
|
||||||
|
$customConstraints: 'NOT NULL')
|
||||||
|
.withConverter<ChatMetadata>(ChatData.$convertermetadata);
|
||||||
|
@override
|
||||||
|
List<GeneratedColumn> get $columns => [chatId, metadata];
|
||||||
|
@override
|
||||||
|
String get aliasedName => _alias ?? actualTableName;
|
||||||
|
@override
|
||||||
|
String get actualTableName => $name;
|
||||||
|
static const String $name = 'chat_data';
|
||||||
|
@override
|
||||||
|
Set<GeneratedColumn> get $primaryKey => {chatId};
|
||||||
|
@override
|
||||||
|
ChatDataData map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||||
|
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
|
||||||
|
return ChatDataData(
|
||||||
|
chatId: attachedDatabase.typeMapping
|
||||||
|
.read(DriftSqlType.string, data['${effectivePrefix}chat_id'])!,
|
||||||
|
metadata: ChatData.$convertermetadata.fromSql(attachedDatabase.typeMapping
|
||||||
|
.read(DriftSqlType.string, data['${effectivePrefix}metadata'])!),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
ChatData createAlias(String alias) {
|
||||||
|
return ChatData(attachedDatabase, alias);
|
||||||
|
}
|
||||||
|
|
||||||
|
static TypeConverter<ChatMetadata, String> $convertermetadata =
|
||||||
|
const ChatMetadataConverter();
|
||||||
|
@override
|
||||||
|
bool get dontWriteConstraints => true;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ChatDataData extends DataClass implements Insertable<ChatDataData> {
|
||||||
|
final String chatId;
|
||||||
|
final ChatMetadata metadata;
|
||||||
|
const ChatDataData({required this.chatId, required this.metadata});
|
||||||
|
@override
|
||||||
|
Map<String, Expression> toColumns(bool nullToAbsent) {
|
||||||
|
final map = <String, Expression>{};
|
||||||
|
map['chat_id'] = Variable<String>(chatId);
|
||||||
|
{
|
||||||
|
map['metadata'] =
|
||||||
|
Variable<String>(ChatData.$convertermetadata.toSql(metadata));
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
factory ChatDataData.fromJson(Map<String, dynamic> json,
|
||||||
|
{ValueSerializer? serializer}) {
|
||||||
|
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||||
|
return ChatDataData(
|
||||||
|
chatId: serializer.fromJson<String>(json['chat_id']),
|
||||||
|
metadata: serializer.fromJson<ChatMetadata>(json['metadata']),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
|
||||||
|
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||||
|
return <String, dynamic>{
|
||||||
|
'chat_id': serializer.toJson<String>(chatId),
|
||||||
|
'metadata': serializer.toJson<ChatMetadata>(metadata),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
ChatDataData copyWith({String? chatId, ChatMetadata? metadata}) =>
|
||||||
|
ChatDataData(
|
||||||
|
chatId: chatId ?? this.chatId,
|
||||||
|
metadata: metadata ?? this.metadata,
|
||||||
|
);
|
||||||
|
ChatDataData copyWithCompanion(ChatDataCompanion data) {
|
||||||
|
return ChatDataData(
|
||||||
|
chatId: data.chatId.present ? data.chatId.value : this.chatId,
|
||||||
|
metadata: data.metadata.present ? data.metadata.value : this.metadata,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return (StringBuffer('ChatDataData(')
|
||||||
|
..write('chatId: $chatId, ')
|
||||||
|
..write('metadata: $metadata')
|
||||||
|
..write(')'))
|
||||||
|
.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(chatId, metadata);
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) =>
|
||||||
|
identical(this, other) ||
|
||||||
|
(other is ChatDataData &&
|
||||||
|
other.chatId == this.chatId &&
|
||||||
|
other.metadata == this.metadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
class ChatDataCompanion extends UpdateCompanion<ChatDataData> {
|
||||||
|
final Value<String> chatId;
|
||||||
|
final Value<ChatMetadata> metadata;
|
||||||
|
final Value<int> rowid;
|
||||||
|
const ChatDataCompanion({
|
||||||
|
this.chatId = const Value.absent(),
|
||||||
|
this.metadata = const Value.absent(),
|
||||||
|
this.rowid = const Value.absent(),
|
||||||
|
});
|
||||||
|
ChatDataCompanion.insert({
|
||||||
|
required String chatId,
|
||||||
|
required ChatMetadata metadata,
|
||||||
|
this.rowid = const Value.absent(),
|
||||||
|
}) : chatId = Value(chatId),
|
||||||
|
metadata = Value(metadata);
|
||||||
|
static Insertable<ChatDataData> custom({
|
||||||
|
Expression<String>? chatId,
|
||||||
|
Expression<String>? metadata,
|
||||||
|
Expression<int>? rowid,
|
||||||
|
}) {
|
||||||
|
return RawValuesInsertable({
|
||||||
|
if (chatId != null) 'chat_id': chatId,
|
||||||
|
if (metadata != null) 'metadata': metadata,
|
||||||
|
if (rowid != null) 'rowid': rowid,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ChatDataCompanion copyWith(
|
||||||
|
{Value<String>? chatId,
|
||||||
|
Value<ChatMetadata>? metadata,
|
||||||
|
Value<int>? rowid}) {
|
||||||
|
return ChatDataCompanion(
|
||||||
|
chatId: chatId ?? this.chatId,
|
||||||
|
metadata: metadata ?? this.metadata,
|
||||||
|
rowid: rowid ?? this.rowid,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, Expression> toColumns(bool nullToAbsent) {
|
||||||
|
final map = <String, Expression>{};
|
||||||
|
if (chatId.present) {
|
||||||
|
map['chat_id'] = Variable<String>(chatId.value);
|
||||||
|
}
|
||||||
|
if (metadata.present) {
|
||||||
|
map['metadata'] =
|
||||||
|
Variable<String>(ChatData.$convertermetadata.toSql(metadata.value));
|
||||||
|
}
|
||||||
|
if (rowid.present) {
|
||||||
|
map['rowid'] = Variable<int>(rowid.value);
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return (StringBuffer('ChatDataCompanion(')
|
||||||
|
..write('chatId: $chatId, ')
|
||||||
|
..write('metadata: $metadata, ')
|
||||||
|
..write('rowid: $rowid')
|
||||||
|
..write(')'))
|
||||||
|
.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ChatMessage extends Table with TableInfo<ChatMessage, ChatMessageData> {
|
||||||
|
@override
|
||||||
|
final GeneratedDatabase attachedDatabase;
|
||||||
|
final String? _alias;
|
||||||
|
ChatMessage(this.attachedDatabase, [this._alias]);
|
||||||
|
late final GeneratedColumn<String> messageId = GeneratedColumn<String>(
|
||||||
|
'message_id', aliasedName, false,
|
||||||
|
type: DriftSqlType.string,
|
||||||
|
requiredDuringInsert: true,
|
||||||
|
$customConstraints: 'PRIMARY KEY NOT NULL');
|
||||||
|
late final GeneratedColumn<String> chatId = GeneratedColumn<String>(
|
||||||
|
'chat_id', aliasedName, false,
|
||||||
|
type: DriftSqlType.string,
|
||||||
|
requiredDuringInsert: true,
|
||||||
|
$customConstraints: 'NOT NULL');
|
||||||
|
late final GeneratedColumn<String> orderKey = GeneratedColumn<String>(
|
||||||
|
'order_key', aliasedName, false,
|
||||||
|
type: DriftSqlType.string,
|
||||||
|
requiredDuringInsert: true,
|
||||||
|
$customConstraints: 'NOT NULL');
|
||||||
|
late final GeneratedColumn<String> messageJson = GeneratedColumn<String>(
|
||||||
|
'message_json', aliasedName, false,
|
||||||
|
type: DriftSqlType.string,
|
||||||
|
requiredDuringInsert: true,
|
||||||
|
$customConstraints: 'NOT NULL');
|
||||||
|
@override
|
||||||
|
List<GeneratedColumn> get $columns =>
|
||||||
|
[messageId, chatId, orderKey, messageJson];
|
||||||
|
@override
|
||||||
|
String get aliasedName => _alias ?? actualTableName;
|
||||||
|
@override
|
||||||
|
String get actualTableName => $name;
|
||||||
|
static const String $name = 'chat_message';
|
||||||
|
@override
|
||||||
|
Set<GeneratedColumn> get $primaryKey => {messageId};
|
||||||
|
@override
|
||||||
|
ChatMessageData map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||||
|
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
|
||||||
|
return ChatMessageData(
|
||||||
|
messageId: attachedDatabase.typeMapping
|
||||||
|
.read(DriftSqlType.string, data['${effectivePrefix}message_id'])!,
|
||||||
|
chatId: attachedDatabase.typeMapping
|
||||||
|
.read(DriftSqlType.string, data['${effectivePrefix}chat_id'])!,
|
||||||
|
orderKey: attachedDatabase.typeMapping
|
||||||
|
.read(DriftSqlType.string, data['${effectivePrefix}order_key'])!,
|
||||||
|
messageJson: attachedDatabase.typeMapping
|
||||||
|
.read(DriftSqlType.string, data['${effectivePrefix}message_json'])!,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
ChatMessage createAlias(String alias) {
|
||||||
|
return ChatMessage(attachedDatabase, alias);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get dontWriteConstraints => true;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ChatMessageData extends DataClass implements Insertable<ChatMessageData> {
|
||||||
|
final String messageId;
|
||||||
|
final String chatId;
|
||||||
|
final String orderKey;
|
||||||
|
final String messageJson;
|
||||||
|
const ChatMessageData(
|
||||||
|
{required this.messageId,
|
||||||
|
required this.chatId,
|
||||||
|
required this.orderKey,
|
||||||
|
required this.messageJson});
|
||||||
|
@override
|
||||||
|
Map<String, Expression> toColumns(bool nullToAbsent) {
|
||||||
|
final map = <String, Expression>{};
|
||||||
|
map['message_id'] = Variable<String>(messageId);
|
||||||
|
map['chat_id'] = Variable<String>(chatId);
|
||||||
|
map['order_key'] = Variable<String>(orderKey);
|
||||||
|
map['message_json'] = Variable<String>(messageJson);
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
factory ChatMessageData.fromJson(Map<String, dynamic> json,
|
||||||
|
{ValueSerializer? serializer}) {
|
||||||
|
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||||
|
return ChatMessageData(
|
||||||
|
messageId: serializer.fromJson<String>(json['message_id']),
|
||||||
|
chatId: serializer.fromJson<String>(json['chat_id']),
|
||||||
|
orderKey: serializer.fromJson<String>(json['order_key']),
|
||||||
|
messageJson: serializer.fromJson<String>(json['message_json']),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
|
||||||
|
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||||
|
return <String, dynamic>{
|
||||||
|
'message_id': serializer.toJson<String>(messageId),
|
||||||
|
'chat_id': serializer.toJson<String>(chatId),
|
||||||
|
'order_key': serializer.toJson<String>(orderKey),
|
||||||
|
'message_json': serializer.toJson<String>(messageJson),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
ChatMessageData copyWith(
|
||||||
|
{String? messageId,
|
||||||
|
String? chatId,
|
||||||
|
String? orderKey,
|
||||||
|
String? messageJson}) =>
|
||||||
|
ChatMessageData(
|
||||||
|
messageId: messageId ?? this.messageId,
|
||||||
|
chatId: chatId ?? this.chatId,
|
||||||
|
orderKey: orderKey ?? this.orderKey,
|
||||||
|
messageJson: messageJson ?? this.messageJson,
|
||||||
|
);
|
||||||
|
ChatMessageData copyWithCompanion(ChatMessageCompanion data) {
|
||||||
|
return ChatMessageData(
|
||||||
|
messageId: data.messageId.present ? data.messageId.value : this.messageId,
|
||||||
|
chatId: data.chatId.present ? data.chatId.value : this.chatId,
|
||||||
|
orderKey: data.orderKey.present ? data.orderKey.value : this.orderKey,
|
||||||
|
messageJson:
|
||||||
|
data.messageJson.present ? data.messageJson.value : this.messageJson,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return (StringBuffer('ChatMessageData(')
|
||||||
|
..write('messageId: $messageId, ')
|
||||||
|
..write('chatId: $chatId, ')
|
||||||
|
..write('orderKey: $orderKey, ')
|
||||||
|
..write('messageJson: $messageJson')
|
||||||
|
..write(')'))
|
||||||
|
.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(messageId, chatId, orderKey, messageJson);
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) =>
|
||||||
|
identical(this, other) ||
|
||||||
|
(other is ChatMessageData &&
|
||||||
|
other.messageId == this.messageId &&
|
||||||
|
other.chatId == this.chatId &&
|
||||||
|
other.orderKey == this.orderKey &&
|
||||||
|
other.messageJson == this.messageJson);
|
||||||
|
}
|
||||||
|
|
||||||
|
class ChatMessageCompanion extends UpdateCompanion<ChatMessageData> {
|
||||||
|
final Value<String> messageId;
|
||||||
|
final Value<String> chatId;
|
||||||
|
final Value<String> orderKey;
|
||||||
|
final Value<String> messageJson;
|
||||||
|
final Value<int> rowid;
|
||||||
|
const ChatMessageCompanion({
|
||||||
|
this.messageId = const Value.absent(),
|
||||||
|
this.chatId = const Value.absent(),
|
||||||
|
this.orderKey = const Value.absent(),
|
||||||
|
this.messageJson = const Value.absent(),
|
||||||
|
this.rowid = const Value.absent(),
|
||||||
|
});
|
||||||
|
ChatMessageCompanion.insert({
|
||||||
|
required String messageId,
|
||||||
|
required String chatId,
|
||||||
|
required String orderKey,
|
||||||
|
required String messageJson,
|
||||||
|
this.rowid = const Value.absent(),
|
||||||
|
}) : messageId = Value(messageId),
|
||||||
|
chatId = Value(chatId),
|
||||||
|
orderKey = Value(orderKey),
|
||||||
|
messageJson = Value(messageJson);
|
||||||
|
static Insertable<ChatMessageData> custom({
|
||||||
|
Expression<String>? messageId,
|
||||||
|
Expression<String>? chatId,
|
||||||
|
Expression<String>? orderKey,
|
||||||
|
Expression<String>? messageJson,
|
||||||
|
Expression<int>? rowid,
|
||||||
|
}) {
|
||||||
|
return RawValuesInsertable({
|
||||||
|
if (messageId != null) 'message_id': messageId,
|
||||||
|
if (chatId != null) 'chat_id': chatId,
|
||||||
|
if (orderKey != null) 'order_key': orderKey,
|
||||||
|
if (messageJson != null) 'message_json': messageJson,
|
||||||
|
if (rowid != null) 'rowid': rowid,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ChatMessageCompanion copyWith(
|
||||||
|
{Value<String>? messageId,
|
||||||
|
Value<String>? chatId,
|
||||||
|
Value<String>? orderKey,
|
||||||
|
Value<String>? messageJson,
|
||||||
|
Value<int>? rowid}) {
|
||||||
|
return ChatMessageCompanion(
|
||||||
|
messageId: messageId ?? this.messageId,
|
||||||
|
chatId: chatId ?? this.chatId,
|
||||||
|
orderKey: orderKey ?? this.orderKey,
|
||||||
|
messageJson: messageJson ?? this.messageJson,
|
||||||
|
rowid: rowid ?? this.rowid,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, Expression> toColumns(bool nullToAbsent) {
|
||||||
|
final map = <String, Expression>{};
|
||||||
|
if (messageId.present) {
|
||||||
|
map['message_id'] = Variable<String>(messageId.value);
|
||||||
|
}
|
||||||
|
if (chatId.present) {
|
||||||
|
map['chat_id'] = Variable<String>(chatId.value);
|
||||||
|
}
|
||||||
|
if (orderKey.present) {
|
||||||
|
map['order_key'] = Variable<String>(orderKey.value);
|
||||||
|
}
|
||||||
|
if (messageJson.present) {
|
||||||
|
map['message_json'] = Variable<String>(messageJson.value);
|
||||||
|
}
|
||||||
|
if (rowid.present) {
|
||||||
|
map['rowid'] = Variable<int>(rowid.value);
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return (StringBuffer('ChatMessageCompanion(')
|
||||||
|
..write('messageId: $messageId, ')
|
||||||
|
..write('chatId: $chatId, ')
|
||||||
|
..write('orderKey: $orderKey, ')
|
||||||
|
..write('messageJson: $messageJson, ')
|
||||||
|
..write('rowid: $rowid')
|
||||||
|
..write(')'))
|
||||||
|
.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class _$ChatDatabase extends GeneratedDatabase {
|
||||||
|
_$ChatDatabase(QueryExecutor e) : super(e);
|
||||||
|
$ChatDatabaseManager get managers => $ChatDatabaseManager(this);
|
||||||
|
late final ChatData chatData = ChatData(this);
|
||||||
|
late final ChatMessage chatMessage = ChatMessage(this);
|
||||||
|
late final Index groupOrderIdx = Index('group_order_idx',
|
||||||
|
'CREATE UNIQUE INDEX group_order_idx ON chat_message (chat_id, order_key)');
|
||||||
|
late final MessagesDao messagesDao = MessagesDao(this as ChatDatabase);
|
||||||
|
late final MetadataDao metadataDao = MetadataDao(this as ChatDatabase);
|
||||||
|
Selectable<String> leadingOrderKey(
|
||||||
|
{required int bucket, required String chatId}) {
|
||||||
|
return customSelect(
|
||||||
|
'SELECT lexo_rank_previous(?1, (SELECT order_key FROM chat_message WHERE chat_id = ?2 ORDER BY order_key LIMIT 1)) AS _c0',
|
||||||
|
variables: [
|
||||||
|
Variable<int>(bucket),
|
||||||
|
Variable<String>(chatId)
|
||||||
|
],
|
||||||
|
readsFrom: {
|
||||||
|
chatMessage,
|
||||||
|
}).map((QueryRow row) => row.read<String>('_c0'));
|
||||||
|
}
|
||||||
|
|
||||||
|
Selectable<String> trailingOrderKey(
|
||||||
|
{required int bucket, required String chatId}) {
|
||||||
|
return customSelect(
|
||||||
|
'SELECT lexo_rank_next(?1, (SELECT order_key FROM chat_message WHERE chat_id = ?2 ORDER BY order_key DESC LIMIT 1)) AS _c0',
|
||||||
|
variables: [
|
||||||
|
Variable<int>(bucket),
|
||||||
|
Variable<String>(chatId)
|
||||||
|
],
|
||||||
|
readsFrom: {
|
||||||
|
chatMessage,
|
||||||
|
}).map((QueryRow row) => row.read<String>('_c0'));
|
||||||
|
}
|
||||||
|
|
||||||
|
Selectable<String> orderKeyBeforeIndex(
|
||||||
|
{required String chatId, required int rowIdx}) {
|
||||||
|
return customSelect(
|
||||||
|
'WITH ordered_table AS (SELECT message_id, order_key, LEAD(order_key)OVER (ORDER BY order_key RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW EXCLUDE NO OTHERS) AS next_order_key, ROW_NUMBER()OVER (ORDER BY order_key RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW EXCLUDE NO OTHERS) AS row_idx, COUNT(*)OVER (RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW EXCLUDE NO OTHERS) AS total_rows FROM chat_message WHERE chat_id = ?1) SELECT lexo_rank_reorder_after(order_key, next_order_key) AS _c0 FROM ordered_table WHERE row_idx = MIN(?2, total_rows)',
|
||||||
|
variables: [
|
||||||
|
Variable<String>(chatId),
|
||||||
|
Variable<int>(rowIdx)
|
||||||
|
],
|
||||||
|
readsFrom: {
|
||||||
|
chatMessage,
|
||||||
|
}).map((QueryRow row) => row.read<String>('_c0'));
|
||||||
|
}
|
||||||
|
|
||||||
|
Selectable<int> messageIndex(
|
||||||
|
{required String chatId, required String messageId}) {
|
||||||
|
return customSelect(
|
||||||
|
'WITH ordered_table AS (SELECT message_id, ROW_NUMBER()OVER (ORDER BY order_key RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW EXCLUDE NO OTHERS) AS row_idx FROM chat_message WHERE chat_id = ?1) SELECT row_idx FROM ordered_table WHERE message_id = ?2',
|
||||||
|
variables: [
|
||||||
|
Variable<String>(chatId),
|
||||||
|
Variable<String>(messageId)
|
||||||
|
],
|
||||||
|
readsFrom: {
|
||||||
|
chatMessage,
|
||||||
|
}).map((QueryRow row) => row.read<int>('row_idx'));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Iterable<TableInfo<Table, Object?>> get allTables =>
|
||||||
|
allSchemaEntities.whereType<TableInfo<Table, Object?>>();
|
||||||
|
@override
|
||||||
|
List<DatabaseSchemaEntity> get allSchemaEntities =>
|
||||||
|
[chatData, chatMessage, groupOrderIdx];
|
||||||
|
}
|
||||||
|
|
||||||
|
typedef $ChatDataCreateCompanionBuilder = ChatDataCompanion Function({
|
||||||
|
required String chatId,
|
||||||
|
required ChatMetadata metadata,
|
||||||
|
Value<int> rowid,
|
||||||
|
});
|
||||||
|
typedef $ChatDataUpdateCompanionBuilder = ChatDataCompanion Function({
|
||||||
|
Value<String> chatId,
|
||||||
|
Value<ChatMetadata> metadata,
|
||||||
|
Value<int> rowid,
|
||||||
|
});
|
||||||
|
|
||||||
|
class $ChatDataFilterComposer extends Composer<_$ChatDatabase, ChatData> {
|
||||||
|
$ChatDataFilterComposer({
|
||||||
|
required super.$db,
|
||||||
|
required super.$table,
|
||||||
|
super.joinBuilder,
|
||||||
|
super.$addJoinBuilderToRootComposer,
|
||||||
|
super.$removeJoinBuilderFromRootComposer,
|
||||||
|
});
|
||||||
|
ColumnFilters<String> get chatId => $composableBuilder(
|
||||||
|
column: $table.chatId, builder: (column) => ColumnFilters(column));
|
||||||
|
|
||||||
|
ColumnWithTypeConverterFilters<ChatMetadata, ChatMetadata, String>
|
||||||
|
get metadata => $composableBuilder(
|
||||||
|
column: $table.metadata,
|
||||||
|
builder: (column) => ColumnWithTypeConverterFilters(column));
|
||||||
|
}
|
||||||
|
|
||||||
|
class $ChatDataOrderingComposer extends Composer<_$ChatDatabase, ChatData> {
|
||||||
|
$ChatDataOrderingComposer({
|
||||||
|
required super.$db,
|
||||||
|
required super.$table,
|
||||||
|
super.joinBuilder,
|
||||||
|
super.$addJoinBuilderToRootComposer,
|
||||||
|
super.$removeJoinBuilderFromRootComposer,
|
||||||
|
});
|
||||||
|
ColumnOrderings<String> get chatId => $composableBuilder(
|
||||||
|
column: $table.chatId, builder: (column) => ColumnOrderings(column));
|
||||||
|
|
||||||
|
ColumnOrderings<String> get metadata => $composableBuilder(
|
||||||
|
column: $table.metadata, builder: (column) => ColumnOrderings(column));
|
||||||
|
}
|
||||||
|
|
||||||
|
class $ChatDataAnnotationComposer extends Composer<_$ChatDatabase, ChatData> {
|
||||||
|
$ChatDataAnnotationComposer({
|
||||||
|
required super.$db,
|
||||||
|
required super.$table,
|
||||||
|
super.joinBuilder,
|
||||||
|
super.$addJoinBuilderToRootComposer,
|
||||||
|
super.$removeJoinBuilderFromRootComposer,
|
||||||
|
});
|
||||||
|
GeneratedColumn<String> get chatId =>
|
||||||
|
$composableBuilder(column: $table.chatId, builder: (column) => column);
|
||||||
|
|
||||||
|
GeneratedColumnWithTypeConverter<ChatMetadata, String> get metadata =>
|
||||||
|
$composableBuilder(column: $table.metadata, builder: (column) => column);
|
||||||
|
}
|
||||||
|
|
||||||
|
class $ChatDataTableManager extends RootTableManager<
|
||||||
|
_$ChatDatabase,
|
||||||
|
ChatData,
|
||||||
|
ChatDataData,
|
||||||
|
$ChatDataFilterComposer,
|
||||||
|
$ChatDataOrderingComposer,
|
||||||
|
$ChatDataAnnotationComposer,
|
||||||
|
$ChatDataCreateCompanionBuilder,
|
||||||
|
$ChatDataUpdateCompanionBuilder,
|
||||||
|
(ChatDataData, BaseReferences<_$ChatDatabase, ChatData, ChatDataData>),
|
||||||
|
ChatDataData,
|
||||||
|
PrefetchHooks Function()> {
|
||||||
|
$ChatDataTableManager(_$ChatDatabase db, ChatData table)
|
||||||
|
: super(TableManagerState(
|
||||||
|
db: db,
|
||||||
|
table: table,
|
||||||
|
createFilteringComposer: () =>
|
||||||
|
$ChatDataFilterComposer($db: db, $table: table),
|
||||||
|
createOrderingComposer: () =>
|
||||||
|
$ChatDataOrderingComposer($db: db, $table: table),
|
||||||
|
createComputedFieldComposer: () =>
|
||||||
|
$ChatDataAnnotationComposer($db: db, $table: table),
|
||||||
|
updateCompanionCallback: ({
|
||||||
|
Value<String> chatId = const Value.absent(),
|
||||||
|
Value<ChatMetadata> metadata = const Value.absent(),
|
||||||
|
Value<int> rowid = const Value.absent(),
|
||||||
|
}) =>
|
||||||
|
ChatDataCompanion(
|
||||||
|
chatId: chatId,
|
||||||
|
metadata: metadata,
|
||||||
|
rowid: rowid,
|
||||||
|
),
|
||||||
|
createCompanionCallback: ({
|
||||||
|
required String chatId,
|
||||||
|
required ChatMetadata metadata,
|
||||||
|
Value<int> rowid = const Value.absent(),
|
||||||
|
}) =>
|
||||||
|
ChatDataCompanion.insert(
|
||||||
|
chatId: chatId,
|
||||||
|
metadata: metadata,
|
||||||
|
rowid: rowid,
|
||||||
|
),
|
||||||
|
withReferenceMapper: (p0) => p0
|
||||||
|
.map((e) => (e.readTable(table), BaseReferences(db, table, e)))
|
||||||
|
.toList(),
|
||||||
|
prefetchHooksCallback: null,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
typedef $ChatDataProcessedTableManager = ProcessedTableManager<
|
||||||
|
_$ChatDatabase,
|
||||||
|
ChatData,
|
||||||
|
ChatDataData,
|
||||||
|
$ChatDataFilterComposer,
|
||||||
|
$ChatDataOrderingComposer,
|
||||||
|
$ChatDataAnnotationComposer,
|
||||||
|
$ChatDataCreateCompanionBuilder,
|
||||||
|
$ChatDataUpdateCompanionBuilder,
|
||||||
|
(ChatDataData, BaseReferences<_$ChatDatabase, ChatData, ChatDataData>),
|
||||||
|
ChatDataData,
|
||||||
|
PrefetchHooks Function()>;
|
||||||
|
typedef $ChatMessageCreateCompanionBuilder = ChatMessageCompanion Function({
|
||||||
|
required String messageId,
|
||||||
|
required String chatId,
|
||||||
|
required String orderKey,
|
||||||
|
required String messageJson,
|
||||||
|
Value<int> rowid,
|
||||||
|
});
|
||||||
|
typedef $ChatMessageUpdateCompanionBuilder = ChatMessageCompanion Function({
|
||||||
|
Value<String> messageId,
|
||||||
|
Value<String> chatId,
|
||||||
|
Value<String> orderKey,
|
||||||
|
Value<String> messageJson,
|
||||||
|
Value<int> rowid,
|
||||||
|
});
|
||||||
|
|
||||||
|
class $ChatMessageFilterComposer extends Composer<_$ChatDatabase, ChatMessage> {
|
||||||
|
$ChatMessageFilterComposer({
|
||||||
|
required super.$db,
|
||||||
|
required super.$table,
|
||||||
|
super.joinBuilder,
|
||||||
|
super.$addJoinBuilderToRootComposer,
|
||||||
|
super.$removeJoinBuilderFromRootComposer,
|
||||||
|
});
|
||||||
|
ColumnFilters<String> get messageId => $composableBuilder(
|
||||||
|
column: $table.messageId, builder: (column) => ColumnFilters(column));
|
||||||
|
|
||||||
|
ColumnFilters<String> get chatId => $composableBuilder(
|
||||||
|
column: $table.chatId, builder: (column) => ColumnFilters(column));
|
||||||
|
|
||||||
|
ColumnFilters<String> get orderKey => $composableBuilder(
|
||||||
|
column: $table.orderKey, builder: (column) => ColumnFilters(column));
|
||||||
|
|
||||||
|
ColumnFilters<String> get messageJson => $composableBuilder(
|
||||||
|
column: $table.messageJson, builder: (column) => ColumnFilters(column));
|
||||||
|
}
|
||||||
|
|
||||||
|
class $ChatMessageOrderingComposer
|
||||||
|
extends Composer<_$ChatDatabase, ChatMessage> {
|
||||||
|
$ChatMessageOrderingComposer({
|
||||||
|
required super.$db,
|
||||||
|
required super.$table,
|
||||||
|
super.joinBuilder,
|
||||||
|
super.$addJoinBuilderToRootComposer,
|
||||||
|
super.$removeJoinBuilderFromRootComposer,
|
||||||
|
});
|
||||||
|
ColumnOrderings<String> get messageId => $composableBuilder(
|
||||||
|
column: $table.messageId, builder: (column) => ColumnOrderings(column));
|
||||||
|
|
||||||
|
ColumnOrderings<String> get chatId => $composableBuilder(
|
||||||
|
column: $table.chatId, builder: (column) => ColumnOrderings(column));
|
||||||
|
|
||||||
|
ColumnOrderings<String> get orderKey => $composableBuilder(
|
||||||
|
column: $table.orderKey, builder: (column) => ColumnOrderings(column));
|
||||||
|
|
||||||
|
ColumnOrderings<String> get messageJson => $composableBuilder(
|
||||||
|
column: $table.messageJson, builder: (column) => ColumnOrderings(column));
|
||||||
|
}
|
||||||
|
|
||||||
|
class $ChatMessageAnnotationComposer
|
||||||
|
extends Composer<_$ChatDatabase, ChatMessage> {
|
||||||
|
$ChatMessageAnnotationComposer({
|
||||||
|
required super.$db,
|
||||||
|
required super.$table,
|
||||||
|
super.joinBuilder,
|
||||||
|
super.$addJoinBuilderToRootComposer,
|
||||||
|
super.$removeJoinBuilderFromRootComposer,
|
||||||
|
});
|
||||||
|
GeneratedColumn<String> get messageId =>
|
||||||
|
$composableBuilder(column: $table.messageId, builder: (column) => column);
|
||||||
|
|
||||||
|
GeneratedColumn<String> get chatId =>
|
||||||
|
$composableBuilder(column: $table.chatId, builder: (column) => column);
|
||||||
|
|
||||||
|
GeneratedColumn<String> get orderKey =>
|
||||||
|
$composableBuilder(column: $table.orderKey, builder: (column) => column);
|
||||||
|
|
||||||
|
GeneratedColumn<String> get messageJson => $composableBuilder(
|
||||||
|
column: $table.messageJson, builder: (column) => column);
|
||||||
|
}
|
||||||
|
|
||||||
|
class $ChatMessageTableManager extends RootTableManager<
|
||||||
|
_$ChatDatabase,
|
||||||
|
ChatMessage,
|
||||||
|
ChatMessageData,
|
||||||
|
$ChatMessageFilterComposer,
|
||||||
|
$ChatMessageOrderingComposer,
|
||||||
|
$ChatMessageAnnotationComposer,
|
||||||
|
$ChatMessageCreateCompanionBuilder,
|
||||||
|
$ChatMessageUpdateCompanionBuilder,
|
||||||
|
(
|
||||||
|
ChatMessageData,
|
||||||
|
BaseReferences<_$ChatDatabase, ChatMessage, ChatMessageData>
|
||||||
|
),
|
||||||
|
ChatMessageData,
|
||||||
|
PrefetchHooks Function()> {
|
||||||
|
$ChatMessageTableManager(_$ChatDatabase db, ChatMessage table)
|
||||||
|
: super(TableManagerState(
|
||||||
|
db: db,
|
||||||
|
table: table,
|
||||||
|
createFilteringComposer: () =>
|
||||||
|
$ChatMessageFilterComposer($db: db, $table: table),
|
||||||
|
createOrderingComposer: () =>
|
||||||
|
$ChatMessageOrderingComposer($db: db, $table: table),
|
||||||
|
createComputedFieldComposer: () =>
|
||||||
|
$ChatMessageAnnotationComposer($db: db, $table: table),
|
||||||
|
updateCompanionCallback: ({
|
||||||
|
Value<String> messageId = const Value.absent(),
|
||||||
|
Value<String> chatId = const Value.absent(),
|
||||||
|
Value<String> orderKey = const Value.absent(),
|
||||||
|
Value<String> messageJson = const Value.absent(),
|
||||||
|
Value<int> rowid = const Value.absent(),
|
||||||
|
}) =>
|
||||||
|
ChatMessageCompanion(
|
||||||
|
messageId: messageId,
|
||||||
|
chatId: chatId,
|
||||||
|
orderKey: orderKey,
|
||||||
|
messageJson: messageJson,
|
||||||
|
rowid: rowid,
|
||||||
|
),
|
||||||
|
createCompanionCallback: ({
|
||||||
|
required String messageId,
|
||||||
|
required String chatId,
|
||||||
|
required String orderKey,
|
||||||
|
required String messageJson,
|
||||||
|
Value<int> rowid = const Value.absent(),
|
||||||
|
}) =>
|
||||||
|
ChatMessageCompanion.insert(
|
||||||
|
messageId: messageId,
|
||||||
|
chatId: chatId,
|
||||||
|
orderKey: orderKey,
|
||||||
|
messageJson: messageJson,
|
||||||
|
rowid: rowid,
|
||||||
|
),
|
||||||
|
withReferenceMapper: (p0) => p0
|
||||||
|
.map((e) => (e.readTable(table), BaseReferences(db, table, e)))
|
||||||
|
.toList(),
|
||||||
|
prefetchHooksCallback: null,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
typedef $ChatMessageProcessedTableManager = ProcessedTableManager<
|
||||||
|
_$ChatDatabase,
|
||||||
|
ChatMessage,
|
||||||
|
ChatMessageData,
|
||||||
|
$ChatMessageFilterComposer,
|
||||||
|
$ChatMessageOrderingComposer,
|
||||||
|
$ChatMessageAnnotationComposer,
|
||||||
|
$ChatMessageCreateCompanionBuilder,
|
||||||
|
$ChatMessageUpdateCompanionBuilder,
|
||||||
|
(
|
||||||
|
ChatMessageData,
|
||||||
|
BaseReferences<_$ChatDatabase, ChatMessage, ChatMessageData>
|
||||||
|
),
|
||||||
|
ChatMessageData,
|
||||||
|
PrefetchHooks Function()>;
|
||||||
|
|
||||||
|
class $ChatDatabaseManager {
|
||||||
|
final _$ChatDatabase _db;
|
||||||
|
$ChatDatabaseManager(this._db);
|
||||||
|
$ChatDataTableManager get chatData =>
|
||||||
|
$ChatDataTableManager(_db, _db.chatData);
|
||||||
|
$ChatMessageTableManager get chatMessage =>
|
||||||
|
$ChatMessageTableManager(_db, _db.chatMessage);
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
|
||||||
|
part 'chat_metadata.g.dart';
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
class ChatMetadata {
|
||||||
|
final String? mainDocumentId;
|
||||||
|
final String? contextId;
|
||||||
|
|
||||||
|
ChatMetadata({this.mainDocumentId, this.contextId});
|
||||||
|
|
||||||
|
factory ChatMetadata.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$ChatMetadataFromJson(json);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => _$ChatMetadataToJson(this);
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'chat_metadata.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
ChatMetadata _$ChatMetadataFromJson(Map<String, dynamic> json) => ChatMetadata(
|
||||||
|
mainDocumentId: json['mainDocumentId'] as String?,
|
||||||
|
contextId: json['contextId'] as String?,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$ChatMetadataToJson(ChatMetadata instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'mainDocumentId': instance.mainDocumentId,
|
||||||
|
'contextId': instance.contextId,
|
||||||
|
};
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import 'package:flutter_chat_core/flutter_chat_core.dart';
|
||||||
|
|
||||||
|
enum MessageAuthor {
|
||||||
|
///langchain SystemChatMessage
|
||||||
|
system(User(id: 'system')),
|
||||||
|
|
||||||
|
///langchain HumanChatMessage
|
||||||
|
human(User(id: 'human')),
|
||||||
|
|
||||||
|
///langchain AIChatMessage
|
||||||
|
ai(User(id: 'ai')),
|
||||||
|
|
||||||
|
///langchain ToolChatMessage
|
||||||
|
tool(User(id: 'tool')),
|
||||||
|
|
||||||
|
///langchain CustomChatMessage
|
||||||
|
custom(User(id: 'custom'));
|
||||||
|
|
||||||
|
final User user;
|
||||||
|
|
||||||
|
const MessageAuthor(this.user);
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:lensai/data/database/functions/lexo_rank_functions.dart';
|
||||||
|
import 'package:lensai/features/chat/features/chat_store/data/database/database.dart';
|
||||||
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:drift/native.dart';
|
||||||
|
import 'package:path/path.dart' as p;
|
||||||
|
import 'package:path_provider/path_provider.dart' as path_provider;
|
||||||
|
import 'package:riverpod/riverpod.dart';
|
||||||
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
import 'package:sqlite3/sqlite3.dart';
|
||||||
|
import 'package:sqlite3_flutter_libs/sqlite3_flutter_libs.dart';
|
||||||
|
import 'package:universal_io/io.dart';
|
||||||
|
|
||||||
|
part 'providers.g.dart';
|
||||||
|
|
||||||
|
@Riverpod(keepAlive: true)
|
||||||
|
ChatDatabase chatDatabase(Ref ref) {
|
||||||
|
final db = ChatDatabase(
|
||||||
|
LazyDatabase(() async {
|
||||||
|
// put the database file, called db.sqlite here, into the documents folder
|
||||||
|
// for your app.
|
||||||
|
final dbFolder = await path_provider.getApplicationDocumentsDirectory();
|
||||||
|
final file = File(p.join(dbFolder.path, 'chat.db'));
|
||||||
|
|
||||||
|
// Also work around limitations on old Android versions
|
||||||
|
if (Platform.isAndroid) {
|
||||||
|
await applyWorkaroundToOpenSqlite3OnOldAndroidVersions();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sqlite3 pick a more suitable location for temporary files - the
|
||||||
|
// one from the system may be inaccessible due to sandboxing.
|
||||||
|
final cachebase = (await path_provider.getTemporaryDirectory()).path;
|
||||||
|
// We can't access /tmp on Android, which sqlite3 would try by default.
|
||||||
|
// Explicitly tell it about the correct temporary directory.
|
||||||
|
sqlite3.tempDirectory = cachebase;
|
||||||
|
|
||||||
|
return NativeDatabase.createInBackground(
|
||||||
|
file,
|
||||||
|
setup: (database) {
|
||||||
|
registerLexorankFunctions(database);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
ref.onDispose(() {
|
||||||
|
unawaited(db.close());
|
||||||
|
});
|
||||||
|
|
||||||
|
return db;
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'providers.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// RiverpodGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
String _$chatDatabaseHash() => r'b48955bc016f8c74684ed6e2c5cba99f5ab38dfe';
|
||||||
|
|
||||||
|
/// See also [chatDatabase].
|
||||||
|
@ProviderFor(chatDatabase)
|
||||||
|
final chatDatabaseProvider = Provider<ChatDatabase>.internal(
|
||||||
|
chatDatabase,
|
||||||
|
name: r'chatDatabaseProvider',
|
||||||
|
debugGetCreateSourceHash:
|
||||||
|
const bool.fromEnvironment('dart.vm.product') ? null : _$chatDatabaseHash,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
|
// ignore: unused_element
|
||||||
|
typedef ChatDatabaseRef = ProviderRef<ChatDatabase>;
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
// ignore_for_file: avoid_dynamic_calls
|
||||||
|
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:flutter_chat_core/flutter_chat_core.dart';
|
||||||
|
import 'package:langchain/langchain.dart';
|
||||||
|
import 'package:lensai/features/chat/features/chat_store/data/models/message_types.dart';
|
||||||
|
|
||||||
|
class ChatMemory implements BaseMemory {
|
||||||
|
final ChatController _controller;
|
||||||
|
|
||||||
|
/// Max number of tokens to use.
|
||||||
|
final int maxTokenLimit;
|
||||||
|
|
||||||
|
/// Language model to use for counting tokens.
|
||||||
|
final BaseLanguageModel llm;
|
||||||
|
|
||||||
|
/// The memory key to use for the chat history.
|
||||||
|
/// This will be passed as input variable to the prompt.
|
||||||
|
final String memoryKey;
|
||||||
|
|
||||||
|
/// If true, when [loadMemoryVariables] is called, it will return
|
||||||
|
/// [ChatMessage] objects. If false, it will return a String representation
|
||||||
|
/// of the messages.
|
||||||
|
///
|
||||||
|
/// Set this to true when you are using a Chat model like `ChatOpenAI`.
|
||||||
|
/// Set this to false when you are use a text LLM like `OpenAI`.
|
||||||
|
final bool returnMessages;
|
||||||
|
|
||||||
|
/// The prefix to use for system messages if [returnMessages] is false.
|
||||||
|
final String systemPrefix;
|
||||||
|
|
||||||
|
/// The prefix to use for human messages if [returnMessages] is false.
|
||||||
|
final String humanPrefix;
|
||||||
|
|
||||||
|
/// The prefix to use for AI messages if [returnMessages] is false.
|
||||||
|
final String aiPrefix;
|
||||||
|
|
||||||
|
/// The prefix to use for tool messages if [returnMessages] is false.
|
||||||
|
final String toolPrefix;
|
||||||
|
|
||||||
|
ChatMemory(
|
||||||
|
this._controller, {
|
||||||
|
required this.returnMessages,
|
||||||
|
this.maxTokenLimit = 2000,
|
||||||
|
required this.llm,
|
||||||
|
this.memoryKey = BaseMemory.defaultMemoryKey,
|
||||||
|
this.systemPrefix = SystemChatMessage.defaultPrefix,
|
||||||
|
this.humanPrefix = HumanChatMessage.defaultPrefix,
|
||||||
|
this.aiPrefix = AIChatMessage.defaultPrefix,
|
||||||
|
this.toolPrefix = ToolChatMessage.defaultPrefix,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Set<String> get memoryKeys => {memoryKey};
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<MemoryVariables> loadMemoryVariables([
|
||||||
|
MemoryInputValues values = const {},
|
||||||
|
]) async {
|
||||||
|
final messages = _controller.messages
|
||||||
|
.whereNot(
|
||||||
|
(message) => message.metadata?['hideFromModelChatHistory'] == true,
|
||||||
|
)
|
||||||
|
.map(
|
||||||
|
(message) => switch (message) {
|
||||||
|
TextMessage(
|
||||||
|
authorId: final authorId,
|
||||||
|
text: final text,
|
||||||
|
metadata: final metadata,
|
||||||
|
) =>
|
||||||
|
() {
|
||||||
|
if (authorId == MessageAuthor.system.user.id) {
|
||||||
|
return SystemChatMessage(content: text);
|
||||||
|
}
|
||||||
|
if (authorId == MessageAuthor.human.user.id) {
|
||||||
|
return HumanChatMessage(
|
||||||
|
content: ChatMessageContent.text(text),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (authorId == MessageAuthor.ai.user.id) {
|
||||||
|
final toolsRaw = metadata?['toolCalls'];
|
||||||
|
if (toolsRaw is String) {
|
||||||
|
return AIChatMessage(
|
||||||
|
content: text,
|
||||||
|
toolCalls: _deserializeToolCalls(toolsRaw),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return AIChatMessage(content: text);
|
||||||
|
}
|
||||||
|
throw UnimplementedError();
|
||||||
|
}(),
|
||||||
|
ImageMessage() => throw UnimplementedError(),
|
||||||
|
CustomMessage() => throw UnimplementedError(),
|
||||||
|
UnsupportedMessage() => throw UnimplementedError()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
int currentBufferLength = await llm.countTokens(PromptValue.chat(messages));
|
||||||
|
// Prune buffer if it exceeds max token limit
|
||||||
|
if (currentBufferLength > maxTokenLimit) {
|
||||||
|
while (currentBufferLength > maxTokenLimit) {
|
||||||
|
//remove oldest entry
|
||||||
|
messages.removeAt(0);
|
||||||
|
currentBufferLength = await llm.countTokens(PromptValue.chat(messages));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (returnMessages) {
|
||||||
|
return {memoryKey: messages};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
memoryKey: messages.toBufferString(
|
||||||
|
systemPrefix: systemPrefix,
|
||||||
|
humanPrefix: humanPrefix,
|
||||||
|
aiPrefix: aiPrefix,
|
||||||
|
toolPrefix: toolPrefix,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
List<AIChatMessageToolCall> _deserializeToolCalls(String toolsRaw) {
|
||||||
|
final decoded = jsonDecode(toolsRaw) as List<dynamic>;
|
||||||
|
final tools = decoded
|
||||||
|
.map(
|
||||||
|
(tool) => AIChatMessageToolCall(
|
||||||
|
id: tool['id'] as String,
|
||||||
|
name: tool['name'] as String,
|
||||||
|
arguments: tool['arguments'] as Map<String, dynamic>,
|
||||||
|
argumentsRaw: tool['argumentsRaw'] as String,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
return tools;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> saveContext({
|
||||||
|
required MemoryInputValues inputValues,
|
||||||
|
required MemoryOutputValues outputValues,
|
||||||
|
}) {
|
||||||
|
throw UnimplementedError('This is a read only memory');
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> clear() {
|
||||||
|
throw UnimplementedError('This is a read only memory');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:flutter_chat_core/flutter_chat_core.dart';
|
||||||
|
import 'package:lensai/features/chat/features/chat_store/data/database/daos/messages.dart';
|
||||||
|
import 'package:rxdart/rxdart.dart';
|
||||||
|
|
||||||
|
class DriftChatController implements ChatController {
|
||||||
|
final MessagesDao _dao;
|
||||||
|
final String chatId;
|
||||||
|
|
||||||
|
final _operationsController = StreamController<ChatOperation>.broadcast();
|
||||||
|
|
||||||
|
late final ValueStream<List<Message>> _mergedMessagesStream;
|
||||||
|
late final StreamSubscription<List<Message>> _mergedMessagesStreamSub;
|
||||||
|
late final BehaviorSubject<Map<String, Message>> _intermediateMessages;
|
||||||
|
|
||||||
|
DriftChatController(this._dao, {required this.chatId}) {
|
||||||
|
final chatMessagesStream = _dao
|
||||||
|
.chatMessages(chatId: chatId)
|
||||||
|
.map(
|
||||||
|
(data) => Message.fromJson(
|
||||||
|
jsonDecode(data.messageJson) as Map<String, dynamic>,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.watch();
|
||||||
|
|
||||||
|
_intermediateMessages = BehaviorSubject.seeded({});
|
||||||
|
|
||||||
|
_mergedMessagesStream = Rx.combineLatest2(
|
||||||
|
chatMessagesStream,
|
||||||
|
_intermediateMessages,
|
||||||
|
(a, b) {
|
||||||
|
if (a.isNotEmpty && b.isNotEmpty) {
|
||||||
|
return [
|
||||||
|
for (final message in a) b[message.id] ?? message,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return a;
|
||||||
|
},
|
||||||
|
).shareValue();
|
||||||
|
|
||||||
|
//Set initial messages
|
||||||
|
unawaited(
|
||||||
|
_mergedMessagesStream.first.then((messages) {
|
||||||
|
if (messages.isNotEmpty) {
|
||||||
|
_operationsController.add(ChatOperation.set());
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
_mergedMessagesStreamSub = _mergedMessagesStream.listen(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Message> get messages => _mergedMessagesStream.valueOrNull ?? [];
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> insert(Message message, {int? index}) async {
|
||||||
|
final insertedIndex = await _dao.insertMessage(
|
||||||
|
chatId: chatId,
|
||||||
|
message: message,
|
||||||
|
index: index,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (insertedIndex != null) {
|
||||||
|
await _mergedMessagesStream
|
||||||
|
.firstWhere(
|
||||||
|
(messages) =>
|
||||||
|
messages.length > insertedIndex &&
|
||||||
|
messages[insertedIndex].id == message.id,
|
||||||
|
)
|
||||||
|
.timeout(const Duration(milliseconds: 100));
|
||||||
|
|
||||||
|
_operationsController.add(ChatOperation.insert(message, insertedIndex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> remove(Message message) async {
|
||||||
|
final index = await _dao
|
||||||
|
.messageIndex(chatId: chatId, messageId: message.id)
|
||||||
|
.getSingleOrNull();
|
||||||
|
|
||||||
|
if (index != null) {
|
||||||
|
await _dao.removeMessage(messageId: message.id);
|
||||||
|
await _mergedMessagesStream
|
||||||
|
.firstWhere(
|
||||||
|
(messages) =>
|
||||||
|
!messages.any((dbMessage) => dbMessage.id == message.id),
|
||||||
|
)
|
||||||
|
.timeout(const Duration(milliseconds: 100));
|
||||||
|
|
||||||
|
_operationsController.add(ChatOperation.remove(message, index));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> update(
|
||||||
|
Message oldMessage,
|
||||||
|
Message newMessage, {
|
||||||
|
bool persistMessage = true,
|
||||||
|
}) async {
|
||||||
|
assert(oldMessage.id == newMessage.id);
|
||||||
|
|
||||||
|
if (persistMessage) {
|
||||||
|
final intermediateMessage = _intermediateMessages.value[newMessage.id];
|
||||||
|
|
||||||
|
if (intermediateMessage == null && oldMessage == newMessage) {
|
||||||
|
//nothing to update
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final rows = await _dao.updateMessage(
|
||||||
|
messageId: oldMessage.id,
|
||||||
|
message: newMessage,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (intermediateMessage != null) {
|
||||||
|
_intermediateMessages.add(
|
||||||
|
{..._intermediateMessages.value}..remove(newMessage.id),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rows <= 0 || intermediateMessage == newMessage) {
|
||||||
|
//Dont send event
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if (oldMessage != newMessage) {
|
||||||
|
_intermediateMessages.add({
|
||||||
|
..._intermediateMessages.value,
|
||||||
|
newMessage.id: newMessage,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await _mergedMessagesStream
|
||||||
|
.firstWhere(
|
||||||
|
(messages) => messages.any((message) => message == newMessage),
|
||||||
|
)
|
||||||
|
.timeout(const Duration(milliseconds: 100));
|
||||||
|
|
||||||
|
_operationsController.add(ChatOperation.update(oldMessage, newMessage));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> set(List<Message> messages) async {
|
||||||
|
await _dao.replaceChatMessages(chatId: chatId, messages: messages);
|
||||||
|
await _mergedMessagesStream
|
||||||
|
.firstWhere(
|
||||||
|
(replacedMessages) =>
|
||||||
|
const DeepCollectionEquality().equals(messages, replacedMessages),
|
||||||
|
)
|
||||||
|
.timeout(const Duration(milliseconds: 100));
|
||||||
|
|
||||||
|
_operationsController.add(ChatOperation.set());
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Stream<ChatOperation> get operationsStream => _operationsController.stream;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
unawaited(_intermediateMessages.close());
|
||||||
|
unawaited(_mergedMessagesStreamSub.cancel());
|
||||||
|
unawaited(_operationsController.close());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import 'package:lensai/features/chat/features/chat_store/data/models/chat_metadata.dart';
|
||||||
|
import 'package:lensai/features/chat/features/chat_store/data/providers.dart';
|
||||||
|
import 'package:lensai/features/chat/features/chat_store/domain/drift_chat_controller.dart';
|
||||||
|
import 'package:riverpod/riverpod.dart';
|
||||||
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
|
||||||
|
part 'providers.g.dart';
|
||||||
|
|
||||||
|
@Riverpod()
|
||||||
|
DriftChatController chatController(Ref ref, String chatId) {
|
||||||
|
final db = ref.watch(chatDatabaseProvider);
|
||||||
|
final controller = DriftChatController(db.messagesDao, chatId: chatId);
|
||||||
|
|
||||||
|
ref.onDispose(() {
|
||||||
|
controller.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
|
return controller;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Riverpod()
|
||||||
|
Stream<ChatMetadata?> chatMetadata(Ref ref, String chatId) {
|
||||||
|
final db = ref.watch(chatDatabaseProvider);
|
||||||
|
return db.metadataDao.chatMetadata(chatId).watchSingleOrNull();
|
||||||
|
}
|
||||||
@@ -0,0 +1,292 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'providers.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// RiverpodGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
String _$chatControllerHash() => r'493264a4413ffc2d767d73a90634e1a1744cfcfd';
|
||||||
|
|
||||||
|
/// Copied from Dart SDK
|
||||||
|
class _SystemHash {
|
||||||
|
_SystemHash._();
|
||||||
|
|
||||||
|
static int combine(int hash, int value) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
hash = 0x1fffffff & (hash + value);
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
|
||||||
|
return hash ^ (hash >> 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int finish(int hash) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
hash = hash ^ (hash >> 11);
|
||||||
|
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// See also [chatController].
|
||||||
|
@ProviderFor(chatController)
|
||||||
|
const chatControllerProvider = ChatControllerFamily();
|
||||||
|
|
||||||
|
/// See also [chatController].
|
||||||
|
class ChatControllerFamily extends Family<DriftChatController> {
|
||||||
|
/// See also [chatController].
|
||||||
|
const ChatControllerFamily();
|
||||||
|
|
||||||
|
/// See also [chatController].
|
||||||
|
ChatControllerProvider call(
|
||||||
|
String chatId,
|
||||||
|
) {
|
||||||
|
return ChatControllerProvider(
|
||||||
|
chatId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
ChatControllerProvider getProviderOverride(
|
||||||
|
covariant ChatControllerProvider provider,
|
||||||
|
) {
|
||||||
|
return call(
|
||||||
|
provider.chatId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static const Iterable<ProviderOrFamily>? _dependencies = null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
|
||||||
|
|
||||||
|
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
|
||||||
|
_allTransitiveDependencies;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? get name => r'chatControllerProvider';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// See also [chatController].
|
||||||
|
class ChatControllerProvider extends AutoDisposeProvider<DriftChatController> {
|
||||||
|
/// See also [chatController].
|
||||||
|
ChatControllerProvider(
|
||||||
|
String chatId,
|
||||||
|
) : this._internal(
|
||||||
|
(ref) => chatController(
|
||||||
|
ref as ChatControllerRef,
|
||||||
|
chatId,
|
||||||
|
),
|
||||||
|
from: chatControllerProvider,
|
||||||
|
name: r'chatControllerProvider',
|
||||||
|
debugGetCreateSourceHash:
|
||||||
|
const bool.fromEnvironment('dart.vm.product')
|
||||||
|
? null
|
||||||
|
: _$chatControllerHash,
|
||||||
|
dependencies: ChatControllerFamily._dependencies,
|
||||||
|
allTransitiveDependencies:
|
||||||
|
ChatControllerFamily._allTransitiveDependencies,
|
||||||
|
chatId: chatId,
|
||||||
|
);
|
||||||
|
|
||||||
|
ChatControllerProvider._internal(
|
||||||
|
super._createNotifier, {
|
||||||
|
required super.name,
|
||||||
|
required super.dependencies,
|
||||||
|
required super.allTransitiveDependencies,
|
||||||
|
required super.debugGetCreateSourceHash,
|
||||||
|
required super.from,
|
||||||
|
required this.chatId,
|
||||||
|
}) : super.internal();
|
||||||
|
|
||||||
|
final String chatId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Override overrideWith(
|
||||||
|
DriftChatController Function(ChatControllerRef provider) create,
|
||||||
|
) {
|
||||||
|
return ProviderOverride(
|
||||||
|
origin: this,
|
||||||
|
override: ChatControllerProvider._internal(
|
||||||
|
(ref) => create(ref as ChatControllerRef),
|
||||||
|
from: from,
|
||||||
|
name: null,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
debugGetCreateSourceHash: null,
|
||||||
|
chatId: chatId,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
AutoDisposeProviderElement<DriftChatController> createElement() {
|
||||||
|
return _ChatControllerProviderElement(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return other is ChatControllerProvider && other.chatId == chatId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode {
|
||||||
|
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||||
|
hash = _SystemHash.combine(hash, chatId.hashCode);
|
||||||
|
|
||||||
|
return _SystemHash.finish(hash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
|
// ignore: unused_element
|
||||||
|
mixin ChatControllerRef on AutoDisposeProviderRef<DriftChatController> {
|
||||||
|
/// The parameter `chatId` of this provider.
|
||||||
|
String get chatId;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ChatControllerProviderElement
|
||||||
|
extends AutoDisposeProviderElement<DriftChatController>
|
||||||
|
with ChatControllerRef {
|
||||||
|
_ChatControllerProviderElement(super.provider);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get chatId => (origin as ChatControllerProvider).chatId;
|
||||||
|
}
|
||||||
|
|
||||||
|
String _$chatMetadataHash() => r'22252e4a4c4e9cb039c5eee0e0f6eb5b8bd03944';
|
||||||
|
|
||||||
|
/// See also [chatMetadata].
|
||||||
|
@ProviderFor(chatMetadata)
|
||||||
|
const chatMetadataProvider = ChatMetadataFamily();
|
||||||
|
|
||||||
|
/// See also [chatMetadata].
|
||||||
|
class ChatMetadataFamily extends Family<AsyncValue<ChatMetadata?>> {
|
||||||
|
/// See also [chatMetadata].
|
||||||
|
const ChatMetadataFamily();
|
||||||
|
|
||||||
|
/// See also [chatMetadata].
|
||||||
|
ChatMetadataProvider call(
|
||||||
|
String chatId,
|
||||||
|
) {
|
||||||
|
return ChatMetadataProvider(
|
||||||
|
chatId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
ChatMetadataProvider getProviderOverride(
|
||||||
|
covariant ChatMetadataProvider provider,
|
||||||
|
) {
|
||||||
|
return call(
|
||||||
|
provider.chatId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static const Iterable<ProviderOrFamily>? _dependencies = null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
|
||||||
|
|
||||||
|
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
|
||||||
|
_allTransitiveDependencies;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? get name => r'chatMetadataProvider';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// See also [chatMetadata].
|
||||||
|
class ChatMetadataProvider extends AutoDisposeStreamProvider<ChatMetadata?> {
|
||||||
|
/// See also [chatMetadata].
|
||||||
|
ChatMetadataProvider(
|
||||||
|
String chatId,
|
||||||
|
) : this._internal(
|
||||||
|
(ref) => chatMetadata(
|
||||||
|
ref as ChatMetadataRef,
|
||||||
|
chatId,
|
||||||
|
),
|
||||||
|
from: chatMetadataProvider,
|
||||||
|
name: r'chatMetadataProvider',
|
||||||
|
debugGetCreateSourceHash:
|
||||||
|
const bool.fromEnvironment('dart.vm.product')
|
||||||
|
? null
|
||||||
|
: _$chatMetadataHash,
|
||||||
|
dependencies: ChatMetadataFamily._dependencies,
|
||||||
|
allTransitiveDependencies:
|
||||||
|
ChatMetadataFamily._allTransitiveDependencies,
|
||||||
|
chatId: chatId,
|
||||||
|
);
|
||||||
|
|
||||||
|
ChatMetadataProvider._internal(
|
||||||
|
super._createNotifier, {
|
||||||
|
required super.name,
|
||||||
|
required super.dependencies,
|
||||||
|
required super.allTransitiveDependencies,
|
||||||
|
required super.debugGetCreateSourceHash,
|
||||||
|
required super.from,
|
||||||
|
required this.chatId,
|
||||||
|
}) : super.internal();
|
||||||
|
|
||||||
|
final String chatId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Override overrideWith(
|
||||||
|
Stream<ChatMetadata?> Function(ChatMetadataRef provider) create,
|
||||||
|
) {
|
||||||
|
return ProviderOverride(
|
||||||
|
origin: this,
|
||||||
|
override: ChatMetadataProvider._internal(
|
||||||
|
(ref) => create(ref as ChatMetadataRef),
|
||||||
|
from: from,
|
||||||
|
name: null,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
debugGetCreateSourceHash: null,
|
||||||
|
chatId: chatId,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
AutoDisposeStreamProviderElement<ChatMetadata?> createElement() {
|
||||||
|
return _ChatMetadataProviderElement(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return other is ChatMetadataProvider && other.chatId == chatId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode {
|
||||||
|
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||||
|
hash = _SystemHash.combine(hash, chatId.hashCode);
|
||||||
|
|
||||||
|
return _SystemHash.finish(hash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
|
// ignore: unused_element
|
||||||
|
mixin ChatMetadataRef on AutoDisposeStreamProviderRef<ChatMetadata?> {
|
||||||
|
/// The parameter `chatId` of this provider.
|
||||||
|
String get chatId;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ChatMetadataProviderElement
|
||||||
|
extends AutoDisposeStreamProviderElement<ChatMetadata?>
|
||||||
|
with ChatMetadataRef {
|
||||||
|
_ChatMetadataProviderElement(super.provider);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get chatId => (origin as ChatMetadataProvider).chatId;
|
||||||
|
}
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:exceptions/exceptions.dart';
|
||||||
|
import 'package:flutter_chat_core/flutter_chat_core.dart';
|
||||||
|
import 'package:langchain/langchain.dart';
|
||||||
|
import 'package:lensai/core/uuid.dart';
|
||||||
|
import 'package:lensai/features/chat/features/chat_store/data/models/message_types.dart';
|
||||||
|
import 'package:lensai/features/chat/features/chat_store/domain/drift_chat_controller.dart';
|
||||||
|
import 'package:lensai/features/chat/features/chat_store/domain/providers.dart';
|
||||||
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
|
||||||
|
part 'chat_message.g.dart';
|
||||||
|
|
||||||
|
@Riverpod()
|
||||||
|
class ChatMessageRepository extends _$ChatMessageRepository {
|
||||||
|
late DriftChatController _controller;
|
||||||
|
|
||||||
|
final _isTyping = <MessageAuthor>{};
|
||||||
|
|
||||||
|
bool isTyping(MessageAuthor author) => _isTyping.contains(author);
|
||||||
|
|
||||||
|
Future<void> setTyping({
|
||||||
|
required MessageAuthor author,
|
||||||
|
required bool typing,
|
||||||
|
}) async {
|
||||||
|
if (isTyping(author) == typing) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typing) {
|
||||||
|
await _controller.insert(
|
||||||
|
CustomMessage(
|
||||||
|
id: uuid.v4(),
|
||||||
|
authorId: author.user.id,
|
||||||
|
createdAt: DateTime.now().toUtc(),
|
||||||
|
metadata: {
|
||||||
|
'type': 'typing',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
_isTyping.add(author);
|
||||||
|
} else {
|
||||||
|
final typingMessages = _controller.messages.where(
|
||||||
|
(message) =>
|
||||||
|
message.authorId == author.user.id &&
|
||||||
|
message.metadata?['type'] == 'typing',
|
||||||
|
);
|
||||||
|
|
||||||
|
await Future.wait(
|
||||||
|
typingMessages.map((message) => _controller.remove(message)),
|
||||||
|
);
|
||||||
|
|
||||||
|
_isTyping.remove(author);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Result<TextMessage>> insertTextMessage({
|
||||||
|
required MessageAuthor author,
|
||||||
|
required String content,
|
||||||
|
List<AIChatMessageToolCall>? toolCalls,
|
||||||
|
bool hideFromModelChatHistory = false,
|
||||||
|
}) async {
|
||||||
|
final message = TextMessage(
|
||||||
|
id: uuid.v4(),
|
||||||
|
authorId: author.user.id,
|
||||||
|
createdAt: DateTime.now().toUtc(),
|
||||||
|
text: content,
|
||||||
|
isOnlyEmoji: isOnlyEmoji(content),
|
||||||
|
metadata: (hideFromModelChatHistory || (toolCalls?.isNotEmpty ?? false))
|
||||||
|
? {
|
||||||
|
'toolCalls': toolCalls?.map((tool) => tool.toMap()).toList(),
|
||||||
|
if (hideFromModelChatHistory) 'hideFromModelChatHistory': true,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
return Result.fromAsync(() async {
|
||||||
|
await _controller.insert(message);
|
||||||
|
return message;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void build(String chatId) {
|
||||||
|
_controller = ref.watch(chatControllerProvider(chatId));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'chat_message.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// RiverpodGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
String _$chatMessageRepositoryHash() =>
|
||||||
|
r'a4144618b339f7c0c329a01a294b6bebbb648bf6';
|
||||||
|
|
||||||
|
/// Copied from Dart SDK
|
||||||
|
class _SystemHash {
|
||||||
|
_SystemHash._();
|
||||||
|
|
||||||
|
static int combine(int hash, int value) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
hash = 0x1fffffff & (hash + value);
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
|
||||||
|
return hash ^ (hash >> 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int finish(int hash) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
hash = hash ^ (hash >> 11);
|
||||||
|
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class _$ChatMessageRepository
|
||||||
|
extends BuildlessAutoDisposeNotifier<void> {
|
||||||
|
late final String chatId;
|
||||||
|
|
||||||
|
void build(
|
||||||
|
String chatId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// See also [ChatMessageRepository].
|
||||||
|
@ProviderFor(ChatMessageRepository)
|
||||||
|
const chatMessageRepositoryProvider = ChatMessageRepositoryFamily();
|
||||||
|
|
||||||
|
/// See also [ChatMessageRepository].
|
||||||
|
class ChatMessageRepositoryFamily extends Family<void> {
|
||||||
|
/// See also [ChatMessageRepository].
|
||||||
|
const ChatMessageRepositoryFamily();
|
||||||
|
|
||||||
|
/// See also [ChatMessageRepository].
|
||||||
|
ChatMessageRepositoryProvider call(
|
||||||
|
String chatId,
|
||||||
|
) {
|
||||||
|
return ChatMessageRepositoryProvider(
|
||||||
|
chatId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
ChatMessageRepositoryProvider getProviderOverride(
|
||||||
|
covariant ChatMessageRepositoryProvider provider,
|
||||||
|
) {
|
||||||
|
return call(
|
||||||
|
provider.chatId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static const Iterable<ProviderOrFamily>? _dependencies = null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
|
||||||
|
|
||||||
|
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
|
||||||
|
_allTransitiveDependencies;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? get name => r'chatMessageRepositoryProvider';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// See also [ChatMessageRepository].
|
||||||
|
class ChatMessageRepositoryProvider
|
||||||
|
extends AutoDisposeNotifierProviderImpl<ChatMessageRepository, void> {
|
||||||
|
/// See also [ChatMessageRepository].
|
||||||
|
ChatMessageRepositoryProvider(
|
||||||
|
String chatId,
|
||||||
|
) : this._internal(
|
||||||
|
() => ChatMessageRepository()..chatId = chatId,
|
||||||
|
from: chatMessageRepositoryProvider,
|
||||||
|
name: r'chatMessageRepositoryProvider',
|
||||||
|
debugGetCreateSourceHash:
|
||||||
|
const bool.fromEnvironment('dart.vm.product')
|
||||||
|
? null
|
||||||
|
: _$chatMessageRepositoryHash,
|
||||||
|
dependencies: ChatMessageRepositoryFamily._dependencies,
|
||||||
|
allTransitiveDependencies:
|
||||||
|
ChatMessageRepositoryFamily._allTransitiveDependencies,
|
||||||
|
chatId: chatId,
|
||||||
|
);
|
||||||
|
|
||||||
|
ChatMessageRepositoryProvider._internal(
|
||||||
|
super._createNotifier, {
|
||||||
|
required super.name,
|
||||||
|
required super.dependencies,
|
||||||
|
required super.allTransitiveDependencies,
|
||||||
|
required super.debugGetCreateSourceHash,
|
||||||
|
required super.from,
|
||||||
|
required this.chatId,
|
||||||
|
}) : super.internal();
|
||||||
|
|
||||||
|
final String chatId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void runNotifierBuild(
|
||||||
|
covariant ChatMessageRepository notifier,
|
||||||
|
) {
|
||||||
|
return notifier.build(
|
||||||
|
chatId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Override overrideWith(ChatMessageRepository Function() create) {
|
||||||
|
return ProviderOverride(
|
||||||
|
origin: this,
|
||||||
|
override: ChatMessageRepositoryProvider._internal(
|
||||||
|
() => create()..chatId = chatId,
|
||||||
|
from: from,
|
||||||
|
name: null,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
debugGetCreateSourceHash: null,
|
||||||
|
chatId: chatId,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
AutoDisposeNotifierProviderElement<ChatMessageRepository, void>
|
||||||
|
createElement() {
|
||||||
|
return _ChatMessageRepositoryProviderElement(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return other is ChatMessageRepositoryProvider && other.chatId == chatId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode {
|
||||||
|
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||||
|
hash = _SystemHash.combine(hash, chatId.hashCode);
|
||||||
|
|
||||||
|
return _SystemHash.finish(hash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
|
// ignore: unused_element
|
||||||
|
mixin ChatMessageRepositoryRef on AutoDisposeNotifierProviderRef<void> {
|
||||||
|
/// The parameter `chatId` of this provider.
|
||||||
|
String get chatId;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ChatMessageRepositoryProviderElement
|
||||||
|
extends AutoDisposeNotifierProviderElement<ChatMessageRepository, void>
|
||||||
|
with ChatMessageRepositoryRef {
|
||||||
|
_ChatMessageRepositoryProviderElement(super.provider);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get chatId => (origin as ChatMessageRepositoryProvider).chatId;
|
||||||
|
}
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import 'package:exceptions/exceptions.dart';
|
||||||
|
import 'package:lensai/features/chat/features/chat_store/data/database/database.dart';
|
||||||
|
import 'package:lensai/features/chat/features/chat_store/data/models/chat_metadata.dart';
|
||||||
|
import 'package:lensai/features/chat/features/chat_store/data/providers.dart';
|
||||||
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
|
||||||
|
part 'chat_metadata.g.dart';
|
||||||
|
|
||||||
|
@Riverpod()
|
||||||
|
class ChatMetadataRepository extends _$ChatMetadataRepository {
|
||||||
|
late ChatDatabase _db;
|
||||||
|
|
||||||
|
Future<Result<void>> updateMetadata(ChatMetadata metadata) {
|
||||||
|
return Result.fromAsync(() {
|
||||||
|
return _db.metadataDao.updateChatMetadata(chatId, metadata);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void build(String chatId) {
|
||||||
|
_db = ref.watch(chatDatabaseProvider);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'chat_metadata.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// RiverpodGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
String _$chatMetadataRepositoryHash() =>
|
||||||
|
r'72a0226ed7e87f452f4dfe44594bb30663c2db45';
|
||||||
|
|
||||||
|
/// Copied from Dart SDK
|
||||||
|
class _SystemHash {
|
||||||
|
_SystemHash._();
|
||||||
|
|
||||||
|
static int combine(int hash, int value) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
hash = 0x1fffffff & (hash + value);
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
|
||||||
|
return hash ^ (hash >> 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int finish(int hash) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
hash = hash ^ (hash >> 11);
|
||||||
|
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class _$ChatMetadataRepository
|
||||||
|
extends BuildlessAutoDisposeNotifier<void> {
|
||||||
|
late final String chatId;
|
||||||
|
|
||||||
|
void build(
|
||||||
|
String chatId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// See also [ChatMetadataRepository].
|
||||||
|
@ProviderFor(ChatMetadataRepository)
|
||||||
|
const chatMetadataRepositoryProvider = ChatMetadataRepositoryFamily();
|
||||||
|
|
||||||
|
/// See also [ChatMetadataRepository].
|
||||||
|
class ChatMetadataRepositoryFamily extends Family<void> {
|
||||||
|
/// See also [ChatMetadataRepository].
|
||||||
|
const ChatMetadataRepositoryFamily();
|
||||||
|
|
||||||
|
/// See also [ChatMetadataRepository].
|
||||||
|
ChatMetadataRepositoryProvider call(
|
||||||
|
String chatId,
|
||||||
|
) {
|
||||||
|
return ChatMetadataRepositoryProvider(
|
||||||
|
chatId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
ChatMetadataRepositoryProvider getProviderOverride(
|
||||||
|
covariant ChatMetadataRepositoryProvider provider,
|
||||||
|
) {
|
||||||
|
return call(
|
||||||
|
provider.chatId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static const Iterable<ProviderOrFamily>? _dependencies = null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
|
||||||
|
|
||||||
|
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
|
||||||
|
_allTransitiveDependencies;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? get name => r'chatMetadataRepositoryProvider';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// See also [ChatMetadataRepository].
|
||||||
|
class ChatMetadataRepositoryProvider
|
||||||
|
extends AutoDisposeNotifierProviderImpl<ChatMetadataRepository, void> {
|
||||||
|
/// See also [ChatMetadataRepository].
|
||||||
|
ChatMetadataRepositoryProvider(
|
||||||
|
String chatId,
|
||||||
|
) : this._internal(
|
||||||
|
() => ChatMetadataRepository()..chatId = chatId,
|
||||||
|
from: chatMetadataRepositoryProvider,
|
||||||
|
name: r'chatMetadataRepositoryProvider',
|
||||||
|
debugGetCreateSourceHash:
|
||||||
|
const bool.fromEnvironment('dart.vm.product')
|
||||||
|
? null
|
||||||
|
: _$chatMetadataRepositoryHash,
|
||||||
|
dependencies: ChatMetadataRepositoryFamily._dependencies,
|
||||||
|
allTransitiveDependencies:
|
||||||
|
ChatMetadataRepositoryFamily._allTransitiveDependencies,
|
||||||
|
chatId: chatId,
|
||||||
|
);
|
||||||
|
|
||||||
|
ChatMetadataRepositoryProvider._internal(
|
||||||
|
super._createNotifier, {
|
||||||
|
required super.name,
|
||||||
|
required super.dependencies,
|
||||||
|
required super.allTransitiveDependencies,
|
||||||
|
required super.debugGetCreateSourceHash,
|
||||||
|
required super.from,
|
||||||
|
required this.chatId,
|
||||||
|
}) : super.internal();
|
||||||
|
|
||||||
|
final String chatId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void runNotifierBuild(
|
||||||
|
covariant ChatMetadataRepository notifier,
|
||||||
|
) {
|
||||||
|
return notifier.build(
|
||||||
|
chatId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Override overrideWith(ChatMetadataRepository Function() create) {
|
||||||
|
return ProviderOverride(
|
||||||
|
origin: this,
|
||||||
|
override: ChatMetadataRepositoryProvider._internal(
|
||||||
|
() => create()..chatId = chatId,
|
||||||
|
from: from,
|
||||||
|
name: null,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
debugGetCreateSourceHash: null,
|
||||||
|
chatId: chatId,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
AutoDisposeNotifierProviderElement<ChatMetadataRepository, void>
|
||||||
|
createElement() {
|
||||||
|
return _ChatMetadataRepositoryProviderElement(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return other is ChatMetadataRepositoryProvider && other.chatId == chatId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode {
|
||||||
|
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||||
|
hash = _SystemHash.combine(hash, chatId.hashCode);
|
||||||
|
|
||||||
|
return _SystemHash.finish(hash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
|
// ignore: unused_element
|
||||||
|
mixin ChatMetadataRepositoryRef on AutoDisposeNotifierProviderRef<void> {
|
||||||
|
/// The parameter `chatId` of this provider.
|
||||||
|
String get chatId;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ChatMetadataRepositoryProviderElement
|
||||||
|
extends AutoDisposeNotifierProviderElement<ChatMetadataRepository, void>
|
||||||
|
with ChatMetadataRepositoryRef {
|
||||||
|
_ChatMetadataRepositoryProviderElement(super.provider);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get chatId => (origin as ChatMetadataRepositoryProvider).chatId;
|
||||||
|
}
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
||||||
@@ -403,8 +403,8 @@ abstract class _$ChatSearchDatabase extends GeneratedDatabase {
|
|||||||
chatFts,
|
chatFts,
|
||||||
}).map((QueryRow row) => ChatQueryResult(
|
}).map((QueryRow row) => ChatQueryResult(
|
||||||
fileName: row.read<String>('file_name'),
|
fileName: row.read<String>('file_name'),
|
||||||
title: row.read<String>('title'),
|
title: row.readNullable<String>('title'),
|
||||||
contentSnippet: row.read<String>('content_snippet'),
|
contentSnippet: row.readNullable<String>('content_snippet'),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -702,11 +702,11 @@ class $ChatSearchDatabaseManager {
|
|||||||
|
|
||||||
class ChatQueryResult {
|
class ChatQueryResult {
|
||||||
final String fileName;
|
final String fileName;
|
||||||
final String title;
|
final String? title;
|
||||||
final String contentSnippet;
|
final String? contentSnippet;
|
||||||
ChatQueryResult({
|
ChatQueryResult({
|
||||||
required this.fileName,
|
required this.fileName,
|
||||||
required this.title,
|
this.title,
|
||||||
required this.contentSnippet,
|
this.contentSnippet,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:drift/native.dart';
|
import 'package:drift/native.dart';
|
||||||
import 'package:lensai/features/chat_archive/data/database/database.dart';
|
import 'package:lensai/features/chat_archive/data/database/database.dart';
|
||||||
@@ -12,7 +14,7 @@ part 'providers.g.dart';
|
|||||||
|
|
||||||
@Riverpod()
|
@Riverpod()
|
||||||
ChatSearchDatabase chatSearchDatabase(Ref ref) {
|
ChatSearchDatabase chatSearchDatabase(Ref ref) {
|
||||||
return ChatSearchDatabase(
|
final db = ChatSearchDatabase(
|
||||||
LazyDatabase(() async {
|
LazyDatabase(() async {
|
||||||
// Also work around limitations on old Android versions
|
// Also work around limitations on old Android versions
|
||||||
if (Platform.isAndroid) {
|
if (Platform.isAndroid) {
|
||||||
@@ -29,4 +31,10 @@ ChatSearchDatabase chatSearchDatabase(Ref ref) {
|
|||||||
return NativeDatabase.memory();
|
return NativeDatabase.memory();
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
ref.onDispose(() {
|
||||||
|
unawaited(db.close());
|
||||||
|
});
|
||||||
|
|
||||||
|
return db;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ part of 'providers.dart';
|
|||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$chatSearchDatabaseHash() =>
|
String _$chatSearchDatabaseHash() =>
|
||||||
r'99700f1a2c25b24742c60fa8dd91fe744dcf21be';
|
r'17c972b3da21b6a2365cc5fd56a4eade0ac0a3f3';
|
||||||
|
|
||||||
/// See also [chatSearchDatabase].
|
/// See also [chatSearchDatabase].
|
||||||
@ProviderFor(chatSearchDatabase)
|
@ProviderFor(chatSearchDatabase)
|
||||||
|
|||||||
@@ -91,9 +91,10 @@ class ChatArchiveSearchScreen extends HookConsumerWidget {
|
|||||||
chatEntity.dateTime!.formatWithMinutePrecision(),
|
chatEntity.dateTime!.formatWithMinutePrecision(),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8.0),
|
const SizedBox(height: 8.0),
|
||||||
MarkdownBody(
|
if (chat.contentSnippet != null)
|
||||||
data: chat.contentSnippet,
|
MarkdownBody(
|
||||||
),
|
data: chat.contentSnippet!,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -78,13 +78,13 @@ class TabRepository extends _$TabRepository {
|
|||||||
|
|
||||||
final tabContentSub =
|
final tabContentSub =
|
||||||
tabContentService.tabContentStream.listen((content) async {
|
tabContentService.tabContentStream.listen((content) async {
|
||||||
await _db.tabDao.updateTab(
|
await _db.tabDao.updateTabContent(
|
||||||
content.tabId,
|
content.tabId,
|
||||||
isProbablyReaderable: Value(content.isProbablyReaderable),
|
isProbablyReaderable: content.isProbablyReaderable,
|
||||||
extractedContentMarkdown: Value(content.extractedContentMarkdown),
|
extractedContentMarkdown: content.extractedContentMarkdown,
|
||||||
extractedContentPlain: Value(content.extractedContentPlain),
|
extractedContentPlain: content.extractedContentPlain,
|
||||||
fullContentMarkdown: Value(content.fullContentMarkdown),
|
fullContentMarkdown: content.fullContentMarkdown,
|
||||||
fullContentPlain: Value(content.fullContentPlain),
|
fullContentPlain: content.fullContentPlain,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ part of 'tab.dart';
|
|||||||
// RiverpodGenerator
|
// RiverpodGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$tabRepositoryHash() => r'4269add3c89250ab30835055e3567a1e6d5c185a';
|
String _$tabRepositoryHash() => r'b9caf89fe372b79f37c4a3ebff800ccc2d9289d2';
|
||||||
|
|
||||||
/// See also [TabRepository].
|
/// See also [TabRepository].
|
||||||
@ProviderFor(TabRepository)
|
@ProviderFor(TabRepository)
|
||||||
|
|||||||
@@ -26,3 +26,15 @@ class ViewTabsSheet extends Sheet {
|
|||||||
@override
|
@override
|
||||||
List<Object?> get hashParameters => [];
|
List<Object?> get hashParameters => [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class TabQaChatSheet extends Sheet {
|
||||||
|
final String chatId;
|
||||||
|
|
||||||
|
TabQaChatSheet({required this.chatId});
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get cacheHash => true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get hashParameters => [chatId];
|
||||||
|
}
|
||||||
|
|||||||
+213
-133
@@ -1,16 +1,24 @@
|
|||||||
import 'dart:ui' as ui;
|
import 'dart:ui' as ui;
|
||||||
|
|
||||||
|
import 'package:fading_scroll/fading_scroll.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
|
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:lensai/data/models/web_page_info.dart';
|
import 'package:lensai/data/models/web_page_info.dart';
|
||||||
|
import 'package:lensai/features/bangs/data/models/bang_data.dart';
|
||||||
import 'package:lensai/features/bangs/domain/providers/bangs.dart';
|
import 'package:lensai/features/bangs/domain/providers/bangs.dart';
|
||||||
import 'package:lensai/features/bangs/presentation/widgets/site_search.dart';
|
import 'package:lensai/features/bangs/presentation/widgets/site_search.dart';
|
||||||
|
import 'package:lensai/features/chat/features/chat_store/data/models/chat_metadata.dart';
|
||||||
|
import 'package:lensai/features/chat/features/chat_store/domain/repositories/chat_metadata.dart';
|
||||||
|
import 'package:lensai/features/geckoview/domain/controllers/bottom_sheet.dart';
|
||||||
import 'package:lensai/features/geckoview/domain/providers.dart';
|
import 'package:lensai/features/geckoview/domain/providers.dart';
|
||||||
import 'package:lensai/features/geckoview/domain/providers/tab_session.dart';
|
import 'package:lensai/features/geckoview/domain/providers/tab_session.dart';
|
||||||
|
import 'package:lensai/features/geckoview/domain/providers/tab_state.dart';
|
||||||
import 'package:lensai/features/geckoview/domain/repositories/tab.dart';
|
import 'package:lensai/features/geckoview/domain/repositories/tab.dart';
|
||||||
|
import 'package:lensai/features/geckoview/features/browser/domain/entities/sheet.dart';
|
||||||
import 'package:lensai/features/kagi/data/entities/modes.dart';
|
import 'package:lensai/features/kagi/data/entities/modes.dart';
|
||||||
import 'package:lensai/features/kagi/utils/url_builder.dart' as uri_builder;
|
import 'package:lensai/features/kagi/utils/url_builder.dart' as uri_builder;
|
||||||
import 'package:lensai/features/share_intent/domain/entities/shared_content.dart';
|
import 'package:lensai/features/share_intent/domain/entities/shared_content.dart';
|
||||||
@@ -24,12 +32,9 @@ class WebPageDialog extends HookConsumerWidget {
|
|||||||
final Uri url;
|
final Uri url;
|
||||||
final WebPageInfo? precachedInfo;
|
final WebPageInfo? precachedInfo;
|
||||||
|
|
||||||
final void Function()? onDismiss;
|
|
||||||
|
|
||||||
const WebPageDialog({
|
const WebPageDialog({
|
||||||
required this.url,
|
required this.url,
|
||||||
this.precachedInfo,
|
this.precachedInfo,
|
||||||
this.onDismiss,
|
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -56,21 +61,19 @@ class WebPageDialog extends HookConsumerWidget {
|
|||||||
useTextEditingController(text: url.toString());
|
useTextEditingController(text: url.toString());
|
||||||
final addressTextFocusNode = useFocusNode();
|
final addressTextFocusNode = useFocusNode();
|
||||||
|
|
||||||
return Stack(
|
return MediaQuery.removeViewInsets(
|
||||||
children: [
|
context: context,
|
||||||
ModalBarrier(
|
removeBottom: true,
|
||||||
color: Theme.of(context).dialogTheme.barrierColor ?? Colors.black54,
|
child: Dialog(
|
||||||
onDismiss: onDismiss,
|
insetPadding:
|
||||||
),
|
const EdgeInsets.symmetric(horizontal: 16.0, vertical: 64.0),
|
||||||
SimpleDialog(
|
child: Column(
|
||||||
titlePadding: const EdgeInsets.fromLTRB(16.0, 12.0, 16.0, 0.0),
|
mainAxisSize: MainAxisSize.min,
|
||||||
contentPadding: EdgeInsets.zero,
|
|
||||||
insetPadding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 20.0,
|
|
||||||
vertical: 24.0,
|
|
||||||
),
|
|
||||||
title: WebsiteTitleTile(url, precachedInfo: precachedInfo),
|
|
||||||
children: [
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(16.0, 12.0, 16.0, 0.0),
|
||||||
|
child: WebsiteTitleTile(url, precachedInfo: precachedInfo),
|
||||||
|
),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
//We need this to stretch the dialog, then padding from dialog is applied
|
//We need this to stretch the dialog, then padding from dialog is applied
|
||||||
width: double.maxFinite,
|
width: double.maxFinite,
|
||||||
@@ -82,6 +85,7 @@ class WebPageDialog extends HookConsumerWidget {
|
|||||||
controller: addressTextController,
|
controller: addressTextController,
|
||||||
focusNode: addressTextFocusNode,
|
focusNode: addressTextFocusNode,
|
||||||
enableIMEPersonalizedLearning: !incognitoEnabled,
|
enableIMEPersonalizedLearning: !incognitoEnabled,
|
||||||
|
keyboardType: TextInputType.url,
|
||||||
decoration: const InputDecoration(
|
decoration: const InputDecoration(
|
||||||
labelText: 'Address',
|
labelText: 'Address',
|
||||||
),
|
),
|
||||||
@@ -106,136 +110,212 @@ class WebPageDialog extends HookConsumerWidget {
|
|||||||
.read(tabSessionProvider(tabId: null).notifier)
|
.read(tabSessionProvider(tabId: null).notifier)
|
||||||
.loadUrl(url: Uri.tryParse(value)!);
|
.loadUrl(url: Uri.tryParse(value)!);
|
||||||
|
|
||||||
onDismiss?.call();
|
if (context.mounted) {
|
||||||
|
context.pop();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 4,
|
||||||
|
),
|
||||||
const Divider(),
|
const Divider(),
|
||||||
Padding(
|
Flexible(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
child: FadingScroll(
|
||||||
child: availableBangsAsync.when(
|
fadingSize: 25,
|
||||||
data: (availableBangs) {
|
builder: (context, controller) {
|
||||||
if (availableBangs.isEmpty) {
|
return SingleChildScrollView(
|
||||||
return const SizedBox.shrink();
|
controller: controller,
|
||||||
}
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||||
|
child: availableBangsAsync.when(
|
||||||
|
data: (availableBangs) {
|
||||||
|
if (availableBangs.isEmpty) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
return SiteSearch(
|
return SiteSearch(
|
||||||
domain: url.host,
|
domain: url.host,
|
||||||
availableBangs: availableBangs,
|
availableBangs: availableBangs,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
error: (error, stackTrace) => FailureWidget(
|
||||||
|
title: 'Could not load bangs',
|
||||||
|
exception: error,
|
||||||
|
),
|
||||||
|
loading: () => SiteSearch(
|
||||||
|
domain: url.host,
|
||||||
|
availableBangs: [
|
||||||
|
BangData(
|
||||||
|
websiteName: 'websiteName',
|
||||||
|
domain: 'domain',
|
||||||
|
trigger: 'trigger',
|
||||||
|
urlTemplate: 'urlTemplate',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (availableBangsAsync.isLoading ||
|
||||||
|
availableBangCount == null ||
|
||||||
|
availableBangCount > 0)
|
||||||
|
const Divider(),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(MdiIcons.contentCopy),
|
||||||
|
title: const Text('Copy address'),
|
||||||
|
onTap: () async {
|
||||||
|
await Clipboard.setData(
|
||||||
|
ClipboardData(text: url.toString()),
|
||||||
|
);
|
||||||
|
if (context.mounted) {
|
||||||
|
context.pop();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
onTap: () async {
|
||||||
|
await ui_helper.launchUrlFeedback(context, url);
|
||||||
|
},
|
||||||
|
leading: const Icon(Icons.open_in_browser),
|
||||||
|
title: const Text('Launch External'),
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(MdiIcons.tabPlus),
|
||||||
|
title: const Text('Clone tab'),
|
||||||
|
onTap: () async {
|
||||||
|
await ref
|
||||||
|
.read(tabRepositoryProvider.notifier)
|
||||||
|
.addTab(url: url);
|
||||||
|
|
||||||
|
if (context.mounted) {
|
||||||
|
context.pop();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.share),
|
||||||
|
title: const Text('Share link'),
|
||||||
|
onTap: () async {
|
||||||
|
await Share.shareUri(url);
|
||||||
|
|
||||||
|
if (context.mounted) {
|
||||||
|
context.pop();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.mobile_screen_share),
|
||||||
|
title: const Text('Share screenshot'),
|
||||||
|
onTap: () async {
|
||||||
|
final screenshot = await ref
|
||||||
|
.read(selectedTabSessionNotifierProvider)
|
||||||
|
.requestScreenshot();
|
||||||
|
|
||||||
|
if (screenshot != null) {
|
||||||
|
ui.decodeImageFromList(
|
||||||
|
screenshot,
|
||||||
|
(result) async {
|
||||||
|
final png = await result.toByteData(
|
||||||
|
format: ui.ImageByteFormat.png,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (png != null) {
|
||||||
|
final file = XFile.fromData(
|
||||||
|
png.buffer.asUint8List(),
|
||||||
|
mimeType: 'image/png',
|
||||||
|
);
|
||||||
|
|
||||||
|
await Share.shareXFiles(
|
||||||
|
[file],
|
||||||
|
subject: precachedInfo?.title,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (context.mounted) {
|
||||||
|
context.pop();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const Divider(),
|
||||||
|
ListTile(
|
||||||
|
leading: Icon(KagiTool.assistant.icon),
|
||||||
|
title: const Text('QA Chat'),
|
||||||
|
onTap: () async {
|
||||||
|
final selectedTabId =
|
||||||
|
ref.read(selectedTabStateProvider)?.id;
|
||||||
|
|
||||||
|
if (selectedTabId != null) {
|
||||||
|
final updateResult = await ref
|
||||||
|
.read(
|
||||||
|
chatMetadataRepositoryProvider(
|
||||||
|
selectedTabId)
|
||||||
|
.notifier,
|
||||||
|
)
|
||||||
|
.updateMetadata(
|
||||||
|
ChatMetadata(mainDocumentId: selectedTabId),
|
||||||
|
);
|
||||||
|
|
||||||
|
updateResult.onSuccess((_) {
|
||||||
|
ref
|
||||||
|
.read(
|
||||||
|
bottomSheetControllerProvider.notifier)
|
||||||
|
.show(
|
||||||
|
TabQaChatSheet(chatId: selectedTabId));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (context.mounted) {
|
||||||
|
context.pop();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: Icon(KagiTool.summarizer.icon),
|
||||||
|
title: const Text('Summarize'),
|
||||||
|
onTap: () async {
|
||||||
|
final summarizerUrl = uri_builder.summarizerUri(
|
||||||
|
document: SharedUrl(url),
|
||||||
|
mode: SummarizerMode.keyMoments,
|
||||||
|
);
|
||||||
|
|
||||||
|
await ref
|
||||||
|
.read(tabRepositoryProvider.notifier)
|
||||||
|
.addTab(url: summarizerUrl);
|
||||||
|
|
||||||
|
if (context.mounted) {
|
||||||
|
context.pop();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
error: (error, stackTrace) => FailureWidget(
|
),
|
||||||
title: 'Could not load bangs',
|
),
|
||||||
exception: error,
|
Align(
|
||||||
),
|
alignment: Alignment.bottomRight,
|
||||||
loading: () => SiteSearch(
|
child: Padding(
|
||||||
domain: url.host,
|
padding: const EdgeInsets.only(right: 16.0, bottom: 16.0),
|
||||||
availableBangs: [
|
child: TextButton(
|
||||||
// BangData(
|
onPressed: () {
|
||||||
// websiteName: 'websiteName',
|
context.pop();
|
||||||
// domain: 'domain',
|
},
|
||||||
// trigger: 'trigger',
|
child: const Text('Close'),
|
||||||
// urlTemplate: 'urlTemplate',
|
|
||||||
// ),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (availableBangsAsync.isLoading ||
|
|
||||||
availableBangCount == null ||
|
|
||||||
availableBangCount > 0)
|
|
||||||
const Divider(),
|
|
||||||
ListTile(
|
|
||||||
leading: const Icon(MdiIcons.contentCopy),
|
|
||||||
title: const Text('Copy address'),
|
|
||||||
onTap: () async {
|
|
||||||
await Clipboard.setData(
|
|
||||||
ClipboardData(text: url.toString()),
|
|
||||||
);
|
|
||||||
onDismiss?.call();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
ListTile(
|
|
||||||
onTap: () async {
|
|
||||||
await ui_helper.launchUrlFeedback(context, url);
|
|
||||||
},
|
|
||||||
leading: const Icon(Icons.open_in_browser),
|
|
||||||
title: const Text('Launch External'),
|
|
||||||
),
|
|
||||||
ListTile(
|
|
||||||
leading: const Icon(MdiIcons.tabPlus),
|
|
||||||
title: const Text('Clone tab'),
|
|
||||||
onTap: () async {
|
|
||||||
await ref.read(tabRepositoryProvider.notifier).addTab(url: url);
|
|
||||||
|
|
||||||
onDismiss?.call();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
ListTile(
|
|
||||||
leading: const Icon(Icons.share),
|
|
||||||
title: const Text('Share link'),
|
|
||||||
onTap: () async {
|
|
||||||
await Share.shareUri(url);
|
|
||||||
|
|
||||||
onDismiss?.call();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
ListTile(
|
|
||||||
leading: const Icon(Icons.mobile_screen_share),
|
|
||||||
title: const Text('Share screenshot'),
|
|
||||||
onTap: () async {
|
|
||||||
final screenshot = await ref
|
|
||||||
.read(selectedTabSessionNotifierProvider)
|
|
||||||
.requestScreenshot();
|
|
||||||
|
|
||||||
if (screenshot != null) {
|
|
||||||
ui.decodeImageFromList(
|
|
||||||
screenshot,
|
|
||||||
(result) async {
|
|
||||||
final png = await result.toByteData(
|
|
||||||
format: ui.ImageByteFormat.png,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (png != null) {
|
|
||||||
final file = XFile.fromData(
|
|
||||||
png.buffer.asUint8List(),
|
|
||||||
mimeType: 'image/png',
|
|
||||||
);
|
|
||||||
|
|
||||||
await Share.shareXFiles(
|
|
||||||
[file],
|
|
||||||
subject: precachedInfo?.title,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
onDismiss?.call();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
const Divider(),
|
|
||||||
ListTile(
|
|
||||||
leading: Icon(KagiTool.summarizer.icon),
|
|
||||||
title: const Text('Summarize'),
|
|
||||||
onTap: () async {
|
|
||||||
final summarizerUrl = uri_builder.summarizerUri(
|
|
||||||
document: SharedUrl(url),
|
|
||||||
mode: SummarizerMode.keyMoments,
|
|
||||||
);
|
|
||||||
|
|
||||||
await ref
|
|
||||||
.read(tabRepositoryProvider.notifier)
|
|
||||||
.addTab(url: summarizerUrl);
|
|
||||||
|
|
||||||
onDismiss?.call();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
|
|||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:lensai/core/routing/routes.dart';
|
import 'package:lensai/core/routing/routes.dart';
|
||||||
|
import 'package:lensai/features/geckoview/domain/controllers/bottom_sheet.dart';
|
||||||
|
import 'package:lensai/features/geckoview/domain/controllers/overlay_dialog.dart';
|
||||||
import 'package:lensai/features/geckoview/domain/providers.dart';
|
import 'package:lensai/features/geckoview/domain/providers.dart';
|
||||||
import 'package:lensai/features/geckoview/domain/providers/tab_list.dart';
|
import 'package:lensai/features/geckoview/domain/providers/tab_list.dart';
|
||||||
import 'package:lensai/features/geckoview/domain/providers/tab_session.dart';
|
import 'package:lensai/features/geckoview/domain/providers/tab_session.dart';
|
||||||
@@ -17,18 +19,20 @@ import 'package:lensai/features/geckoview/features/browser/domain/services/engin
|
|||||||
import 'package:lensai/features/geckoview/features/browser/presentation/dialogs/web_page_dialog.dart';
|
import 'package:lensai/features/geckoview/features/browser/presentation/dialogs/web_page_dialog.dart';
|
||||||
import 'package:lensai/features/geckoview/features/browser/presentation/widgets/app_bar_title.dart';
|
import 'package:lensai/features/geckoview/features/browser/presentation/widgets/app_bar_title.dart';
|
||||||
import 'package:lensai/features/geckoview/features/browser/presentation/widgets/browser_view.dart';
|
import 'package:lensai/features/geckoview/features/browser/presentation/widgets/browser_view.dart';
|
||||||
|
import 'package:lensai/features/geckoview/features/browser/presentation/widgets/draggable_scrollable_header.dart';
|
||||||
import 'package:lensai/features/geckoview/features/browser/presentation/widgets/extension_badge_icon.dart';
|
import 'package:lensai/features/geckoview/features/browser/presentation/widgets/extension_badge_icon.dart';
|
||||||
import 'package:lensai/features/geckoview/features/browser/presentation/widgets/sheets/create_tab.dart';
|
import 'package:lensai/features/geckoview/features/browser/presentation/widgets/sheets/create_tab.dart';
|
||||||
import 'package:lensai/features/geckoview/features/browser/presentation/widgets/sheets/view_tabs.dart';
|
import 'package:lensai/features/geckoview/features/browser/presentation/widgets/sheets/view_tabs.dart';
|
||||||
import 'package:lensai/features/geckoview/features/browser/presentation/widgets/tabs_action_button.dart';
|
import 'package:lensai/features/geckoview/features/browser/presentation/widgets/tabs_action_button.dart';
|
||||||
import 'package:lensai/features/geckoview/features/controllers/bottom_sheet.dart';
|
|
||||||
import 'package:lensai/features/geckoview/features/controllers/overlay_dialog.dart';
|
|
||||||
import 'package:lensai/features/geckoview/features/find_in_page/presentation/controllers/find_in_page_visibility.dart';
|
import 'package:lensai/features/geckoview/features/find_in_page/presentation/controllers/find_in_page_visibility.dart';
|
||||||
import 'package:lensai/features/geckoview/features/find_in_page/presentation/widgets/find_in_page.dart';
|
import 'package:lensai/features/geckoview/features/find_in_page/presentation/widgets/find_in_page.dart';
|
||||||
import 'package:lensai/features/geckoview/features/readerview/presentation/widgets/reader_appearance_button.dart';
|
import 'package:lensai/features/geckoview/features/readerview/presentation/widgets/reader_appearance_button.dart';
|
||||||
import 'package:lensai/features/geckoview/features/readerview/presentation/widgets/reader_button.dart';
|
import 'package:lensai/features/geckoview/features/readerview/presentation/widgets/reader_button.dart';
|
||||||
|
import 'package:lensai/features/geckoview/features/tabs/features/chat/presentation/widgets/tab_qa_chat.dart';
|
||||||
import 'package:lensai/features/kagi/data/entities/modes.dart';
|
import 'package:lensai/features/kagi/data/entities/modes.dart';
|
||||||
import 'package:lensai/features/user/domain/repositories/settings.dart';
|
import 'package:lensai/features/user/domain/repositories/settings.dart';
|
||||||
|
import 'package:lensai/presentation/hooks/draggable_scrollable_controller.dart';
|
||||||
|
import 'package:lensai/presentation/hooks/menu_controller.dart';
|
||||||
import 'package:lensai/presentation/hooks/overlay_portal_controller.dart';
|
import 'package:lensai/presentation/hooks/overlay_portal_controller.dart';
|
||||||
import 'package:lensai/utils/ui_helper.dart' as ui_helper;
|
import 'package:lensai/utils/ui_helper.dart' as ui_helper;
|
||||||
import 'package:share_plus/share_plus.dart';
|
import 'package:share_plus/share_plus.dart';
|
||||||
@@ -52,7 +56,8 @@ class BrowserScreen extends HookConsumerWidget {
|
|||||||
final selectedTabId =
|
final selectedTabId =
|
||||||
ref.watch(selectedTabStateProvider.select((value) => value?.id));
|
ref.watch(selectedTabStateProvider.select((value) => value?.id));
|
||||||
|
|
||||||
final menuController = useMemoized(() => MenuController());
|
final trippleDotMenuController = useMenuController();
|
||||||
|
final tabMenuController = useMenuController();
|
||||||
|
|
||||||
final lastBackButtonPress = useRef<DateTime?>(null);
|
final lastBackButtonPress = useRef<DateTime?>(null);
|
||||||
|
|
||||||
@@ -120,24 +125,13 @@ class BrowserScreen extends HookConsumerWidget {
|
|||||||
return (tabState != null)
|
return (tabState != null)
|
||||||
? AppBarTitle(
|
? AppBarTitle(
|
||||||
tab: tabState,
|
tab: tabState,
|
||||||
onTap: () {
|
onTap: () async {
|
||||||
ref
|
await context.push(
|
||||||
.read(
|
WebPageRoute(
|
||||||
overlayDialogControllerProvider
|
url: tabState.url.toString(),
|
||||||
.notifier,
|
).location,
|
||||||
)
|
extra: tabState,
|
||||||
.show(
|
);
|
||||||
WebPageDialog(
|
|
||||||
url: tabState.url,
|
|
||||||
precachedInfo: tabState,
|
|
||||||
onDismiss: ref
|
|
||||||
.read(
|
|
||||||
overlayDialogControllerProvider
|
|
||||||
.notifier,
|
|
||||||
)
|
|
||||||
.dismiss,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
: const SizedBox.shrink();
|
: const SizedBox.shrink();
|
||||||
@@ -261,22 +255,58 @@ class BrowserScreen extends HookConsumerWidget {
|
|||||||
// ),
|
// ),
|
||||||
// ),
|
// ),
|
||||||
// ),
|
// ),
|
||||||
TabsActionButton(
|
MenuAnchor(
|
||||||
isActive: displayedSheet is ViewTabsSheet,
|
controller: tabMenuController,
|
||||||
onTap: () {
|
builder: (context, controller, child) {
|
||||||
if (displayedSheet case ViewTabsSheet()) {
|
return child!;
|
||||||
ref
|
|
||||||
.read(bottomSheetControllerProvider.notifier)
|
|
||||||
.dismiss();
|
|
||||||
} else {
|
|
||||||
ref
|
|
||||||
.read(bottomSheetControllerProvider.notifier)
|
|
||||||
.show(ViewTabsSheet());
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
menuChildren: [
|
||||||
|
if (selectedTabId != null)
|
||||||
|
MenuItemButton(
|
||||||
|
onPressed: () async {
|
||||||
|
await ref
|
||||||
|
.read(
|
||||||
|
tabRepositoryProvider.notifier,
|
||||||
|
)
|
||||||
|
.closeTab(selectedTabId);
|
||||||
|
},
|
||||||
|
leadingIcon: const Icon(Icons.close),
|
||||||
|
child: const Text('Close Tab'),
|
||||||
|
),
|
||||||
|
MenuItemButton(
|
||||||
|
onPressed: () async {
|
||||||
|
await ref
|
||||||
|
.read(tabRepositoryProvider.notifier)
|
||||||
|
.addTab(url: Uri.https('kagi.com'));
|
||||||
|
},
|
||||||
|
leadingIcon: const Icon(Icons.add),
|
||||||
|
child: const Text('Add Tab'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
child: TabsActionButton(
|
||||||
|
isActive: displayedSheet is ViewTabsSheet,
|
||||||
|
onTap: () {
|
||||||
|
if (displayedSheet case ViewTabsSheet()) {
|
||||||
|
ref
|
||||||
|
.read(bottomSheetControllerProvider.notifier)
|
||||||
|
.dismiss();
|
||||||
|
} else {
|
||||||
|
ref
|
||||||
|
.read(bottomSheetControllerProvider.notifier)
|
||||||
|
.show(ViewTabsSheet());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onLongPress: () {
|
||||||
|
if (tabMenuController.isOpen) {
|
||||||
|
tabMenuController.close();
|
||||||
|
} else {
|
||||||
|
tabMenuController.open();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
MenuAnchor(
|
MenuAnchor(
|
||||||
controller: menuController,
|
controller: trippleDotMenuController,
|
||||||
builder: (context, controller, child) {
|
builder: (context, controller, child) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(right: 4.0),
|
padding: const EdgeInsets.only(right: 4.0),
|
||||||
@@ -487,7 +517,7 @@ class BrowserScreen extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
|
|
||||||
await controller.reload();
|
await controller.reload();
|
||||||
menuController.close();
|
trippleDotMenuController.close();
|
||||||
},
|
},
|
||||||
leadingIcon: const Icon(Icons.refresh),
|
leadingIcon: const Icon(Icons.refresh),
|
||||||
child: const Text('Reload'),
|
child: const Text('Reload'),
|
||||||
@@ -514,7 +544,7 @@ class BrowserScreen extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
|
|
||||||
await controller.goBack();
|
await controller.goBack();
|
||||||
menuController.close();
|
trippleDotMenuController.close();
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.arrow_back),
|
icon: const Icon(Icons.arrow_back),
|
||||||
)
|
)
|
||||||
@@ -526,7 +556,7 @@ class BrowserScreen extends HookConsumerWidget {
|
|||||||
.notifier,
|
.notifier,
|
||||||
)
|
)
|
||||||
.closeTab(selectedTabId);
|
.closeTab(selectedTabId);
|
||||||
menuController.close();
|
trippleDotMenuController.close();
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.close),
|
icon: const Icon(Icons.close),
|
||||||
),
|
),
|
||||||
@@ -546,7 +576,7 @@ class BrowserScreen extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
|
|
||||||
await controller.goForward();
|
await controller.goForward();
|
||||||
menuController.close();
|
trippleDotMenuController.close();
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
icon: const Icon(Icons.arrow_forward),
|
icon: const Icon(Icons.arrow_forward),
|
||||||
@@ -556,6 +586,15 @@ class BrowserScreen extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
MenuItemButton(
|
||||||
|
onPressed: () async {
|
||||||
|
final x =
|
||||||
|
await context.push(UserAuthRoute().location);
|
||||||
|
print(x);
|
||||||
|
},
|
||||||
|
leadingIcon: const Icon(Icons.info),
|
||||||
|
child: const Text('Auth'),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -744,25 +783,36 @@ class BrowserScreen extends HookConsumerWidget {
|
|||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
child: switch (displayedSheet) {
|
child: switch (displayedSheet) {
|
||||||
ViewTabsSheet() => DraggableScrollableSheet(
|
ViewTabsSheet() => HookBuilder(
|
||||||
key: ValueKey(displayedSheet),
|
builder: (localContext) {
|
||||||
expand: false,
|
final draggableScrollableController =
|
||||||
minChildSize: 0.1,
|
useDraggableScrollableController();
|
||||||
maxChildSize: _realtiveSafeArea(context),
|
|
||||||
builder: (context, scrollController) {
|
return DraggableScrollableSheet(
|
||||||
return ClipRRect(
|
key: ValueKey(displayedSheet),
|
||||||
borderRadius: const BorderRadius.only(
|
controller: draggableScrollableController,
|
||||||
topLeft: Radius.circular(28),
|
expand: false,
|
||||||
topRight: Radius.circular(28),
|
minChildSize: 0.1,
|
||||||
),
|
maxChildSize: _realtiveSafeArea(context),
|
||||||
child: ViewTabsSheetWidget(
|
builder: (context, scrollController) {
|
||||||
sheetScrollController: scrollController,
|
return ClipRRect(
|
||||||
onClose: () {
|
borderRadius: const BorderRadius.only(
|
||||||
ref
|
topLeft: Radius.circular(28),
|
||||||
.read(bottomSheetControllerProvider.notifier)
|
topRight: Radius.circular(28),
|
||||||
.dismiss();
|
),
|
||||||
},
|
child: ViewTabsSheetWidget(
|
||||||
),
|
sheetScrollController: scrollController,
|
||||||
|
draggableScrollableController:
|
||||||
|
draggableScrollableController,
|
||||||
|
onClose: () {
|
||||||
|
ref
|
||||||
|
.read(bottomSheetControllerProvider
|
||||||
|
.notifier)
|
||||||
|
.dismiss();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -794,6 +844,60 @@ class BrowserScreen extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
final TabQaChatSheet parameter => HookBuilder(
|
||||||
|
builder: (localContext) {
|
||||||
|
final draggableScrollableController =
|
||||||
|
useDraggableScrollableController();
|
||||||
|
|
||||||
|
return DraggableScrollableSheet(
|
||||||
|
key: ValueKey(displayedSheet),
|
||||||
|
controller: draggableScrollableController,
|
||||||
|
expand: false,
|
||||||
|
minChildSize: 0.1,
|
||||||
|
maxChildSize: _realtiveSafeArea(context),
|
||||||
|
builder: (context, scrollController) {
|
||||||
|
return ClipRRect(
|
||||||
|
borderRadius: const BorderRadius.only(
|
||||||
|
topLeft: Radius.circular(28),
|
||||||
|
topRight: Radius.circular(28),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
DraggableScrollableHeader(
|
||||||
|
controller: draggableScrollableController,
|
||||||
|
child: Material(
|
||||||
|
child: SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: Center(
|
||||||
|
child: Container(
|
||||||
|
width: 40,
|
||||||
|
height: 4,
|
||||||
|
margin: const EdgeInsets.symmetric(
|
||||||
|
vertical: 16.0,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey[300],
|
||||||
|
borderRadius:
|
||||||
|
BorderRadius.circular(2),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: TabQaChat(
|
||||||
|
chatId: parameter.chatId,
|
||||||
|
scrollController: scrollController,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import 'package:lensai/features/geckoview/domain/providers.dart';
|
|||||||
import 'package:lensai/features/geckoview/domain/providers/tab_state.dart';
|
import 'package:lensai/features/geckoview/domain/providers/tab_state.dart';
|
||||||
import 'package:lensai/features/geckoview/domain/providers/web_extensions_state.dart';
|
import 'package:lensai/features/geckoview/domain/providers/web_extensions_state.dart';
|
||||||
import 'package:lensai/features/geckoview/domain/repositories/tab.dart';
|
import 'package:lensai/features/geckoview/domain/repositories/tab.dart';
|
||||||
|
import 'package:lensai/features/geckoview/features/tabs/features/vector_store/domain/repositories/document.dart';
|
||||||
import 'package:lensai/features/user/domain/repositories/cache.dart';
|
import 'package:lensai/features/user/domain/repositories/cache.dart';
|
||||||
|
|
||||||
class BrowserView extends StatefulHookConsumerWidget {
|
class BrowserView extends StatefulHookConsumerWidget {
|
||||||
@@ -87,6 +88,7 @@ class _BrowserViewState extends ConsumerState<BrowserView>
|
|||||||
|
|
||||||
//Initialize and register dependencies
|
//Initialize and register dependencies
|
||||||
ref.listenManual(tabRepositoryProvider, (previous, next) {});
|
ref.listenManual(tabRepositoryProvider, (previous, next) {});
|
||||||
|
ref.listenManual(documentRepositoryProvider, (previous, next) {});
|
||||||
|
|
||||||
ref.listenManual(
|
ref.listenManual(
|
||||||
selectionActionServiceProvider,
|
selectionActionServiceProvider,
|
||||||
|
|||||||
+32
@@ -0,0 +1,32 @@
|
|||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class DraggableScrollableHeader extends StatelessWidget {
|
||||||
|
final DraggableScrollableController controller;
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
const DraggableScrollableHeader({
|
||||||
|
super.key,
|
||||||
|
required this.controller,
|
||||||
|
required this.child,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return GestureDetector(
|
||||||
|
onVerticalDragUpdate: (details) {
|
||||||
|
// Use the DraggableScrollableSheet's controller
|
||||||
|
controller.jumpTo(
|
||||||
|
min(
|
||||||
|
1,
|
||||||
|
controller.pixelsToSize(
|
||||||
|
controller.pixels - details.delta.dy,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+8
-8
@@ -79,10 +79,10 @@ class CreateTabSheetWidget extends HookConsumerWidget {
|
|||||||
TabBar(
|
TabBar(
|
||||||
controller: tabController,
|
controller: tabController,
|
||||||
tabs: [
|
tabs: [
|
||||||
Tab(
|
// Tab(
|
||||||
icon: Icon(KagiTool.search.icon),
|
// icon: Icon(KagiTool.search.icon),
|
||||||
text: 'Search',
|
// text: 'Search',
|
||||||
),
|
// ),
|
||||||
Tab(
|
Tab(
|
||||||
icon: Icon(KagiTool.summarizer.icon),
|
icon: Icon(KagiTool.summarizer.icon),
|
||||||
text: 'Summarize',
|
text: 'Summarize',
|
||||||
@@ -99,10 +99,10 @@ class CreateTabSheetWidget extends HookConsumerWidget {
|
|||||||
child: ExpandablePageView(
|
child: ExpandablePageView(
|
||||||
controller: pageController,
|
controller: pageController,
|
||||||
children: [
|
children: [
|
||||||
SearchTab(
|
// SearchTab(
|
||||||
sharedContent: sharedContent,
|
// sharedContent: sharedContent,
|
||||||
onSubmit: onSubmit,
|
// onSubmit: onSubmit,
|
||||||
),
|
// ),
|
||||||
SummarizeTab(
|
SummarizeTab(
|
||||||
sharedContent: sharedContent,
|
sharedContent: sharedContent,
|
||||||
onSubmit: onSubmit,
|
onSubmit: onSubmit,
|
||||||
|
|||||||
+169
-181
@@ -10,9 +10,10 @@ import 'package:lensai/features/geckoview/domain/providers/tab_state.dart';
|
|||||||
import 'package:lensai/features/geckoview/domain/repositories/tab.dart';
|
import 'package:lensai/features/geckoview/domain/repositories/tab.dart';
|
||||||
import 'package:lensai/features/geckoview/features/browser/domain/providers.dart';
|
import 'package:lensai/features/geckoview/features/browser/domain/providers.dart';
|
||||||
import 'package:lensai/features/geckoview/features/browser/presentation/dialogs/tab_action.dart';
|
import 'package:lensai/features/geckoview/features/browser/presentation/dialogs/tab_action.dart';
|
||||||
import 'package:lensai/features/geckoview/features/browser/presentation/widgets/speech_to_text_button.dart';
|
import 'package:lensai/features/geckoview/features/browser/presentation/widgets/draggable_scrollable_header.dart';
|
||||||
|
import 'package:lensai/presentation/widgets/speech_to_text_button.dart';
|
||||||
import 'package:lensai/features/geckoview/features/browser/presentation/widgets/tab_preview.dart';
|
import 'package:lensai/features/geckoview/features/browser/presentation/widgets/tab_preview.dart';
|
||||||
import 'package:lensai/features/geckoview/features/controllers/overlay_dialog.dart';
|
import 'package:lensai/features/geckoview/domain/controllers/overlay_dialog.dart';
|
||||||
import 'package:lensai/features/geckoview/features/tabs/domain/providers/selected_container.dart';
|
import 'package:lensai/features/geckoview/features/tabs/domain/providers/selected_container.dart';
|
||||||
import 'package:lensai/features/geckoview/features/tabs/domain/repositories/container.dart';
|
import 'package:lensai/features/geckoview/features/tabs/domain/repositories/container.dart';
|
||||||
import 'package:lensai/features/geckoview/features/tabs/domain/repositories/tab.dart';
|
import 'package:lensai/features/geckoview/features/tabs/domain/repositories/tab.dart';
|
||||||
@@ -22,6 +23,8 @@ import 'package:lensai/presentation/hooks/listenable_callback.dart';
|
|||||||
import 'package:reorderable_grid/reorderable_grid.dart';
|
import 'package:reorderable_grid/reorderable_grid.dart';
|
||||||
|
|
||||||
class _Tab extends HookConsumerWidget {
|
class _Tab extends HookConsumerWidget {
|
||||||
|
static const headerSize = 124.0;
|
||||||
|
|
||||||
final VoidCallback onClose;
|
final VoidCallback onClose;
|
||||||
|
|
||||||
const _Tab({required this.onClose});
|
const _Tab({required this.onClose});
|
||||||
@@ -148,40 +151,15 @@ class _Tab extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _SliverHeaderDelagate extends SliverPersistentHeaderDelegate {
|
|
||||||
static const headerSize = 124.0;
|
|
||||||
|
|
||||||
final VoidCallback onClose;
|
|
||||||
|
|
||||||
_SliverHeaderDelagate({required this.onClose});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(
|
|
||||||
BuildContext context,
|
|
||||||
double shrinkOffset,
|
|
||||||
bool overlapsContent,
|
|
||||||
) {
|
|
||||||
return _Tab(onClose: onClose);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
double get minExtent => headerSize;
|
|
||||||
|
|
||||||
@override
|
|
||||||
double get maxExtent => headerSize;
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) =>
|
|
||||||
false;
|
|
||||||
}
|
|
||||||
|
|
||||||
class ViewTabsSheetWidget extends HookConsumerWidget {
|
class ViewTabsSheetWidget extends HookConsumerWidget {
|
||||||
final ScrollController sheetScrollController;
|
final ScrollController sheetScrollController;
|
||||||
|
final DraggableScrollableController draggableScrollableController;
|
||||||
final VoidCallback onClose;
|
final VoidCallback onClose;
|
||||||
|
|
||||||
const ViewTabsSheetWidget({
|
const ViewTabsSheetWidget({
|
||||||
required this.onClose,
|
required this.onClose,
|
||||||
required this.sheetScrollController,
|
required this.sheetScrollController,
|
||||||
|
required this.draggableScrollableController,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -209,181 +187,191 @@ class ViewTabsSheetWidget extends HookConsumerWidget {
|
|||||||
return Stack(
|
return Stack(
|
||||||
alignment: Alignment.bottomRight,
|
alignment: Alignment.bottomRight,
|
||||||
children: [
|
children: [
|
||||||
CustomScrollView(
|
Column(
|
||||||
controller: sheetScrollController,
|
children: [
|
||||||
slivers: [
|
DraggableScrollableHeader(
|
||||||
SliverPersistentHeader(
|
controller: draggableScrollableController,
|
||||||
pinned: true,
|
child: _Tab(onClose: onClose),
|
||||||
delegate: _SliverHeaderDelagate(onClose: onClose),
|
|
||||||
),
|
),
|
||||||
HookConsumer(
|
Expanded(
|
||||||
builder: (context, ref, child) {
|
child: CustomScrollView(
|
||||||
final container = ref.watch(selectedContainerProvider);
|
controller: sheetScrollController,
|
||||||
|
slivers: [
|
||||||
|
HookConsumer(
|
||||||
|
builder: (context, ref, child) {
|
||||||
|
final container = ref.watch(selectedContainerProvider);
|
||||||
|
|
||||||
final filteredTabs = ref
|
final filteredTabs = ref
|
||||||
.watch(
|
.watch(
|
||||||
seamlessFilteredTabsProvider(container).select(
|
seamlessFilteredTabsProvider(container).select(
|
||||||
(value) => EquatableCollection(value, immutable: true),
|
(value) =>
|
||||||
),
|
EquatableCollection(value, immutable: true),
|
||||||
)
|
),
|
||||||
.collection;
|
)
|
||||||
|
.collection;
|
||||||
|
|
||||||
final activeTab = ref.watch(selectedTabProvider);
|
final activeTab = ref.watch(selectedTabProvider);
|
||||||
|
|
||||||
final itemHeight = useMemoized(
|
final itemHeight = useMemoized(
|
||||||
() => _calculateItemHeight(
|
() => _calculateItemHeight(
|
||||||
screenWidth: MediaQuery.of(context).size.width,
|
screenWidth: MediaQuery.of(context).size.width,
|
||||||
childAspectRatio: 0.75,
|
childAspectRatio: 0.75,
|
||||||
horizontalPadding: 4.0,
|
horizontalPadding: 4.0,
|
||||||
mainAxisSpacing: 8.0,
|
mainAxisSpacing: 8.0,
|
||||||
crossAxisSpacing: 8.0,
|
crossAxisSpacing: 8.0,
|
||||||
crossAxisCount: 2,
|
crossAxisCount: 2,
|
||||||
),
|
),
|
||||||
[MediaQuery.of(context).size.width],
|
[MediaQuery.of(context).size.width],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
() {
|
() {
|
||||||
final index = filteredTabs
|
final index = filteredTabs
|
||||||
.indexWhere((webView) => webView == activeTab);
|
.indexWhere((webView) => webView == activeTab);
|
||||||
|
|
||||||
if (index > -1) {
|
if (index > -1) {
|
||||||
final offset = (index ~/ 2) * itemHeight;
|
final offset = (index ~/ 2) * itemHeight;
|
||||||
|
|
||||||
if (offset != sheetScrollController.offset) {
|
if (offset != sheetScrollController.offset) {
|
||||||
unawaited(
|
unawaited(
|
||||||
sheetScrollController.animateTo(
|
sheetScrollController.animateTo(
|
||||||
offset,
|
offset,
|
||||||
duration: const Duration(milliseconds: 200),
|
duration: const Duration(milliseconds: 200),
|
||||||
curve: Curves.easeInOut,
|
curve: Curves.easeInOut,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
[filteredTabs, activeTab],
|
[filteredTabs, activeTab],
|
||||||
);
|
);
|
||||||
|
|
||||||
final tabs = useMemoized(
|
final tabs = useMemoized(
|
||||||
() {
|
() {
|
||||||
return filteredTabs
|
return filteredTabs
|
||||||
.mapIndexed(
|
.mapIndexed(
|
||||||
(index, tabId) =>
|
(index, tabId) =>
|
||||||
ReorderableGridDelayedDragStartListener(
|
ReorderableGridDelayedDragStartListener(
|
||||||
key: ValueKey(tabId),
|
key: ValueKey(tabId),
|
||||||
index: index,
|
index: index,
|
||||||
child: Consumer(
|
child: Consumer(
|
||||||
builder: (context, ref, child) {
|
builder: (context, ref, child) {
|
||||||
final tab = ref.watch(tabStateProvider(tabId));
|
final tab =
|
||||||
return (tab != null)
|
ref.watch(tabStateProvider(tabId));
|
||||||
? TabPreview(
|
return (tab != null)
|
||||||
tab: tab,
|
? TabPreview(
|
||||||
isActive: tabId == activeTab,
|
tab: tab,
|
||||||
onTap: () async {
|
isActive: tabId == activeTab,
|
||||||
if (tabId != activeTab) {
|
onTap: () async {
|
||||||
//Close first to avoid rebuilds
|
if (tabId != activeTab) {
|
||||||
onClose();
|
//Close first to avoid rebuilds
|
||||||
await ref
|
onClose();
|
||||||
.read(
|
await ref
|
||||||
tabRepositoryProvider
|
|
||||||
.notifier,
|
|
||||||
)
|
|
||||||
.selectTab(tab.id);
|
|
||||||
} else {
|
|
||||||
onClose();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onDoubleTap: () {
|
|
||||||
ref
|
|
||||||
.read(
|
|
||||||
overlayDialogControllerProvider
|
|
||||||
.notifier,
|
|
||||||
)
|
|
||||||
.show(
|
|
||||||
TabActionDialog(
|
|
||||||
initialTab: tab,
|
|
||||||
onDismiss: ref
|
|
||||||
.read(
|
.read(
|
||||||
overlayDialogControllerProvider
|
tabRepositoryProvider
|
||||||
.notifier,
|
.notifier,
|
||||||
)
|
)
|
||||||
.dismiss,
|
.selectTab(tab.id);
|
||||||
),
|
} else {
|
||||||
);
|
onClose();
|
||||||
},
|
}
|
||||||
onDelete: () async {
|
},
|
||||||
await ref
|
onDoubleTap: () {
|
||||||
.read(
|
ref
|
||||||
tabRepositoryProvider.notifier,
|
.read(
|
||||||
)
|
overlayDialogControllerProvider
|
||||||
.closeTab(tab.id);
|
.notifier,
|
||||||
},
|
)
|
||||||
)
|
.show(
|
||||||
: const SizedBox.shrink();
|
TabActionDialog(
|
||||||
},
|
initialTab: tab,
|
||||||
),
|
onDismiss: ref
|
||||||
|
.read(
|
||||||
|
overlayDialogControllerProvider
|
||||||
|
.notifier,
|
||||||
|
)
|
||||||
|
.dismiss,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onDelete: () async {
|
||||||
|
await ref
|
||||||
|
.read(
|
||||||
|
tabRepositoryProvider
|
||||||
|
.notifier,
|
||||||
|
)
|
||||||
|
.closeTab(tab.id);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: const SizedBox.shrink();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
},
|
||||||
|
[
|
||||||
|
EquatableCollection(filteredTabs, immutable: true),
|
||||||
|
activeTab,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
return SliverPadding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
||||||
|
sliver: SliverReorderableGrid(
|
||||||
|
gridDelegate:
|
||||||
|
const SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
|
//Sync values for itemHeight calculation _calculateItemHeight
|
||||||
|
childAspectRatio: 0.75,
|
||||||
|
mainAxisSpacing: 8.0,
|
||||||
|
crossAxisSpacing: 8.0,
|
||||||
|
crossAxisCount: 2,
|
||||||
),
|
),
|
||||||
)
|
itemCount: tabs.length,
|
||||||
.toList();
|
itemBuilder: (context, index) => tabs[index],
|
||||||
},
|
onReorder: (oldIndex, newIndex) async {
|
||||||
[
|
final containerRepository =
|
||||||
EquatableCollection(filteredTabs, immutable: true),
|
ref.read(containerRepositoryProvider.notifier);
|
||||||
activeTab
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
return SliverPadding(
|
final tabId = filteredTabs[oldIndex];
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
final containerId = await ref
|
||||||
sliver: SliverReorderableGrid(
|
.read(tabDataRepositoryProvider.notifier)
|
||||||
gridDelegate:
|
.containerTabId(tabId);
|
||||||
const SliverGridDelegateWithFixedCrossAxisCount(
|
|
||||||
//Sync values for itemHeight calculation _calculateItemHeight
|
|
||||||
childAspectRatio: 0.75,
|
|
||||||
mainAxisSpacing: 8.0,
|
|
||||||
crossAxisSpacing: 8.0,
|
|
||||||
crossAxisCount: 2,
|
|
||||||
),
|
|
||||||
itemCount: tabs.length,
|
|
||||||
itemBuilder: (context, index) => tabs[index],
|
|
||||||
onReorder: (oldIndex, newIndex) async {
|
|
||||||
final containerRepository =
|
|
||||||
ref.read(containerRepositoryProvider.notifier);
|
|
||||||
|
|
||||||
final tabId = filteredTabs[oldIndex];
|
final String key;
|
||||||
final containerId = await ref
|
if (newIndex <= 0) {
|
||||||
.read(tabDataRepositoryProvider.notifier)
|
key = await containerRepository
|
||||||
.containerTabId(tabId);
|
.getLeadingOrderKey(containerId);
|
||||||
|
} else if (newIndex >= filteredTabs.length - 1) {
|
||||||
|
key = await containerRepository
|
||||||
|
.getTrailingOrderKey(containerId);
|
||||||
|
} else {
|
||||||
|
final orderAfterIndex = newIndex;
|
||||||
|
key =
|
||||||
|
await containerRepository.getOrderKeyAfterTab(
|
||||||
|
filteredTabs[orderAfterIndex],
|
||||||
|
containerId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
final String key;
|
await ref
|
||||||
if (newIndex <= 0) {
|
.read(tabDataRepositoryProvider.notifier)
|
||||||
key = await containerRepository
|
.assignOrderKey(tabId, key);
|
||||||
.getLeadingOrderKey(containerId);
|
},
|
||||||
} else if (newIndex >= filteredTabs.length - 1) {
|
),
|
||||||
key = await containerRepository
|
);
|
||||||
.getTrailingOrderKey(containerId);
|
|
||||||
} else {
|
|
||||||
final orderAfterIndex = newIndex;
|
|
||||||
key = await containerRepository.getOrderKeyAfterTab(
|
|
||||||
filteredTabs[orderAfterIndex],
|
|
||||||
containerId,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
await ref
|
|
||||||
.read(tabDataRepositoryProvider.notifier)
|
|
||||||
.assignOrderKey(tabId, key);
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
],
|
||||||
},
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(
|
padding: const EdgeInsets.only(
|
||||||
top: _SliverHeaderDelagate.headerSize + 4,
|
top: _Tab.headerSize + 4,
|
||||||
right: 4,
|
right: 4,
|
||||||
),
|
),
|
||||||
child: FloatingActionButton.small(
|
child: FloatingActionButton.small(
|
||||||
|
|||||||
+3
@@ -5,9 +5,11 @@ import 'package:lensai/features/geckoview/domain/providers/tab_list.dart';
|
|||||||
class TabsActionButton extends HookConsumerWidget {
|
class TabsActionButton extends HookConsumerWidget {
|
||||||
final bool isActive;
|
final bool isActive;
|
||||||
final VoidCallback onTap;
|
final VoidCallback onTap;
|
||||||
|
final VoidCallback onLongPress;
|
||||||
|
|
||||||
const TabsActionButton({
|
const TabsActionButton({
|
||||||
required this.onTap,
|
required this.onTap,
|
||||||
|
required this.onLongPress,
|
||||||
this.isActive = false,
|
this.isActive = false,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
@@ -20,6 +22,7 @@ class TabsActionButton extends HookConsumerWidget {
|
|||||||
|
|
||||||
return InkWell(
|
return InkWell(
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
|
onLongPress: onLongPress,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
horizontal: 8.0,
|
horizontal: 8.0,
|
||||||
|
|||||||
+7
-1
@@ -5,6 +5,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||||||
import 'package:lensai/features/geckoview/domain/entities/readerable_state.dart';
|
import 'package:lensai/features/geckoview/domain/entities/readerable_state.dart';
|
||||||
import 'package:lensai/features/geckoview/domain/providers/tab_state.dart';
|
import 'package:lensai/features/geckoview/domain/providers/tab_state.dart';
|
||||||
import 'package:lensai/features/geckoview/features/readerview/presentation/controllers/readerable.dart';
|
import 'package:lensai/features/geckoview/features/readerview/presentation/controllers/readerable.dart';
|
||||||
|
import 'package:lensai/features/user/domain/repositories/settings.dart';
|
||||||
import 'package:lensai/presentation/widgets/animate_gradient_shader.dart';
|
import 'package:lensai/presentation/widgets/animate_gradient_shader.dart';
|
||||||
|
|
||||||
class ReaderButton extends HookConsumerWidget {
|
class ReaderButton extends HookConsumerWidget {
|
||||||
@@ -14,6 +15,10 @@ class ReaderButton extends HookConsumerWidget {
|
|||||||
|
|
||||||
final readerChanging = ref.watch(readerableScreenControllerProvider);
|
final readerChanging = ref.watch(readerableScreenControllerProvider);
|
||||||
|
|
||||||
|
final enableReadability = ref.watch(
|
||||||
|
settingsRepositoryProvider.select((value) => value.enableReadability),
|
||||||
|
);
|
||||||
|
|
||||||
final readerabilityState = ref.watch(
|
final readerabilityState = ref.watch(
|
||||||
selectedTabStateProvider.select(
|
selectedTabStateProvider.select(
|
||||||
(state) => state?.readerableState ?? ReaderableState.$default(),
|
(state) => state?.readerableState ?? ReaderableState.$default(),
|
||||||
@@ -34,7 +39,8 @@ class ReaderButton extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return Visibility(
|
return Visibility(
|
||||||
visible: readerabilityState.readerable,
|
visible: readerabilityState.readerable &&
|
||||||
|
(enableReadability || readerabilityState.active),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
vertical: 15.0,
|
vertical: 15.0,
|
||||||
|
|||||||
@@ -110,38 +110,25 @@ class TabDao extends DatabaseAccessor<TabDatabase> with _$TabDaoMixin {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> updateTab(
|
Future<void> updateTabContent(
|
||||||
String id, {
|
String id, {
|
||||||
Value<String?> url = const Value.absent(),
|
required bool isProbablyReaderable,
|
||||||
Value<String?> title = const Value.absent(),
|
required String? extractedContentMarkdown,
|
||||||
Value<bool> isProbablyReaderable = const Value.absent(),
|
required String? extractedContentPlain,
|
||||||
Value<String?> extractedContentMarkdown = const Value.absent(),
|
required String? fullContentMarkdown,
|
||||||
Value<String?> extractedContentPlain = const Value.absent(),
|
required String? fullContentPlain,
|
||||||
Value<String?> fullContentMarkdown = const Value.absent(),
|
|
||||||
Value<String?> fullContentPlain = const Value.absent(),
|
|
||||||
}) async {
|
}) async {
|
||||||
final doUpdate = url != const Value.absent() ||
|
final statement = _updateByIdStatement(id);
|
||||||
title != const Value.absent() ||
|
|
||||||
isProbablyReaderable != const Value.absent() ||
|
|
||||||
extractedContentMarkdown != const Value.absent() ||
|
|
||||||
extractedContentPlain != const Value.absent() ||
|
|
||||||
fullContentMarkdown != const Value.absent() ||
|
|
||||||
fullContentPlain != const Value.absent();
|
|
||||||
|
|
||||||
if (doUpdate) {
|
await statement.write(
|
||||||
final statement = _updateByIdStatement(id);
|
TabCompanion(
|
||||||
await statement.write(
|
isProbablyReaderable: Value(isProbablyReaderable),
|
||||||
TabCompanion(
|
extractedContentMarkdown: Value(extractedContentMarkdown),
|
||||||
url: url,
|
extractedContentPlain: Value(extractedContentPlain),
|
||||||
title: title,
|
fullContentMarkdown: Value(fullContentMarkdown),
|
||||||
isProbablyReaderable: isProbablyReaderable,
|
fullContentPlain: Value(fullContentPlain),
|
||||||
extractedContentMarkdown: extractedContentMarkdown,
|
),
|
||||||
extractedContentPlain: extractedContentPlain,
|
);
|
||||||
fullContentMarkdown: fullContentMarkdown,
|
|
||||||
fullContentPlain: fullContentPlain,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> updateTabs(
|
Future<void> updateTabs(
|
||||||
@@ -209,7 +196,7 @@ class TabDao extends DatabaseAccessor<TabDatabase> with _$TabDaoMixin {
|
|||||||
|
|
||||||
if (ftsQuery.isNotEmpty) {
|
if (ftsQuery.isNotEmpty) {
|
||||||
return db.queryTabsFullContent(
|
return db.queryTabsFullContent(
|
||||||
query: db.buildFtsQuery(searchString),
|
query: ftsQuery,
|
||||||
snippetLength: snippetLength,
|
snippetLength: snippetLength,
|
||||||
beforeMatch: matchPrefix,
|
beforeMatch: matchPrefix,
|
||||||
afterMatch: matchSuffix,
|
afterMatch: matchSuffix,
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter/widgets.dart' show Color, IconData;
|
import 'package:flutter/widgets.dart' show Color, IconData;
|
||||||
import 'package:lensai/data/database/converters/color.dart';
|
import 'package:lensai/data/database/converters/color.dart';
|
||||||
import 'package:lensai/data/database/converters/icon_data.dart';
|
import 'package:lensai/data/database/converters/icon_data.dart';
|
||||||
@@ -7,6 +6,9 @@ import 'package:lensai/features/geckoview/features/tabs/data/database/daos/conta
|
|||||||
import 'package:lensai/features/geckoview/features/tabs/data/database/daos/tab.dart';
|
import 'package:lensai/features/geckoview/features/tabs/data/database/daos/tab.dart';
|
||||||
import 'package:lensai/features/geckoview/features/tabs/data/models/container_data.dart';
|
import 'package:lensai/features/geckoview/features/tabs/data/models/container_data.dart';
|
||||||
import 'package:lensai/features/geckoview/features/tabs/data/models/tab_query_result.dart';
|
import 'package:lensai/features/geckoview/features/tabs/data/models/tab_query_result.dart';
|
||||||
|
import 'package:lensai/features/geckoview/features/tabs/features/vector_store/data/database/daos/vector.dart';
|
||||||
|
import 'package:lensai/features/geckoview/features/tabs/features/vector_store/data/database/database.dart';
|
||||||
|
import 'package:lensai/features/geckoview/features/tabs/features/vector_store/data/models/vector_result.dart';
|
||||||
import 'package:lensai/features/search/domain/entities/abstract/i_query_builder.dart';
|
import 'package:lensai/features/search/domain/entities/abstract/i_query_builder.dart';
|
||||||
import 'package:lensai/features/search/domain/fts_tokenizer.dart';
|
import 'package:lensai/features/search/domain/fts_tokenizer.dart';
|
||||||
import 'package:lensai/features/search/domain/unix_tokenizer.dart';
|
import 'package:lensai/features/search/domain/unix_tokenizer.dart';
|
||||||
@@ -15,9 +17,11 @@ part 'database.g.dart';
|
|||||||
|
|
||||||
@DriftDatabase(
|
@DriftDatabase(
|
||||||
include: {'database.drift'},
|
include: {'database.drift'},
|
||||||
daos: [ContainerDao, TabDao],
|
daos: [ContainerDao, TabDao, VectorDao],
|
||||||
)
|
)
|
||||||
class TabDatabase extends _$TabDatabase implements IQueryBuilder {
|
class TabDatabase extends _$TabDatabase with TrigramQueryBuilderMixin {
|
||||||
|
final int embeddingDimensions;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
final int schemaVersion = 2;
|
final int schemaVersion = 2;
|
||||||
|
|
||||||
@@ -26,61 +30,25 @@ class TabDatabase extends _$TabDatabase implements IQueryBuilder {
|
|||||||
@override
|
@override
|
||||||
final int ftsMinTokenLength = 3;
|
final int ftsMinTokenLength = 3;
|
||||||
|
|
||||||
@override
|
|
||||||
String buildFtsQuery(String input) {
|
|
||||||
final ftsQueryBuilder = TrigramQueryBuilder.tokenize(
|
|
||||||
input: input,
|
|
||||||
minTokenLength: ftsMinTokenLength,
|
|
||||||
tokenLimit: ftsTokenLimit,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (ftsQueryBuilder.hasTokens) {
|
|
||||||
return ftsQueryBuilder.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
String buildLikeQuery(String input) {
|
|
||||||
final likeQueryBuilder = UnixLikeQueryBuilder.tokenize(
|
|
||||||
input: input,
|
|
||||||
minTokenLength: 1,
|
|
||||||
tokenLimit: 5,
|
|
||||||
);
|
|
||||||
|
|
||||||
return likeQueryBuilder.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
MigrationStrategy get migration => MigrationStrategy(
|
MigrationStrategy get migration => MigrationStrategy(
|
||||||
onUpgrade: (m, from, to) async {
|
onCreate: (m) async {
|
||||||
// disable foreign_keys before migrations
|
final migrator =
|
||||||
await customStatement('PRAGMA foreign_keys = OFF');
|
VectorDatabaseMigrator(dimensions: embeddingDimensions);
|
||||||
|
|
||||||
if (from < 2) {
|
await m.database.customStatement(migrator.vectorTableDefinition);
|
||||||
await transaction(() async {
|
|
||||||
// await m.dropColumn(tabLink, 'url');
|
|
||||||
// await m.dropColumn(tabLink, 'title');
|
|
||||||
// await m.dropColumn(tabLink, 'screenshot');
|
|
||||||
|
|
||||||
// await m.alterTable(TableMigration(tab));
|
//instead of m.createAll(); we igoner vec0 table
|
||||||
});
|
for (final entity
|
||||||
}
|
in allSchemaEntities.where((entity) => entity is! DocumentVec)) {
|
||||||
|
await m.create(entity);
|
||||||
// Assert that the schema is valid after migrations
|
|
||||||
if (kDebugMode) {
|
|
||||||
final wrongForeignKeys =
|
|
||||||
await customSelect('PRAGMA foreign_key_check').get();
|
|
||||||
assert(
|
|
||||||
wrongForeignKeys.isEmpty,
|
|
||||||
'${wrongForeignKeys.map((e) => e.data)}',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
beforeOpen: (details) async {
|
beforeOpen: (details) async {
|
||||||
await customStatement('PRAGMA foreign_keys = ON;');
|
await customStatement('PRAGMA foreign_keys = ON;');
|
||||||
|
await optimizeFtsIndex();
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
TabDatabase(super.e);
|
TabDatabase(super.e, {required this.embeddingDimensions});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,14 @@ CREATE TABLE tab (
|
|||||||
timestamp DATETIME NOT NULL
|
timestamp DATETIME NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
|
import '../../features/vector_store/data/database/vector_store.drift';
|
||||||
|
|
||||||
|
-- automativcally remove documents and embeddings on delete
|
||||||
|
-- isnert and updates are managed code side
|
||||||
|
CREATE TRIGGER tab_document_delete AFTER DELETE ON tab BEGIN
|
||||||
|
DELETE FROM document WHERE main_document_id = old.id;
|
||||||
|
END;
|
||||||
|
|
||||||
CREATE VIRTUAL TABLE tab_fts
|
CREATE VIRTUAL TABLE tab_fts
|
||||||
USING fts5(
|
USING fts5(
|
||||||
title,
|
title,
|
||||||
@@ -55,6 +63,9 @@ CREATE TRIGGER tab_after_update AFTER UPDATE ON tab BEGIN
|
|||||||
VALUES (new.rowid, new.title, new.url, new.extracted_content_plain, new.full_content_plain);
|
VALUES (new.rowid, new.title, new.url, new.extracted_content_plain, new.full_content_plain);
|
||||||
END;
|
END;
|
||||||
|
|
||||||
|
optimizeFtsIndex:
|
||||||
|
INSERT INTO tab_fts(tab_fts) VALUES ('optimize');
|
||||||
|
|
||||||
containersWithCount WITH ContainerDataWithCount:
|
containersWithCount WITH ContainerDataWithCount:
|
||||||
SELECT
|
SELECT
|
||||||
container.*,
|
container.*,
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,8 @@
|
|||||||
class TabQueryResult {
|
class TabQueryResult {
|
||||||
final String id;
|
final String id;
|
||||||
|
|
||||||
final String title;
|
final String? title;
|
||||||
final String url;
|
final String? url;
|
||||||
|
|
||||||
final String? extractedContent;
|
final String? extractedContent;
|
||||||
final String? fullContent;
|
final String? fullContent;
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:drift/native.dart';
|
import 'package:drift/native.dart';
|
||||||
|
import 'package:lensai/core/models.dart';
|
||||||
import 'package:lensai/data/database/functions/lexo_rank_functions.dart';
|
import 'package:lensai/data/database/functions/lexo_rank_functions.dart';
|
||||||
import 'package:lensai/features/geckoview/features/tabs/data/database/database.dart';
|
import 'package:lensai/features/geckoview/features/tabs/data/database/database.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
@@ -8,13 +11,16 @@ import 'package:riverpod/riverpod.dart';
|
|||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
import 'package:sqlite3/sqlite3.dart';
|
import 'package:sqlite3/sqlite3.dart';
|
||||||
import 'package:sqlite3_flutter_libs/sqlite3_flutter_libs.dart';
|
import 'package:sqlite3_flutter_libs/sqlite3_flutter_libs.dart';
|
||||||
|
import 'package:sqlite3_vec/sqlite3_vec.dart';
|
||||||
import 'package:universal_io/io.dart';
|
import 'package:universal_io/io.dart';
|
||||||
|
|
||||||
part 'providers.g.dart';
|
part 'providers.g.dart';
|
||||||
|
|
||||||
@Riverpod(keepAlive: true)
|
@Riverpod(keepAlive: true)
|
||||||
TabDatabase tabDatabase(Ref ref) {
|
TabDatabase tabDatabase(Ref ref) {
|
||||||
return TabDatabase(
|
final dimensions = ref.watch(embeddingDimensionsProvider);
|
||||||
|
|
||||||
|
final db = TabDatabase(
|
||||||
LazyDatabase(() async {
|
LazyDatabase(() async {
|
||||||
// put the database file, called db.sqlite here, into the documents folder
|
// put the database file, called db.sqlite here, into the documents folder
|
||||||
// for your app.
|
// for your app.
|
||||||
@@ -33,6 +39,8 @@ TabDatabase tabDatabase(Ref ref) {
|
|||||||
// Explicitly tell it about the correct temporary directory.
|
// Explicitly tell it about the correct temporary directory.
|
||||||
sqlite3.tempDirectory = cachebase;
|
sqlite3.tempDirectory = cachebase;
|
||||||
|
|
||||||
|
Sqlite3Vec.ensureExtensionLoaded();
|
||||||
|
|
||||||
return NativeDatabase.createInBackground(
|
return NativeDatabase.createInBackground(
|
||||||
file,
|
file,
|
||||||
setup: (database) {
|
setup: (database) {
|
||||||
@@ -40,5 +48,12 @@ TabDatabase tabDatabase(Ref ref) {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
|
embeddingDimensions: dimensions,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
ref.onDispose(() {
|
||||||
|
unawaited(db.close());
|
||||||
|
});
|
||||||
|
|
||||||
|
return db;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ part of 'providers.dart';
|
|||||||
// RiverpodGenerator
|
// RiverpodGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$tabDatabaseHash() => r'1379e222fe4d43c119a89ec89a779ef35fbe1563';
|
String _$tabDatabaseHash() => r'940dd2a1f1df6f2a0b77a1a3e9b8318550b832d2';
|
||||||
|
|
||||||
/// See also [tabDatabase].
|
/// See also [tabDatabase].
|
||||||
@ProviderFor(tabDatabase)
|
@ProviderFor(tabDatabase)
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:exceptions/exceptions.dart';
|
||||||
|
import 'package:lensai/features/chat/features/chat_store/data/models/chat_metadata.dart';
|
||||||
|
import 'package:lensai/features/chat/features/chat_store/data/models/message_types.dart';
|
||||||
|
import 'package:lensai/features/chat/features/chat_store/domain/providers.dart';
|
||||||
|
import 'package:lensai/features/chat/features/chat_store/domain/repositories/chat_message.dart';
|
||||||
|
import 'package:lensai/features/geckoview/features/tabs/features/chat/services/qa_memory_chain.dart';
|
||||||
|
import 'package:lensai/features/geckoview/features/tabs/features/vector_store/domain/repositories/document.dart';
|
||||||
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
|
||||||
|
part 'chat_backend.g.dart';
|
||||||
|
|
||||||
|
@Riverpod()
|
||||||
|
class ChatBackend extends _$ChatBackend {
|
||||||
|
late ChatMetadata? _metadata;
|
||||||
|
|
||||||
|
late ChatMessageRepository _chatRepository;
|
||||||
|
|
||||||
|
late QAMemoryChain _qaMemoryChain;
|
||||||
|
|
||||||
|
Future<void>? _embeddingsUpdate;
|
||||||
|
Future<void> prepareEmbeddings() {
|
||||||
|
if (_embeddingsUpdate != null) {
|
||||||
|
return _embeddingsUpdate!;
|
||||||
|
}
|
||||||
|
|
||||||
|
_embeddingsUpdate =
|
||||||
|
ref.read(documentRepositoryProvider.notifier).updateEmbeddings(
|
||||||
|
mainDocumentId: _metadata?.mainDocumentId,
|
||||||
|
contextId: _metadata?.contextId,
|
||||||
|
);
|
||||||
|
|
||||||
|
_embeddingsUpdate!.whenComplete(() => _embeddingsUpdate = null);
|
||||||
|
|
||||||
|
return _embeddingsUpdate!;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Result<void>> processQAMessage(String input) async {
|
||||||
|
//TODO: fix result mess
|
||||||
|
|
||||||
|
final humanMessageResult = await _chatRepository.insertTextMessage(
|
||||||
|
author: MessageAuthor.human,
|
||||||
|
content: input,
|
||||||
|
);
|
||||||
|
|
||||||
|
await prepareEmbeddings();
|
||||||
|
|
||||||
|
return humanMessageResult.flatMapAsync((humanMessage) async {
|
||||||
|
await _chatRepository.setTyping(
|
||||||
|
author: MessageAuthor.ai,
|
||||||
|
typing: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
final result = await _qaMemoryChain.processQuestion(humanMessage.text);
|
||||||
|
await _chatRepository.setTyping(
|
||||||
|
author: MessageAuthor.ai,
|
||||||
|
typing: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
await _chatRepository.insertTextMessage(
|
||||||
|
author: MessageAuthor.ai,
|
||||||
|
content: result.answer,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void build(String chatId) {
|
||||||
|
_metadata = ref.watch(
|
||||||
|
chatMetadataProvider(chatId).select((value) => value.valueOrNull),
|
||||||
|
);
|
||||||
|
|
||||||
|
_chatRepository = ref.watch(chatMessageRepositoryProvider(chatId).notifier);
|
||||||
|
|
||||||
|
_qaMemoryChain = ref.watch(
|
||||||
|
qAMemoryChainProvider(
|
||||||
|
chatId: chatId,
|
||||||
|
mainDocumentId: _metadata?.mainDocumentId,
|
||||||
|
contextId: _metadata?.contextId,
|
||||||
|
).notifier,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,174 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'chat_backend.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// RiverpodGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
String _$chatBackendHash() => r'befdad9878bf0a1a8a196af5e3006978026eb8ba';
|
||||||
|
|
||||||
|
/// Copied from Dart SDK
|
||||||
|
class _SystemHash {
|
||||||
|
_SystemHash._();
|
||||||
|
|
||||||
|
static int combine(int hash, int value) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
hash = 0x1fffffff & (hash + value);
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
|
||||||
|
return hash ^ (hash >> 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int finish(int hash) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
hash = hash ^ (hash >> 11);
|
||||||
|
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class _$ChatBackend extends BuildlessAutoDisposeNotifier<void> {
|
||||||
|
late final String chatId;
|
||||||
|
|
||||||
|
void build(
|
||||||
|
String chatId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// See also [ChatBackend].
|
||||||
|
@ProviderFor(ChatBackend)
|
||||||
|
const chatBackendProvider = ChatBackendFamily();
|
||||||
|
|
||||||
|
/// See also [ChatBackend].
|
||||||
|
class ChatBackendFamily extends Family<void> {
|
||||||
|
/// See also [ChatBackend].
|
||||||
|
const ChatBackendFamily();
|
||||||
|
|
||||||
|
/// See also [ChatBackend].
|
||||||
|
ChatBackendProvider call(
|
||||||
|
String chatId,
|
||||||
|
) {
|
||||||
|
return ChatBackendProvider(
|
||||||
|
chatId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
ChatBackendProvider getProviderOverride(
|
||||||
|
covariant ChatBackendProvider provider,
|
||||||
|
) {
|
||||||
|
return call(
|
||||||
|
provider.chatId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static const Iterable<ProviderOrFamily>? _dependencies = null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
|
||||||
|
|
||||||
|
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
|
||||||
|
_allTransitiveDependencies;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? get name => r'chatBackendProvider';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// See also [ChatBackend].
|
||||||
|
class ChatBackendProvider
|
||||||
|
extends AutoDisposeNotifierProviderImpl<ChatBackend, void> {
|
||||||
|
/// See also [ChatBackend].
|
||||||
|
ChatBackendProvider(
|
||||||
|
String chatId,
|
||||||
|
) : this._internal(
|
||||||
|
() => ChatBackend()..chatId = chatId,
|
||||||
|
from: chatBackendProvider,
|
||||||
|
name: r'chatBackendProvider',
|
||||||
|
debugGetCreateSourceHash:
|
||||||
|
const bool.fromEnvironment('dart.vm.product')
|
||||||
|
? null
|
||||||
|
: _$chatBackendHash,
|
||||||
|
dependencies: ChatBackendFamily._dependencies,
|
||||||
|
allTransitiveDependencies:
|
||||||
|
ChatBackendFamily._allTransitiveDependencies,
|
||||||
|
chatId: chatId,
|
||||||
|
);
|
||||||
|
|
||||||
|
ChatBackendProvider._internal(
|
||||||
|
super._createNotifier, {
|
||||||
|
required super.name,
|
||||||
|
required super.dependencies,
|
||||||
|
required super.allTransitiveDependencies,
|
||||||
|
required super.debugGetCreateSourceHash,
|
||||||
|
required super.from,
|
||||||
|
required this.chatId,
|
||||||
|
}) : super.internal();
|
||||||
|
|
||||||
|
final String chatId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void runNotifierBuild(
|
||||||
|
covariant ChatBackend notifier,
|
||||||
|
) {
|
||||||
|
return notifier.build(
|
||||||
|
chatId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Override overrideWith(ChatBackend Function() create) {
|
||||||
|
return ProviderOverride(
|
||||||
|
origin: this,
|
||||||
|
override: ChatBackendProvider._internal(
|
||||||
|
() => create()..chatId = chatId,
|
||||||
|
from: from,
|
||||||
|
name: null,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
debugGetCreateSourceHash: null,
|
||||||
|
chatId: chatId,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
AutoDisposeNotifierProviderElement<ChatBackend, void> createElement() {
|
||||||
|
return _ChatBackendProviderElement(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return other is ChatBackendProvider && other.chatId == chatId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode {
|
||||||
|
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||||
|
hash = _SystemHash.combine(hash, chatId.hashCode);
|
||||||
|
|
||||||
|
return _SystemHash.finish(hash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
|
// ignore: unused_element
|
||||||
|
mixin ChatBackendRef on AutoDisposeNotifierProviderRef<void> {
|
||||||
|
/// The parameter `chatId` of this provider.
|
||||||
|
String get chatId;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ChatBackendProviderElement
|
||||||
|
extends AutoDisposeNotifierProviderElement<ChatBackend, void>
|
||||||
|
with ChatBackendRef {
|
||||||
|
_ChatBackendProviderElement(super.provider);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get chatId => (origin as ChatBackendProvider).chatId;
|
||||||
|
}
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
||||||
+55
@@ -0,0 +1,55 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_chat_core/flutter_chat_core.dart';
|
||||||
|
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
class ChatTextMessage extends StatelessWidget {
|
||||||
|
final TextMessage message;
|
||||||
|
final int index;
|
||||||
|
final EdgeInsetsGeometry? padding;
|
||||||
|
final BorderRadiusGeometry? borderRadius;
|
||||||
|
final double? onlyEmojiFontSize;
|
||||||
|
|
||||||
|
const ChatTextMessage({
|
||||||
|
super.key,
|
||||||
|
required this.message,
|
||||||
|
required this.index,
|
||||||
|
this.padding = const EdgeInsets.symmetric(
|
||||||
|
horizontal: 16,
|
||||||
|
vertical: 10,
|
||||||
|
),
|
||||||
|
this.borderRadius = const BorderRadius.all(Radius.circular(12)),
|
||||||
|
this.onlyEmojiFontSize = 48,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final textMessageTheme =
|
||||||
|
context.select((ChatTheme theme) => theme.textMessageTheme);
|
||||||
|
final isSentByMe = context.watch<String>() == message.authorId;
|
||||||
|
final paragraphStyle = isSentByMe
|
||||||
|
? textMessageTheme.sentTextStyle
|
||||||
|
: textMessageTheme.receivedTextStyle;
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
padding: padding,
|
||||||
|
decoration: message.isOnlyEmoji == true
|
||||||
|
? null
|
||||||
|
: BoxDecoration(
|
||||||
|
color: isSentByMe
|
||||||
|
? textMessageTheme.sentBackgroundColor
|
||||||
|
: textMessageTheme.receivedBackgroundColor,
|
||||||
|
borderRadius: borderRadius,
|
||||||
|
),
|
||||||
|
child: MarkdownBody(
|
||||||
|
data: message.text,
|
||||||
|
selectable: true,
|
||||||
|
styleSheet: MarkdownStyleSheet(
|
||||||
|
p: message.isOnlyEmoji == true
|
||||||
|
? paragraphStyle?.copyWith(fontSize: onlyEmojiFontSize)
|
||||||
|
: paragraphStyle,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+183
@@ -0,0 +1,183 @@
|
|||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_chat_core/flutter_chat_core.dart';
|
||||||
|
import 'package:flutter_chat_ui/flutter_chat_ui.dart';
|
||||||
|
import 'package:lensai/presentation/widgets/speech_to_text_button.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
typedef OnMessageTapCallback = void Function(Message message);
|
||||||
|
typedef OnMessageSendCallback = void Function(String text);
|
||||||
|
typedef OnAttachmentTapCallback = VoidCallback;
|
||||||
|
|
||||||
|
class QaChatInput extends StatefulWidget {
|
||||||
|
final double? left;
|
||||||
|
final double? right;
|
||||||
|
final double? top;
|
||||||
|
final double? bottom;
|
||||||
|
final double? sigmaX;
|
||||||
|
final double? sigmaY;
|
||||||
|
final EdgeInsetsGeometry? padding;
|
||||||
|
final Widget? attachmentIcon;
|
||||||
|
final Widget? sendIcon;
|
||||||
|
final double? gap;
|
||||||
|
final InputBorder? inputBorder;
|
||||||
|
final bool? filled;
|
||||||
|
final Widget? topWidget;
|
||||||
|
final bool? handleSafeArea;
|
||||||
|
|
||||||
|
const QaChatInput({
|
||||||
|
super.key,
|
||||||
|
this.left = 0,
|
||||||
|
this.right = 0,
|
||||||
|
this.top,
|
||||||
|
this.bottom = 0,
|
||||||
|
this.sigmaX = 20,
|
||||||
|
this.sigmaY = 20,
|
||||||
|
this.padding = const EdgeInsets.all(8.0),
|
||||||
|
this.attachmentIcon = const Icon(Icons.attachment),
|
||||||
|
this.sendIcon = const Icon(Icons.send),
|
||||||
|
this.gap = 8,
|
||||||
|
this.inputBorder = const OutlineInputBorder(
|
||||||
|
borderSide: BorderSide.none,
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(24)),
|
||||||
|
),
|
||||||
|
this.filled = true,
|
||||||
|
this.topWidget,
|
||||||
|
this.handleSafeArea = true,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<QaChatInput> createState() => _QaChatInputState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _QaChatInputState extends State<QaChatInput> {
|
||||||
|
final GlobalKey _inputKey = GlobalKey();
|
||||||
|
final TextEditingController _textController = TextEditingController();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) => _updateInputHeight());
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(covariant QaChatInput oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) => _updateInputHeight());
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_textController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final bottomSafeArea = widget.handleSafeArea == true
|
||||||
|
? MediaQuery.of(context).padding.bottom
|
||||||
|
: 0.0;
|
||||||
|
final inputTheme = context.select((ChatTheme theme) => theme.inputTheme);
|
||||||
|
final onAttachmentTap = context.read<OnAttachmentTapCallback?>();
|
||||||
|
|
||||||
|
return Positioned(
|
||||||
|
left: widget.left,
|
||||||
|
right: widget.right,
|
||||||
|
top: widget.top,
|
||||||
|
bottom: widget.bottom,
|
||||||
|
child: ClipRect(
|
||||||
|
child: BackdropFilter(
|
||||||
|
filter: ImageFilter.blur(
|
||||||
|
// TODO: remove backdrop filter if both are 0
|
||||||
|
sigmaX: widget.sigmaX ?? 0,
|
||||||
|
sigmaY: widget.sigmaY ?? 0,
|
||||||
|
),
|
||||||
|
child: Container(
|
||||||
|
key: _inputKey,
|
||||||
|
color: inputTheme.backgroundColor,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
if (widget.topWidget != null) widget.topWidget!,
|
||||||
|
Padding(
|
||||||
|
padding: widget.handleSafeArea == true
|
||||||
|
? (widget.padding
|
||||||
|
?.add(EdgeInsets.only(bottom: bottomSafeArea)) ??
|
||||||
|
EdgeInsets.only(bottom: bottomSafeArea))
|
||||||
|
: (widget.padding ?? EdgeInsets.zero),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
if (widget.attachmentIcon != null)
|
||||||
|
IconButton(
|
||||||
|
icon: widget.attachmentIcon!,
|
||||||
|
color: inputTheme.hintStyle?.color,
|
||||||
|
onPressed: onAttachmentTap,
|
||||||
|
)
|
||||||
|
else
|
||||||
|
const SizedBox.shrink(),
|
||||||
|
SizedBox(width: widget.gap),
|
||||||
|
Expanded(
|
||||||
|
child: TextField(
|
||||||
|
controller: _textController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: 'Type a message',
|
||||||
|
hintStyle: inputTheme.hintStyle,
|
||||||
|
border: widget.inputBorder,
|
||||||
|
filled: widget.filled,
|
||||||
|
fillColor: inputTheme.textFieldColor,
|
||||||
|
hoverColor: Colors.transparent,
|
||||||
|
suffixIcon: SpeechToTextButton(
|
||||||
|
onTextReceived: (data) {
|
||||||
|
_textController.text = data.toString();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
style: inputTheme.textStyle,
|
||||||
|
onSubmitted: _handleSubmitted,
|
||||||
|
textInputAction: TextInputAction.send,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(width: widget.gap),
|
||||||
|
if (widget.sendIcon != null)
|
||||||
|
IconButton(
|
||||||
|
icon: widget.sendIcon!,
|
||||||
|
color: inputTheme.hintStyle?.color,
|
||||||
|
onPressed: () =>
|
||||||
|
_handleSubmitted(_textController.text),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
const SizedBox.shrink(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _updateInputHeight() {
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
|
final renderBox =
|
||||||
|
_inputKey.currentContext?.findRenderObject() as RenderBox?;
|
||||||
|
if (renderBox != null) {
|
||||||
|
final height = renderBox.size.height;
|
||||||
|
final bottomSafeArea = MediaQuery.of(context).padding.bottom;
|
||||||
|
|
||||||
|
context.read<ChatInputHeightNotifier>().updateHeight(
|
||||||
|
// only set real height of the input, ignoring safe area
|
||||||
|
widget.handleSafeArea == true ? height - bottomSafeArea : height,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleSubmitted(String text) {
|
||||||
|
if (text.isNotEmpty) {
|
||||||
|
context.read<OnMessageSendCallback?>()?.call(text);
|
||||||
|
_textController.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+75
@@ -0,0 +1,75 @@
|
|||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:cross_cache/cross_cache.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_chat_core/flutter_chat_core.dart';
|
||||||
|
import 'package:flutter_chat_ui/flutter_chat_ui.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:lensai/features/chat/features/chat_store/data/models/message_types.dart';
|
||||||
|
import 'package:lensai/features/chat/features/chat_store/domain/providers.dart';
|
||||||
|
import 'package:lensai/features/geckoview/features/tabs/features/chat/domain/chat_backend.dart';
|
||||||
|
import 'package:lensai/features/geckoview/features/tabs/features/chat/presentation/widgets/chat_text_message.dart';
|
||||||
|
import 'package:lensai/features/geckoview/features/tabs/features/chat/presentation/widgets/qa_chat_input.dart';
|
||||||
|
import 'package:lensai/presentation/hooks/on_initialization.dart';
|
||||||
|
|
||||||
|
class TabQaChat extends HookConsumerWidget {
|
||||||
|
final String chatId;
|
||||||
|
final ScrollController? scrollController;
|
||||||
|
|
||||||
|
const TabQaChat({required this.chatId, this.scrollController, super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final chatBackend = ref.watch(chatBackendProvider(chatId).notifier);
|
||||||
|
final chatController = ref.watch(chatControllerProvider(chatId));
|
||||||
|
|
||||||
|
final crossCache = useMemoized(() => CrossCache());
|
||||||
|
final chatScrollController = scrollController ?? useScrollController();
|
||||||
|
|
||||||
|
useOnInitialization(() async {
|
||||||
|
await chatBackend.prepareEmbeddings();
|
||||||
|
});
|
||||||
|
|
||||||
|
return Chat(
|
||||||
|
darkTheme: ChatTheme.dark(
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||||
|
inputTheme: InputTheme(
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
theme: ChatTheme.light(
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||||
|
),
|
||||||
|
builders: Builders(
|
||||||
|
textMessageBuilder: (context, message, index) =>
|
||||||
|
ChatTextMessage(message: message, index: index),
|
||||||
|
customMessageBuilder: (context, message, index) => Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 16,
|
||||||
|
vertical: 10,
|
||||||
|
),
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: Color(0xFFF0F0F0),
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||||
|
),
|
||||||
|
child: const IsTypingIndicator(),
|
||||||
|
),
|
||||||
|
inputBuilder: (context) => const QaChatInput(
|
||||||
|
attachmentIcon: null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
chatController: chatController,
|
||||||
|
crossCache: crossCache,
|
||||||
|
scrollController: chatScrollController,
|
||||||
|
onMessageSend: (text) async {
|
||||||
|
await chatBackend.processQAMessage(text);
|
||||||
|
},
|
||||||
|
currentUserId: MessageAuthor.human.user.id,
|
||||||
|
resolveUser: (id) => Future.value(
|
||||||
|
MessageAuthor.values
|
||||||
|
.firstWhereOrNull((user) => user.user.id == id)
|
||||||
|
?.user,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import 'package:langchain/langchain.dart';
|
||||||
|
import 'package:langchain_openai/langchain_openai.dart';
|
||||||
|
import 'package:lensai/core/models.dart';
|
||||||
|
import 'package:lensai/features/geckoview/features/tabs/data/providers.dart';
|
||||||
|
import 'package:lensai/features/geckoview/features/tabs/features/vector_store/domain/sqlite_vector_store.dart';
|
||||||
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
|
||||||
|
part 'qa_memory_chain.g.dart';
|
||||||
|
|
||||||
|
typedef QAResult = ({String answer, List<Document> docs});
|
||||||
|
|
||||||
|
@Riverpod()
|
||||||
|
class QAMemoryChain extends _$QAMemoryChain {
|
||||||
|
late RetrievalQAChain _retrievalQA;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void build({
|
||||||
|
required String chatId,
|
||||||
|
String? mainDocumentId,
|
||||||
|
String? contextId,
|
||||||
|
}) {
|
||||||
|
final chatModel = ref.watch(chatModelProvider);
|
||||||
|
final embeddingsModel = ref.watch(embeddingModelProvider);
|
||||||
|
final db = ref.watch(tabDatabaseProvider);
|
||||||
|
|
||||||
|
final retriever = SqliteVectorStore(
|
||||||
|
db.vectorDao,
|
||||||
|
embeddings: embeddingsModel,
|
||||||
|
mainDocumentId: mainDocumentId,
|
||||||
|
contextId: contextId,
|
||||||
|
).asRetriever(
|
||||||
|
defaultOptions: const VectorStoreRetrieverOptions(
|
||||||
|
searchType: VectorStoreSimilaritySearch(k: 3),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final qaChain = OpenAIQAWithSourcesChain(llm: chatModel);
|
||||||
|
final docPrompt = PromptTemplate.fromTemplate(
|
||||||
|
'Content: {page_content}\nSource: {source}',
|
||||||
|
);
|
||||||
|
|
||||||
|
final finalQAChain = StuffDocumentsChain(
|
||||||
|
llmChain: qaChain,
|
||||||
|
documentPrompt: docPrompt,
|
||||||
|
);
|
||||||
|
|
||||||
|
_retrievalQA = RetrievalQAChain(
|
||||||
|
retriever: retriever,
|
||||||
|
combineDocumentsChain: finalQAChain,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<QAResult> processQuestion(String input) async {
|
||||||
|
final result = await _retrievalQA(input);
|
||||||
|
final qaResult = result['result'] as QAWithSources;
|
||||||
|
|
||||||
|
return (answer: qaResult.answer, docs: <Document>[]);
|
||||||
|
}
|
||||||
|
}
|
||||||
+218
@@ -0,0 +1,218 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'qa_memory_chain.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// RiverpodGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
String _$qAMemoryChainHash() => r'd5efc3fc01a6a88c67164d7eefc06412c9ab7c5c';
|
||||||
|
|
||||||
|
/// Copied from Dart SDK
|
||||||
|
class _SystemHash {
|
||||||
|
_SystemHash._();
|
||||||
|
|
||||||
|
static int combine(int hash, int value) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
hash = 0x1fffffff & (hash + value);
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
|
||||||
|
return hash ^ (hash >> 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int finish(int hash) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
hash = hash ^ (hash >> 11);
|
||||||
|
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class _$QAMemoryChain extends BuildlessAutoDisposeNotifier<void> {
|
||||||
|
late final String chatId;
|
||||||
|
late final String? mainDocumentId;
|
||||||
|
late final String? contextId;
|
||||||
|
|
||||||
|
void build({
|
||||||
|
required String chatId,
|
||||||
|
String? mainDocumentId,
|
||||||
|
String? contextId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// See also [QAMemoryChain].
|
||||||
|
@ProviderFor(QAMemoryChain)
|
||||||
|
const qAMemoryChainProvider = QAMemoryChainFamily();
|
||||||
|
|
||||||
|
/// See also [QAMemoryChain].
|
||||||
|
class QAMemoryChainFamily extends Family<void> {
|
||||||
|
/// See also [QAMemoryChain].
|
||||||
|
const QAMemoryChainFamily();
|
||||||
|
|
||||||
|
/// See also [QAMemoryChain].
|
||||||
|
QAMemoryChainProvider call({
|
||||||
|
required String chatId,
|
||||||
|
String? mainDocumentId,
|
||||||
|
String? contextId,
|
||||||
|
}) {
|
||||||
|
return QAMemoryChainProvider(
|
||||||
|
chatId: chatId,
|
||||||
|
mainDocumentId: mainDocumentId,
|
||||||
|
contextId: contextId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
QAMemoryChainProvider getProviderOverride(
|
||||||
|
covariant QAMemoryChainProvider provider,
|
||||||
|
) {
|
||||||
|
return call(
|
||||||
|
chatId: provider.chatId,
|
||||||
|
mainDocumentId: provider.mainDocumentId,
|
||||||
|
contextId: provider.contextId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static const Iterable<ProviderOrFamily>? _dependencies = null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
|
||||||
|
|
||||||
|
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
|
||||||
|
_allTransitiveDependencies;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? get name => r'qAMemoryChainProvider';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// See also [QAMemoryChain].
|
||||||
|
class QAMemoryChainProvider
|
||||||
|
extends AutoDisposeNotifierProviderImpl<QAMemoryChain, void> {
|
||||||
|
/// See also [QAMemoryChain].
|
||||||
|
QAMemoryChainProvider({
|
||||||
|
required String chatId,
|
||||||
|
String? mainDocumentId,
|
||||||
|
String? contextId,
|
||||||
|
}) : this._internal(
|
||||||
|
() => QAMemoryChain()
|
||||||
|
..chatId = chatId
|
||||||
|
..mainDocumentId = mainDocumentId
|
||||||
|
..contextId = contextId,
|
||||||
|
from: qAMemoryChainProvider,
|
||||||
|
name: r'qAMemoryChainProvider',
|
||||||
|
debugGetCreateSourceHash:
|
||||||
|
const bool.fromEnvironment('dart.vm.product')
|
||||||
|
? null
|
||||||
|
: _$qAMemoryChainHash,
|
||||||
|
dependencies: QAMemoryChainFamily._dependencies,
|
||||||
|
allTransitiveDependencies:
|
||||||
|
QAMemoryChainFamily._allTransitiveDependencies,
|
||||||
|
chatId: chatId,
|
||||||
|
mainDocumentId: mainDocumentId,
|
||||||
|
contextId: contextId,
|
||||||
|
);
|
||||||
|
|
||||||
|
QAMemoryChainProvider._internal(
|
||||||
|
super._createNotifier, {
|
||||||
|
required super.name,
|
||||||
|
required super.dependencies,
|
||||||
|
required super.allTransitiveDependencies,
|
||||||
|
required super.debugGetCreateSourceHash,
|
||||||
|
required super.from,
|
||||||
|
required this.chatId,
|
||||||
|
required this.mainDocumentId,
|
||||||
|
required this.contextId,
|
||||||
|
}) : super.internal();
|
||||||
|
|
||||||
|
final String chatId;
|
||||||
|
final String? mainDocumentId;
|
||||||
|
final String? contextId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void runNotifierBuild(
|
||||||
|
covariant QAMemoryChain notifier,
|
||||||
|
) {
|
||||||
|
return notifier.build(
|
||||||
|
chatId: chatId,
|
||||||
|
mainDocumentId: mainDocumentId,
|
||||||
|
contextId: contextId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Override overrideWith(QAMemoryChain Function() create) {
|
||||||
|
return ProviderOverride(
|
||||||
|
origin: this,
|
||||||
|
override: QAMemoryChainProvider._internal(
|
||||||
|
() => create()
|
||||||
|
..chatId = chatId
|
||||||
|
..mainDocumentId = mainDocumentId
|
||||||
|
..contextId = contextId,
|
||||||
|
from: from,
|
||||||
|
name: null,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
debugGetCreateSourceHash: null,
|
||||||
|
chatId: chatId,
|
||||||
|
mainDocumentId: mainDocumentId,
|
||||||
|
contextId: contextId,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
AutoDisposeNotifierProviderElement<QAMemoryChain, void> createElement() {
|
||||||
|
return _QAMemoryChainProviderElement(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return other is QAMemoryChainProvider &&
|
||||||
|
other.chatId == chatId &&
|
||||||
|
other.mainDocumentId == mainDocumentId &&
|
||||||
|
other.contextId == contextId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode {
|
||||||
|
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||||
|
hash = _SystemHash.combine(hash, chatId.hashCode);
|
||||||
|
hash = _SystemHash.combine(hash, mainDocumentId.hashCode);
|
||||||
|
hash = _SystemHash.combine(hash, contextId.hashCode);
|
||||||
|
|
||||||
|
return _SystemHash.finish(hash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
|
// ignore: unused_element
|
||||||
|
mixin QAMemoryChainRef on AutoDisposeNotifierProviderRef<void> {
|
||||||
|
/// The parameter `chatId` of this provider.
|
||||||
|
String get chatId;
|
||||||
|
|
||||||
|
/// The parameter `mainDocumentId` of this provider.
|
||||||
|
String? get mainDocumentId;
|
||||||
|
|
||||||
|
/// The parameter `contextId` of this provider.
|
||||||
|
String? get contextId;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _QAMemoryChainProviderElement
|
||||||
|
extends AutoDisposeNotifierProviderElement<QAMemoryChain, void>
|
||||||
|
with QAMemoryChainRef {
|
||||||
|
_QAMemoryChainProviderElement(super.provider);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get chatId => (origin as QAMemoryChainProvider).chatId;
|
||||||
|
@override
|
||||||
|
String? get mainDocumentId =>
|
||||||
|
(origin as QAMemoryChainProvider).mainDocumentId;
|
||||||
|
@override
|
||||||
|
String? get contextId => (origin as QAMemoryChainProvider).contextId;
|
||||||
|
}
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
||||||
+172
@@ -0,0 +1,172 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:langchain/langchain.dart' as langchain;
|
||||||
|
import 'package:lensai/core/uuid.dart';
|
||||||
|
import 'package:lensai/features/geckoview/features/tabs/data/database/database.dart';
|
||||||
|
import 'package:lensai/features/geckoview/features/tabs/features/vector_store/data/models/vector_result.dart';
|
||||||
|
import 'package:lensai/features/geckoview/features/tabs/features/vector_store/utils/hash.dart';
|
||||||
|
import 'package:lensai/utils/langchain_utils.dart';
|
||||||
|
|
||||||
|
part 'vector.g.dart';
|
||||||
|
|
||||||
|
@DriftAccessor()
|
||||||
|
class VectorDao extends DatabaseAccessor<TabDatabase> with _$VectorDaoMixin {
|
||||||
|
VectorDao(super.attachedDatabase);
|
||||||
|
|
||||||
|
Future<int> deleteDocuments(List<String> ids) {
|
||||||
|
return db.document.deleteWhere((doc) => doc.documentId.isIn(ids));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<int> deleteDocumentsByMainDocumentId(String mainDocumentId) {
|
||||||
|
return db.document
|
||||||
|
.deleteWhere((doc) => doc.mainDocumentId.equals(mainDocumentId));
|
||||||
|
}
|
||||||
|
|
||||||
|
SingleOrNullSelectable<DocumentData> getDocumentById(String documentId) {
|
||||||
|
return db.document.select()
|
||||||
|
..where((row) => row.documentId.equals(documentId));
|
||||||
|
}
|
||||||
|
|
||||||
|
Selectable<DocumentData> getDocuments({
|
||||||
|
String? mainDocumentId,
|
||||||
|
String? contextId,
|
||||||
|
}) {
|
||||||
|
final statement = db.document.select();
|
||||||
|
|
||||||
|
if (mainDocumentId != null) {
|
||||||
|
statement.where((row) => row.mainDocumentId.equals(mainDocumentId));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (contextId != null) {
|
||||||
|
statement.where((row) => row.contextId.equals(contextId));
|
||||||
|
}
|
||||||
|
|
||||||
|
return statement;
|
||||||
|
}
|
||||||
|
|
||||||
|
Selectable<DocumentData> getDocumentsWithMissingEmbeddings({
|
||||||
|
String? mainDocumentId,
|
||||||
|
String? contextId,
|
||||||
|
}) {
|
||||||
|
return db.missingDocumentEmbeddings(
|
||||||
|
mainDocumentId: mainDocumentId,
|
||||||
|
contextId: contextId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> insertDocuments(
|
||||||
|
List<langchain.Document> documents, {
|
||||||
|
String? mainDocumentId,
|
||||||
|
String? contextId,
|
||||||
|
}) {
|
||||||
|
return db.document.insertAll(
|
||||||
|
documents.map((document) {
|
||||||
|
final documentId = document.id ?? uuid.v4();
|
||||||
|
final contentHash = sha2(document.pageContent);
|
||||||
|
|
||||||
|
return DocumentCompanion.insert(
|
||||||
|
documentId: documentId,
|
||||||
|
mainDocumentId: Value.absentIfNull(mainDocumentId),
|
||||||
|
contextId: Value.absentIfNull(contextId),
|
||||||
|
content: document.pageContent,
|
||||||
|
metadata: (document.metadata.isNotEmpty)
|
||||||
|
? Value(jsonEncode(document.metadata))
|
||||||
|
: const Value.absent(),
|
||||||
|
contentHash: contentHash,
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
mode: InsertMode.insertOrReplace,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> insertEmbeddings(List<(DocumentData, List<double>)> documents) {
|
||||||
|
return db.documentVec.insertAll(
|
||||||
|
documents.map(
|
||||||
|
(doc) {
|
||||||
|
final (document, vector) = doc;
|
||||||
|
|
||||||
|
return DocumentVecCompanion.insert(
|
||||||
|
id: document.documentId,
|
||||||
|
embedding: serializeVector(vector),
|
||||||
|
mainDocumentId: Value.absentIfNull(document.mainDocumentId),
|
||||||
|
contextId: Value.absentIfNull(document.contextId),
|
||||||
|
contentHash: document.contentHash,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<String>> insertDocumentsWithEmbedings(
|
||||||
|
List<(langchain.Document, List<double>)> documents, {
|
||||||
|
String? mainDocumentId,
|
||||||
|
String? contextId,
|
||||||
|
}) async {
|
||||||
|
final insertedIds = <String>[];
|
||||||
|
|
||||||
|
await db.batch((b) async {
|
||||||
|
final insertableDocuments = <Insertable<DocumentData>>[];
|
||||||
|
final insertableEmbeddings = <Insertable<DocumentVecData>>[];
|
||||||
|
|
||||||
|
for (final (document, vector) in documents) {
|
||||||
|
final documentId = document.id ?? uuid.v4();
|
||||||
|
final contentHash = sha2(document.pageContent);
|
||||||
|
|
||||||
|
insertedIds.add(documentId);
|
||||||
|
|
||||||
|
insertableDocuments.add(
|
||||||
|
DocumentCompanion.insert(
|
||||||
|
documentId: documentId,
|
||||||
|
mainDocumentId: Value.absentIfNull(mainDocumentId),
|
||||||
|
contextId: Value.absentIfNull(contextId),
|
||||||
|
content: document.pageContent,
|
||||||
|
metadata: (document.metadata.isNotEmpty)
|
||||||
|
? Value(jsonEncode(document.metadata))
|
||||||
|
: const Value.absent(),
|
||||||
|
contentHash: contentHash,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
insertableEmbeddings.add(
|
||||||
|
DocumentVecCompanion.insert(
|
||||||
|
id: documentId,
|
||||||
|
embedding: serializeVector(vector),
|
||||||
|
mainDocumentId: Value.absentIfNull(mainDocumentId),
|
||||||
|
contextId: Value.absentIfNull(contextId),
|
||||||
|
contentHash: contentHash,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.document.insertAll(
|
||||||
|
insertableDocuments,
|
||||||
|
mode: InsertMode.insertOrReplace,
|
||||||
|
);
|
||||||
|
|
||||||
|
await db.documentVec.insertAll(insertableEmbeddings);
|
||||||
|
});
|
||||||
|
|
||||||
|
return insertedIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
Selectable<VectorResult> vectorSearch({
|
||||||
|
required langchain.VectorStoreSimilaritySearch config,
|
||||||
|
required List<double> searchVectors,
|
||||||
|
String? mainDocumentId,
|
||||||
|
String? contextId,
|
||||||
|
}) {
|
||||||
|
assert(config.filter?.isNotEmpty ?? true, 'Filters are unsupported');
|
||||||
|
assert(
|
||||||
|
!(mainDocumentId != null && contextId != null),
|
||||||
|
'Either filter by document or context, not both',
|
||||||
|
);
|
||||||
|
|
||||||
|
return db.queryVectors(
|
||||||
|
searchVectors: serializeVector(searchVectors),
|
||||||
|
k: config.k,
|
||||||
|
mainDocumentId: mainDocumentId,
|
||||||
|
contextId: contextId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+6
@@ -0,0 +1,6 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'vector.dart';
|
||||||
|
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
mixin _$VectorDaoMixin on DatabaseAccessor<TabDatabase> {}
|
||||||
+15
@@ -0,0 +1,15 @@
|
|||||||
|
class VectorDatabaseMigrator {
|
||||||
|
final int dimensions;
|
||||||
|
|
||||||
|
String get vectorTableDefinition => '''
|
||||||
|
CREATE VIRTUAL TABLE document_vec using vec0(
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
main_document_id TEXT,
|
||||||
|
context_id TEXT PARTITION KEY,
|
||||||
|
embedding float[$dimensions],
|
||||||
|
content_hash TEXT
|
||||||
|
);
|
||||||
|
''';
|
||||||
|
|
||||||
|
VectorDatabaseMigrator({required this.dimensions});
|
||||||
|
}
|
||||||
+72
@@ -0,0 +1,72 @@
|
|||||||
|
import 'package:lensai/features/geckoview/features/tabs/features/vector_store/data/models/vector_result.dart';
|
||||||
|
|
||||||
|
CREATE TABLE document(
|
||||||
|
document_id TEXT NOT NULL PRIMARY KEY,
|
||||||
|
main_document_id TEXT,
|
||||||
|
context_id TEXT,
|
||||||
|
content TEXT NOT NULL,
|
||||||
|
content_hash TEXT NOT NULL,
|
||||||
|
metadata TEXT
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX document_main_document_id ON document (main_document_id);
|
||||||
|
CREATE INDEX document_context_id ON document (context_id);
|
||||||
|
|
||||||
|
CREATE TABLE document_vec(
|
||||||
|
-- dummy definition of virtual table
|
||||||
|
-- keep in sync with defined table definition
|
||||||
|
|
||||||
|
id TEXT NOT NULL PRIMARY KEY,
|
||||||
|
main_document_id TEXT,
|
||||||
|
context_id TEXT,
|
||||||
|
embedding BLOB NOT NULL,
|
||||||
|
content_hash TEXT NOT NULL,
|
||||||
|
|
||||||
|
distance REAL,
|
||||||
|
k INTEGER
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TRIGGER document_delete AFTER DELETE ON document BEGIN
|
||||||
|
DELETE FROM document_vec WHERE id = old.document_id;
|
||||||
|
END;
|
||||||
|
|
||||||
|
CREATE TRIGGER document_update_delete AFTER UPDATE ON document BEGIN
|
||||||
|
DELETE FROM document_vec WHERE id = new.document_id AND content_hash != new.content_hash;
|
||||||
|
END;
|
||||||
|
|
||||||
|
missingDocumentEmbeddings(
|
||||||
|
:main_document_id AS TEXT OR NULL,
|
||||||
|
:context_id AS TEXT OR NULL
|
||||||
|
):
|
||||||
|
SELECT
|
||||||
|
doc.*
|
||||||
|
FROM document doc
|
||||||
|
WHERE
|
||||||
|
doc.main_document_id IS COALESCE(:main_document_id, doc.main_document_id) AND
|
||||||
|
doc.context_id IS COALESCE(:context_id, doc.context_id) AND
|
||||||
|
NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM document_vec vec
|
||||||
|
WHERE vec.id = doc.document_id
|
||||||
|
);
|
||||||
|
|
||||||
|
queryVectors(
|
||||||
|
:searchVectors AS BLOB,
|
||||||
|
:main_document_id AS TEXT OR NULL,
|
||||||
|
:context_id AS TEXT OR NULL
|
||||||
|
) WITH VectorResult:
|
||||||
|
SELECT
|
||||||
|
vec.id,
|
||||||
|
doc.main_document_id,
|
||||||
|
doc.context_id,
|
||||||
|
doc.content,
|
||||||
|
doc.metadata,
|
||||||
|
vec.distance
|
||||||
|
FROM document_vec vec
|
||||||
|
INNER JOIN document doc ON doc.document_id = vec.id
|
||||||
|
WHERE
|
||||||
|
vec.embedding MATCH :searchVectors AND
|
||||||
|
vec.k = :k AND
|
||||||
|
vec.main_document_id IS COALESCE(:main_document_id, vec.main_document_id) AND
|
||||||
|
vec.context_id IS COALESCE(:context_id, vec.context_id)
|
||||||
|
ORDER BY vec.distance;
|
||||||
+32
@@ -0,0 +1,32 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:langchain/langchain.dart';
|
||||||
|
|
||||||
|
class VectorResult {
|
||||||
|
final String id;
|
||||||
|
final String? mainDocumentId;
|
||||||
|
final String? contextId;
|
||||||
|
final String content;
|
||||||
|
final Map<String, dynamic> metadata;
|
||||||
|
final double distance;
|
||||||
|
|
||||||
|
VectorResult({
|
||||||
|
required this.id,
|
||||||
|
required this.mainDocumentId,
|
||||||
|
required this.contextId,
|
||||||
|
required this.content,
|
||||||
|
required String? metadata,
|
||||||
|
required double? distance,
|
||||||
|
}) : metadata = (metadata != null)
|
||||||
|
? jsonDecode(metadata) as Map<String, dynamic>
|
||||||
|
: const {},
|
||||||
|
distance = distance!;
|
||||||
|
|
||||||
|
Document toDocument() {
|
||||||
|
return Document(
|
||||||
|
id: id,
|
||||||
|
pageContent: content,
|
||||||
|
metadata: metadata,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+116
@@ -0,0 +1,116 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:exceptions/exceptions.dart';
|
||||||
|
import 'package:langchain/langchain.dart' as langchain;
|
||||||
|
import 'package:langchain/langchain.dart';
|
||||||
|
import 'package:langchain_openai/langchain_openai.dart';
|
||||||
|
import 'package:lensai/core/models.dart';
|
||||||
|
import 'package:lensai/features/geckoview/domain/providers.dart';
|
||||||
|
import 'package:lensai/features/geckoview/features/tabs/data/providers.dart';
|
||||||
|
import 'package:lensai/features/geckoview/features/tabs/features/vector_store/data/database/daos/vector.dart';
|
||||||
|
import 'package:lensai/features/geckoview/features/tabs/features/vector_store/utils/markdown_document_splitter.dart';
|
||||||
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
|
||||||
|
part 'document.g.dart';
|
||||||
|
|
||||||
|
@Riverpod()
|
||||||
|
class DocumentRepository extends _$DocumentRepository {
|
||||||
|
late VectorDao _vectorDao;
|
||||||
|
late OpenAIEmbeddings _embeddings;
|
||||||
|
|
||||||
|
Future<void> _insertMarkdownDocumentsSplitted(
|
||||||
|
List<langchain.Document> originalDocuments, {
|
||||||
|
List<(String, String)> headersToSplitOn = const [
|
||||||
|
('#', 'h1'),
|
||||||
|
('##', 'h2'),
|
||||||
|
('###', 'h3'),
|
||||||
|
('####', 'h4'),
|
||||||
|
('#####', 'h5'),
|
||||||
|
('######', 'h6'),
|
||||||
|
],
|
||||||
|
int chunkSize = 748,
|
||||||
|
int chunkOverlap = 150,
|
||||||
|
}) {
|
||||||
|
return _vectorDao.transaction(() async {
|
||||||
|
for (final doc in originalDocuments) {
|
||||||
|
final splitted = splitMarkdownDocument(
|
||||||
|
doc,
|
||||||
|
headersToSplitOn: headersToSplitOn,
|
||||||
|
chunkSize: chunkSize,
|
||||||
|
chunkOverlap: chunkOverlap,
|
||||||
|
);
|
||||||
|
|
||||||
|
final splittedWithSource = (splitted != null)
|
||||||
|
? (
|
||||||
|
mainDocumentId: splitted.mainDocumentId,
|
||||||
|
parts: splitted.parts
|
||||||
|
.map(
|
||||||
|
(part) => part.copyWith(
|
||||||
|
metadata: {
|
||||||
|
...part.metadata,
|
||||||
|
'source': part.id,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList()
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
await _vectorDao.insertDocuments(
|
||||||
|
splittedWithSource?.parts ?? [doc],
|
||||||
|
mainDocumentId: splittedWithSource?.mainDocumentId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Result> updateEmbeddings({
|
||||||
|
String? mainDocumentId,
|
||||||
|
String? contextId,
|
||||||
|
}) {
|
||||||
|
return Result.fromAsync(() async {
|
||||||
|
final missing = await _vectorDao
|
||||||
|
.getDocumentsWithMissingEmbeddings(
|
||||||
|
mainDocumentId: mainDocumentId,
|
||||||
|
contextId: contextId,
|
||||||
|
)
|
||||||
|
.get();
|
||||||
|
|
||||||
|
final documentEmbeddings = await _embeddings.embedDocuments(
|
||||||
|
missing
|
||||||
|
.map((doc) => langchain.Document(pageContent: doc.content))
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
|
||||||
|
return _vectorDao.insertEmbeddings(
|
||||||
|
missing.mapIndexed((i, doc) => (doc, documentEmbeddings[i])).toList(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void build() {
|
||||||
|
_vectorDao = ref.watch(tabDatabaseProvider).vectorDao;
|
||||||
|
_embeddings = ref.watch(embeddingModelProvider);
|
||||||
|
|
||||||
|
final tabContentService = ref.watch(tabContentServiceProvider);
|
||||||
|
|
||||||
|
final tabContentSub =
|
||||||
|
tabContentService.tabContentStream.listen((content) async {
|
||||||
|
final bestContent = content.isProbablyReaderable
|
||||||
|
? content.extractedContentMarkdown
|
||||||
|
: content.fullContentMarkdown;
|
||||||
|
|
||||||
|
if (bestContent != null) {
|
||||||
|
await _insertMarkdownDocumentsSplitted(
|
||||||
|
[Document(id: content.tabId, pageContent: bestContent)],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ref.onDispose(() {
|
||||||
|
unawaited(tabContentSub.cancel());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
+11
-13
@@ -1,29 +1,27 @@
|
|||||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
part of 'cache.dart';
|
part of 'document.dart';
|
||||||
|
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
// RiverpodGenerator
|
// RiverpodGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$iconCacheSizeMegabytesHash() =>
|
String _$documentRepositoryHash() =>
|
||||||
r'43162fce3ce8b04a5a55ed52021b274afbde2861';
|
r'0c93116e0f9887d452b7c493dd4da45e5bf0c2d4';
|
||||||
|
|
||||||
/// See also [iconCacheSizeMegabytes].
|
/// See also [DocumentRepository].
|
||||||
@ProviderFor(iconCacheSizeMegabytes)
|
@ProviderFor(DocumentRepository)
|
||||||
final iconCacheSizeMegabytesProvider =
|
final documentRepositoryProvider =
|
||||||
AutoDisposeStreamProvider<double>.internal(
|
AutoDisposeNotifierProvider<DocumentRepository, void>.internal(
|
||||||
iconCacheSizeMegabytes,
|
DocumentRepository.new,
|
||||||
name: r'iconCacheSizeMegabytesProvider',
|
name: r'documentRepositoryProvider',
|
||||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||||
? null
|
? null
|
||||||
: _$iconCacheSizeMegabytesHash,
|
: _$documentRepositoryHash,
|
||||||
dependencies: null,
|
dependencies: null,
|
||||||
allTransitiveDependencies: null,
|
allTransitiveDependencies: null,
|
||||||
);
|
);
|
||||||
|
|
||||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
typedef _$DocumentRepository = AutoDisposeNotifier<void>;
|
||||||
// ignore: unused_element
|
|
||||||
typedef IconCacheSizeMegabytesRef = AutoDisposeStreamProviderRef<double>;
|
|
||||||
// ignore_for_file: type=lint
|
// ignore_for_file: type=lint
|
||||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
||||||
+60
@@ -0,0 +1,60 @@
|
|||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:langchain/langchain.dart';
|
||||||
|
import 'package:lensai/features/geckoview/features/tabs/features/vector_store/data/database/daos/vector.dart';
|
||||||
|
import 'package:lensai/features/geckoview/features/tabs/features/vector_store/data/models/vector_result.dart';
|
||||||
|
|
||||||
|
class SqliteVectorStore extends VectorStore {
|
||||||
|
final VectorDao _dao;
|
||||||
|
|
||||||
|
final String? mainDocumentId;
|
||||||
|
final String? contextId;
|
||||||
|
|
||||||
|
SqliteVectorStore(
|
||||||
|
this._dao, {
|
||||||
|
required super.embeddings,
|
||||||
|
this.mainDocumentId,
|
||||||
|
this.contextId,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<String>> addVectors({
|
||||||
|
required List<Document> documents,
|
||||||
|
required List<List<double>> vectors,
|
||||||
|
}) {
|
||||||
|
return _dao.insertDocumentsWithEmbedings(
|
||||||
|
documents.mapIndexed((i, doc) => (doc, vectors[i])).toList(),
|
||||||
|
contextId: contextId,
|
||||||
|
mainDocumentId: mainDocumentId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> delete({required List<String> ids}) {
|
||||||
|
return _dao.deleteDocuments(ids);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<(Document, double)>> similaritySearchByVectorWithScores({
|
||||||
|
required List<double> embedding,
|
||||||
|
VectorStoreSimilaritySearch config = const VectorStoreSimilaritySearch(),
|
||||||
|
}) async {
|
||||||
|
Iterable<VectorResult> results = await _dao
|
||||||
|
.vectorSearch(
|
||||||
|
config: config,
|
||||||
|
searchVectors: embedding,
|
||||||
|
contextId: contextId,
|
||||||
|
mainDocumentId: mainDocumentId,
|
||||||
|
)
|
||||||
|
.get();
|
||||||
|
|
||||||
|
if (config.scoreThreshold != null) {
|
||||||
|
results = results.where(
|
||||||
|
(result) => result.distance >= config.scoreThreshold!,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return results
|
||||||
|
.map((result) => (result.toDocument(), result.distance))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
}
|
||||||
+26
@@ -0,0 +1,26 @@
|
|||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:lensai/core/uuid.dart';
|
||||||
|
import 'package:uuid/data.dart';
|
||||||
|
import 'package:uuid/parsing.dart';
|
||||||
|
import 'package:uuid/rng.dart';
|
||||||
|
|
||||||
|
final _rng = CryptoRNG();
|
||||||
|
|
||||||
|
class DocumentUuid {
|
||||||
|
final Uint8List _baseBytes;
|
||||||
|
|
||||||
|
DocumentUuid([Uint8List? baseBytes])
|
||||||
|
: _baseBytes = baseBytes ?? _rng.generate();
|
||||||
|
|
||||||
|
factory DocumentUuid.fromUuid(String uuid) {
|
||||||
|
return DocumentUuid(UuidParsing.parseAsByteList(uuid));
|
||||||
|
}
|
||||||
|
|
||||||
|
String getDocumentPartUuid(int sequence) {
|
||||||
|
final bytes = Uint8List.fromList(_baseBytes);
|
||||||
|
bytes.buffer.asByteData().setInt16(14, sequence);
|
||||||
|
|
||||||
|
return uuid.v8g(config: V8GenericOptions(bytes));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:crypto/crypto.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
|
String sha2(String input) {
|
||||||
|
return sha256.convert(utf8.encode(input)).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> sha2Isolated(String input) async {
|
||||||
|
if (input.length < 1048576) {
|
||||||
|
// Less than 1MB
|
||||||
|
return sha2(input);
|
||||||
|
} else {
|
||||||
|
return await compute(sha2, input);
|
||||||
|
}
|
||||||
|
}
|
||||||
+112
@@ -0,0 +1,112 @@
|
|||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:langchain/langchain.dart';
|
||||||
|
import 'package:lensai/core/uuid.dart';
|
||||||
|
import 'package:lensai/features/geckoview/features/tabs/features/vector_store/utils/document_uuid.dart';
|
||||||
|
|
||||||
|
typedef DocumentParts = ({String mainDocumentId, List<Document> parts});
|
||||||
|
|
||||||
|
List<Document> mergeShortDocuments(
|
||||||
|
List<Document> documents,
|
||||||
|
int maxLength, {
|
||||||
|
String separator = '\n',
|
||||||
|
required Document Function(Document a, Document b) doMerge,
|
||||||
|
int Function(String) lengthFunction = TextSplitter.defaultLengthFunction,
|
||||||
|
}) {
|
||||||
|
final result = <Document>[];
|
||||||
|
|
||||||
|
Document? current;
|
||||||
|
for (final doc in documents) {
|
||||||
|
if (current == null) {
|
||||||
|
current = doc;
|
||||||
|
} else if (lengthFunction(current.pageContent) +
|
||||||
|
lengthFunction(doc.pageContent) <=
|
||||||
|
maxLength) {
|
||||||
|
current = doMerge(current, doc);
|
||||||
|
} else {
|
||||||
|
result.add(current);
|
||||||
|
current = doc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current != null) {
|
||||||
|
result.add(current);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> _headerValueList(dynamic value) {
|
||||||
|
return switch (value) {
|
||||||
|
String _ => [value],
|
||||||
|
List<String> _ => value,
|
||||||
|
_ => throw Exception('Unsupported type')
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
DocumentParts? splitMarkdownDocument(
|
||||||
|
Document originalDoc, {
|
||||||
|
required List<(String, String)> headersToSplitOn,
|
||||||
|
required int chunkSize,
|
||||||
|
required int chunkOverlap,
|
||||||
|
}) {
|
||||||
|
final markdownHeaderSplitter = MarkdownHeaderTextSplitter(
|
||||||
|
stripHeaders: false,
|
||||||
|
headersToSplitOn: headersToSplitOn,
|
||||||
|
);
|
||||||
|
|
||||||
|
final markdownTextSplitter = MarkdownTextSplitter(
|
||||||
|
chunkSize: chunkSize,
|
||||||
|
chunkOverlap: chunkOverlap,
|
||||||
|
);
|
||||||
|
|
||||||
|
final headerChunks =
|
||||||
|
markdownHeaderSplitter.splitText(originalDoc.pageContent);
|
||||||
|
final docChunks = markdownTextSplitter.splitDocuments(headerChunks);
|
||||||
|
|
||||||
|
if (docChunks.length == 1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final headerKeys = headersToSplitOn.map((header) => header.$2).toSet();
|
||||||
|
final mergedDocChunks = mergeShortDocuments(
|
||||||
|
docChunks,
|
||||||
|
chunkSize,
|
||||||
|
doMerge: (a, b) {
|
||||||
|
//Disallow merging into headers of the same
|
||||||
|
final intersectingHeaders = a.metadata.keys
|
||||||
|
.toSet()
|
||||||
|
.intersection(b.metadata.keys.toSet())
|
||||||
|
.intersection(headerKeys);
|
||||||
|
|
||||||
|
return Document(
|
||||||
|
id: a.id ?? b.id,
|
||||||
|
pageContent: '${a.pageContent}\n${b.pageContent}',
|
||||||
|
metadata: {
|
||||||
|
...a.metadata,
|
||||||
|
...b.metadata,
|
||||||
|
for (final header in intersectingHeaders)
|
||||||
|
header: {
|
||||||
|
..._headerValueList(a.metadata[header]),
|
||||||
|
..._headerValueList(b.metadata[header]),
|
||||||
|
}.toList(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
final mainDocumentId = originalDoc.id ?? uuid.v4();
|
||||||
|
final documentIdFactory = DocumentUuid.fromUuid(mainDocumentId);
|
||||||
|
|
||||||
|
return (
|
||||||
|
mainDocumentId: mainDocumentId,
|
||||||
|
parts: mergedDocChunks
|
||||||
|
.mapIndexed(
|
||||||
|
(i, doc) => Document(
|
||||||
|
id: documentIdFactory.getDocumentPartUuid(i),
|
||||||
|
pageContent: doc.pageContent,
|
||||||
|
metadata: mergeMaps(originalDoc.metadata, doc.metadata),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList()
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -122,30 +122,6 @@ class ContainerListScreen extends HookConsumerWidget {
|
|||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text('Containers'),
|
title: const Text('Containers'),
|
||||||
actions: [
|
|
||||||
IconButton(
|
|
||||||
onPressed: () async {
|
|
||||||
final initialColor =
|
|
||||||
await ref.read(unusedRandomContainerColorProvider.future);
|
|
||||||
|
|
||||||
if (context.mounted) {
|
|
||||||
final result = await showDialog<ContainerResult?>(
|
|
||||||
context: context,
|
|
||||||
builder: (context) => ContainerDialog.create(
|
|
||||||
initialColor: initialColor,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (result != null) {
|
|
||||||
await ref
|
|
||||||
.read(containerRepositoryProvider.notifier)
|
|
||||||
.addContainer(name: result.name, color: result.color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
icon: const Icon(Icons.add),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
body: HookConsumer(
|
body: HookConsumer(
|
||||||
builder: (context, ref, child) {
|
builder: (context, ref, child) {
|
||||||
@@ -206,6 +182,28 @@ class ContainerListScreen extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
floatingActionButton: FloatingActionButton(
|
||||||
|
onPressed: () async {
|
||||||
|
final initialColor =
|
||||||
|
await ref.read(unusedRandomContainerColorProvider.future);
|
||||||
|
|
||||||
|
if (context.mounted) {
|
||||||
|
final result = await showDialog<ContainerResult?>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => ContainerDialog.create(
|
||||||
|
initialColor: initialColor,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result != null) {
|
||||||
|
await ref
|
||||||
|
.read(containerRepositoryProvider.notifier)
|
||||||
|
.addContainer(name: result.name, color: result.color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: const Icon(Icons.add),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,225 +1,225 @@
|
|||||||
import 'package:expandable_page_view/expandable_page_view.dart';
|
// import 'package:expandable_page_view/expandable_page_view.dart';
|
||||||
import 'package:flutter/material.dart';
|
// import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
// import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
|
// import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
// import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:lensai/features/geckoview/features/browser/domain/providers.dart';
|
// import 'package:lensai/features/geckoview/features/browser/domain/providers.dart';
|
||||||
import 'package:lensai/features/geckoview/features/browser/presentation/widgets/sheets/create_tab.dart';
|
// import 'package:lensai/features/geckoview/features/browser/presentation/widgets/sheets/create_tab.dart';
|
||||||
import 'package:lensai/features/geckoview/features/browser/presentation/widgets/speech_to_text_button.dart';
|
// import 'package:lensai/presentation/widgets/speech_to_text_button.dart';
|
||||||
import 'package:lensai/features/kagi/data/entities/modes.dart';
|
// import 'package:lensai/features/kagi/data/entities/modes.dart';
|
||||||
import 'package:lensai/features/kagi/utils/url_builder.dart' as uri_builder;
|
// import 'package:lensai/features/kagi/utils/url_builder.dart' as uri_builder;
|
||||||
import 'package:lensai/features/share_intent/domain/entities/shared_content.dart';
|
// import 'package:lensai/features/share_intent/domain/entities/shared_content.dart';
|
||||||
import 'package:lensai/features/user/domain/repositories/settings.dart';
|
// import 'package:lensai/features/user/domain/repositories/settings.dart';
|
||||||
import 'package:lensai/presentation/hooks/sync_page_tab.dart';
|
// import 'package:lensai/presentation/hooks/sync_page_tab.dart';
|
||||||
|
|
||||||
class AssistantTab extends HookConsumerWidget {
|
// class AssistantTab extends HookConsumerWidget {
|
||||||
final SharedContent? sharedContent;
|
// final SharedContent? sharedContent;
|
||||||
final OnSubmitUri onSubmit;
|
// final OnSubmitUri onSubmit;
|
||||||
|
|
||||||
const AssistantTab({
|
// const AssistantTab({
|
||||||
required this.sharedContent,
|
// required this.sharedContent,
|
||||||
required this.onSubmit,
|
// required this.onSubmit,
|
||||||
super.key,
|
// super.key,
|
||||||
});
|
// });
|
||||||
|
|
||||||
@override
|
// @override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
// Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final incognitoEnabled = ref.watch(
|
// final incognitoEnabled = ref.watch(
|
||||||
settingsRepositoryProvider.select((value) => value.incognitoMode),
|
// settingsRepositoryProvider.select((value) => value.incognitoMode),
|
||||||
);
|
// );
|
||||||
|
|
||||||
final researchVariant = ref.watch(activeResearchVariantProvider);
|
// final researchVariant = ref.watch(activeResearchVariantProvider);
|
||||||
final chatModel = ref.watch(activeChatModelProvider);
|
// final chatModel = ref.watch(activeChatModelProvider);
|
||||||
|
|
||||||
useAutomaticKeepAlive();
|
// useAutomaticKeepAlive();
|
||||||
|
|
||||||
final formKey = useMemoized(() => GlobalKey<FormState>());
|
// final formKey = useMemoized(() => GlobalKey<FormState>());
|
||||||
final textController =
|
// final textController =
|
||||||
useTextEditingController(text: sharedContent?.toString());
|
// useTextEditingController(text: sharedContent?.toString());
|
||||||
|
|
||||||
final tabController = useTabController(
|
// final tabController = useTabController(
|
||||||
initialLength: AssistantMode.values.length,
|
// initialLength: AssistantMode.values.length,
|
||||||
initialIndex: ref.read(lastUsedAssistantModeProvider).index,
|
// initialIndex: ref.read(lastUsedAssistantModeProvider).index,
|
||||||
);
|
// );
|
||||||
final pageController = usePageController(initialPage: tabController.index);
|
// final pageController = usePageController(initialPage: tabController.index);
|
||||||
|
|
||||||
useSyncPageWithTab(
|
// useSyncPageWithTab(
|
||||||
tabController,
|
// tabController,
|
||||||
pageController,
|
// pageController,
|
||||||
onIndexChanged: (index) {
|
// onIndexChanged: (index) {
|
||||||
ref
|
// ref
|
||||||
.read(lastUsedAssistantModeProvider.notifier)
|
// .read(lastUsedAssistantModeProvider.notifier)
|
||||||
.update(AssistantMode.values[index]);
|
// .update(AssistantMode.values[index]);
|
||||||
},
|
// },
|
||||||
);
|
// );
|
||||||
|
|
||||||
return Form(
|
// return Form(
|
||||||
key: formKey,
|
// key: formKey,
|
||||||
child: Column(
|
// child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
// mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
// children: [
|
||||||
TabBar.secondary(
|
// TabBar.secondary(
|
||||||
controller: tabController,
|
// controller: tabController,
|
||||||
tabs: const [
|
// tabs: const [
|
||||||
Tab(
|
// Tab(
|
||||||
text: 'Research',
|
// text: 'Research',
|
||||||
icon: Icon(MdiIcons.layersSearch),
|
// icon: Icon(MdiIcons.layersSearch),
|
||||||
),
|
// ),
|
||||||
Tab(
|
// Tab(
|
||||||
text: 'Code',
|
// text: 'Code',
|
||||||
icon: Icon(MdiIcons.codeJson),
|
// icon: Icon(MdiIcons.codeJson),
|
||||||
),
|
// ),
|
||||||
Tab(
|
// Tab(
|
||||||
text: 'Chat',
|
// text: 'Chat',
|
||||||
icon: Icon(MdiIcons.commentTextMultiple),
|
// icon: Icon(MdiIcons.commentTextMultiple),
|
||||||
),
|
// ),
|
||||||
Tab(
|
// Tab(
|
||||||
text: 'Custom',
|
// text: 'Custom',
|
||||||
icon: Icon(MdiIcons.creation),
|
// icon: Icon(MdiIcons.creation),
|
||||||
),
|
// ),
|
||||||
],
|
// ],
|
||||||
),
|
// ),
|
||||||
ExpandablePageView(
|
// ExpandablePageView(
|
||||||
controller: pageController,
|
// controller: pageController,
|
||||||
children: [
|
// children: [
|
||||||
Column(
|
// Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
// mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
// children: [
|
||||||
const SizedBox(
|
// const SizedBox(
|
||||||
height: 12,
|
// height: 12,
|
||||||
),
|
// ),
|
||||||
SegmentedButton<ResearchVariant>(
|
// SegmentedButton<ResearchVariant>(
|
||||||
segments: const [
|
// segments: const [
|
||||||
ButtonSegment(
|
// ButtonSegment(
|
||||||
value: ResearchVariant.expert,
|
// value: ResearchVariant.expert,
|
||||||
icon: Icon(MdiIcons.textSearch),
|
// icon: Icon(MdiIcons.textSearch),
|
||||||
label: Text('Research'),
|
// label: Text('Research'),
|
||||||
),
|
// ),
|
||||||
ButtonSegment(
|
// ButtonSegment(
|
||||||
value: ResearchVariant.fast,
|
// value: ResearchVariant.fast,
|
||||||
icon: Icon(MdiIcons.invoiceTextFast),
|
// icon: Icon(MdiIcons.invoiceTextFast),
|
||||||
label: Text('Fast'),
|
// label: Text('Fast'),
|
||||||
),
|
// ),
|
||||||
],
|
// ],
|
||||||
selected: {researchVariant},
|
// selected: {researchVariant},
|
||||||
onSelectionChanged: (value) {
|
// onSelectionChanged: (value) {
|
||||||
ref
|
// ref
|
||||||
.read(activeResearchVariantProvider.notifier)
|
// .read(activeResearchVariantProvider.notifier)
|
||||||
.update(value.first);
|
// .update(value.first);
|
||||||
},
|
// },
|
||||||
),
|
// ),
|
||||||
const SizedBox(
|
// const SizedBox(
|
||||||
height: 12,
|
// height: 12,
|
||||||
),
|
// ),
|
||||||
_PromptField(textController, incognitoEnabled),
|
// _PromptField(textController, incognitoEnabled),
|
||||||
],
|
// ],
|
||||||
),
|
// ),
|
||||||
Column(
|
// Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
// mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
// children: [
|
||||||
const SizedBox(
|
// const SizedBox(
|
||||||
height: 12,
|
// height: 12,
|
||||||
),
|
// ),
|
||||||
_PromptField(textController, incognitoEnabled),
|
// _PromptField(textController, incognitoEnabled),
|
||||||
],
|
// ],
|
||||||
),
|
// ),
|
||||||
Column(
|
// Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
// mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
// children: [
|
||||||
const SizedBox(
|
// const SizedBox(
|
||||||
height: 12,
|
// height: 12,
|
||||||
),
|
// ),
|
||||||
DropdownMenu<ChatModel>(
|
// DropdownMenu<ChatModel>(
|
||||||
initialSelection: chatModel,
|
// initialSelection: chatModel,
|
||||||
expandedInsets: EdgeInsets.zero,
|
// expandedInsets: EdgeInsets.zero,
|
||||||
label: const Text('Model'),
|
// label: const Text('Model'),
|
||||||
inputDecorationTheme: const InputDecorationTheme(),
|
// inputDecorationTheme: const InputDecorationTheme(),
|
||||||
dropdownMenuEntries: ChatModel.values
|
// dropdownMenuEntries: ChatModel.values
|
||||||
.map(
|
// .map(
|
||||||
(model) => DropdownMenuEntry(
|
// (model) => DropdownMenuEntry(
|
||||||
value: model,
|
// value: model,
|
||||||
label: model.label,
|
// label: model.label,
|
||||||
),
|
// ),
|
||||||
)
|
// )
|
||||||
.toList(),
|
// .toList(),
|
||||||
onSelected: (value) {
|
// onSelected: (value) {
|
||||||
ref.read(activeChatModelProvider.notifier).update(value!);
|
// ref.read(activeChatModelProvider.notifier).update(value!);
|
||||||
},
|
// },
|
||||||
),
|
// ),
|
||||||
const SizedBox(
|
// const SizedBox(
|
||||||
height: 12,
|
// height: 12,
|
||||||
),
|
// ),
|
||||||
_PromptField(textController, incognitoEnabled),
|
// _PromptField(textController, incognitoEnabled),
|
||||||
],
|
// ],
|
||||||
),
|
// ),
|
||||||
Column(
|
// Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
// mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
// children: [
|
||||||
const SizedBox(
|
// const SizedBox(
|
||||||
height: 12,
|
// height: 12,
|
||||||
),
|
// ),
|
||||||
_PromptField(textController, incognitoEnabled),
|
// _PromptField(textController, incognitoEnabled),
|
||||||
],
|
// ],
|
||||||
),
|
// ),
|
||||||
],
|
// ],
|
||||||
),
|
// ),
|
||||||
const SizedBox(
|
// const SizedBox(
|
||||||
height: 12,
|
// height: 12,
|
||||||
),
|
// ),
|
||||||
SizedBox(
|
// SizedBox(
|
||||||
width: double.infinity,
|
// width: double.infinity,
|
||||||
child: FilledButton.icon(
|
// child: FilledButton.icon(
|
||||||
onPressed: () {
|
// onPressed: () {
|
||||||
if (formKey.currentState?.validate() ?? false) {
|
// if (formKey.currentState?.validate() ?? false) {
|
||||||
onSubmit(
|
// onSubmit(
|
||||||
uri_builder.assistantUri(
|
// uri_builder.assistantUri(
|
||||||
prompt: textController.text,
|
// prompt: textController.text,
|
||||||
assistantMode: AssistantMode.values[tabController.index],
|
// assistantMode: AssistantMode.values[tabController.index],
|
||||||
researchVariant: researchVariant,
|
// researchVariant: researchVariant,
|
||||||
chatModel: chatModel,
|
// chatModel: chatModel,
|
||||||
),
|
// ),
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
},
|
// },
|
||||||
label: const Text('Submit'),
|
// label: const Text('Submit'),
|
||||||
icon: const Icon(MdiIcons.invoiceTextSend),
|
// icon: const Icon(MdiIcons.invoiceTextSend),
|
||||||
),
|
// ),
|
||||||
),
|
// ),
|
||||||
],
|
// ],
|
||||||
),
|
// ),
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
class _PromptField extends StatelessWidget {
|
// class _PromptField extends StatelessWidget {
|
||||||
final TextEditingController textController;
|
// final TextEditingController textController;
|
||||||
final bool incognitoEnabled;
|
// final bool incognitoEnabled;
|
||||||
|
|
||||||
const _PromptField(this.textController, this.incognitoEnabled);
|
// const _PromptField(this.textController, this.incognitoEnabled);
|
||||||
|
|
||||||
@override
|
// @override
|
||||||
Widget build(BuildContext context) {
|
// Widget build(BuildContext context) {
|
||||||
return TextFormField(
|
// return TextFormField(
|
||||||
controller: textController,
|
// controller: textController,
|
||||||
enableIMEPersonalizedLearning: !incognitoEnabled,
|
// enableIMEPersonalizedLearning: !incognitoEnabled,
|
||||||
decoration: InputDecoration(
|
// decoration: InputDecoration(
|
||||||
label: const Text('Prompt'),
|
// label: const Text('Prompt'),
|
||||||
hintText: 'Enter your prompt...',
|
// hintText: 'Enter your prompt...',
|
||||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
// floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||||
suffixIcon: SpeechToTextButton(
|
// suffixIcon: SpeechToTextButton(
|
||||||
onTextReceived: (data) {
|
// onTextReceived: (data) {
|
||||||
textController.text = data.toString();
|
// textController.text = data.toString();
|
||||||
},
|
// },
|
||||||
),
|
// ),
|
||||||
),
|
// ),
|
||||||
maxLines: null,
|
// maxLines: null,
|
||||||
validator: (value) {
|
// validator: (value) {
|
||||||
if (value?.isEmpty ?? true) {
|
// if (value?.isEmpty ?? true) {
|
||||||
return '';
|
// return '';
|
||||||
}
|
// }
|
||||||
|
|
||||||
return null;
|
// return null;
|
||||||
},
|
// },
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|||||||
@@ -1,195 +1,195 @@
|
|||||||
import 'package:flutter/material.dart';
|
// import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
// import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
|
// import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
// import 'package:go_router/go_router.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
// import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:lensai/core/routing/routes.dart';
|
// import 'package:lensai/core/routing/routes.dart';
|
||||||
import 'package:lensai/features/bangs/domain/providers/bangs.dart';
|
// import 'package:lensai/features/bangs/domain/providers/bangs.dart';
|
||||||
import 'package:lensai/features/bangs/domain/providers/search.dart';
|
// import 'package:lensai/features/bangs/domain/providers/search.dart';
|
||||||
import 'package:lensai/features/bangs/domain/repositories/data.dart';
|
// import 'package:lensai/features/bangs/domain/repositories/data.dart';
|
||||||
import 'package:lensai/features/bangs/presentation/widgets/bang_icon.dart';
|
// import 'package:lensai/features/bangs/presentation/widgets/bang_icon.dart';
|
||||||
import 'package:lensai/features/bangs/presentation/widgets/search_field.dart';
|
// import 'package:lensai/features/bangs/presentation/widgets/search_field.dart';
|
||||||
import 'package:lensai/features/geckoview/features/browser/domain/providers.dart';
|
// import 'package:lensai/features/geckoview/features/browser/domain/providers.dart';
|
||||||
import 'package:lensai/features/geckoview/features/browser/presentation/widgets/sheets/create_tab.dart';
|
// import 'package:lensai/features/geckoview/features/browser/presentation/widgets/sheets/create_tab.dart';
|
||||||
import 'package:lensai/features/geckoview/features/controllers/bottom_sheet.dart';
|
// import 'package:lensai/features/geckoview/domain/controllers/bottom_sheet.dart';
|
||||||
import 'package:lensai/features/share_intent/domain/entities/shared_content.dart';
|
// import 'package:lensai/features/share_intent/domain/entities/shared_content.dart';
|
||||||
import 'package:lensai/presentation/widgets/selectable_chips.dart';
|
// import 'package:lensai/presentation/widgets/selectable_chips.dart';
|
||||||
|
|
||||||
class SearchTab extends HookConsumerWidget {
|
// class SearchTab extends HookConsumerWidget {
|
||||||
final SharedContent? sharedContent;
|
// final SharedContent? sharedContent;
|
||||||
final OnSubmitUri onSubmit;
|
// final OnSubmitUri onSubmit;
|
||||||
|
|
||||||
const SearchTab({
|
// const SearchTab({
|
||||||
required this.sharedContent,
|
// required this.sharedContent,
|
||||||
required this.onSubmit,
|
// required this.onSubmit,
|
||||||
super.key,
|
// super.key,
|
||||||
});
|
// });
|
||||||
|
|
||||||
@override
|
// @override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
// Widget build(BuildContext context, WidgetRef ref) {
|
||||||
useAutomaticKeepAlive();
|
// useAutomaticKeepAlive();
|
||||||
|
|
||||||
final formKey = useMemoized(() => GlobalKey<FormState>());
|
// final formKey = useMemoized(() => GlobalKey<FormState>());
|
||||||
|
|
||||||
final selectedBang = ref
|
// final selectedBang = ref
|
||||||
.watch(selectedBangDataProvider().select((value) => value.valueOrNull));
|
// .watch(selectedBangDataProvider().select((value) => value.valueOrNull));
|
||||||
final defaultSearchBang = ref.watch(
|
// final defaultSearchBang = ref.watch(
|
||||||
defaultSearchBangDataProvider.select((value) => value.valueOrNull),
|
// defaultSearchBangDataProvider.select((value) => value.valueOrNull),
|
||||||
);
|
// );
|
||||||
|
|
||||||
final activeBang = selectedBang ?? defaultSearchBang;
|
// final activeBang = selectedBang ?? defaultSearchBang;
|
||||||
|
|
||||||
final textController =
|
// final textController =
|
||||||
useTextEditingController(text: sharedContent?.toString());
|
// useTextEditingController(text: sharedContent?.toString());
|
||||||
|
|
||||||
Future<void> submitSearch() async {
|
// Future<void> submitSearch() async {
|
||||||
if (activeBang != null && (formKey.currentState?.validate() == true)) {
|
// if (activeBang != null && (formKey.currentState?.validate() == true)) {
|
||||||
final searchUri = await ref.read(
|
// final searchUri = await ref.read(
|
||||||
triggerBangSearchProvider(activeBang, textController.text).future,
|
// triggerBangSearchProvider(activeBang, textController.text).future,
|
||||||
);
|
// );
|
||||||
|
|
||||||
onSubmit(searchUri);
|
// onSubmit(searchUri);
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
return Form(
|
// return Form(
|
||||||
key: formKey,
|
// key: formKey,
|
||||||
child: Column(
|
// child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
// mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
// children: [
|
||||||
const SizedBox(height: 4),
|
// const SizedBox(height: 4),
|
||||||
Consumer(
|
// Consumer(
|
||||||
builder: (context, ref, child) {
|
// builder: (context, ref, child) {
|
||||||
final frequentBangsAsync =
|
// final frequentBangsAsync =
|
||||||
ref.watch(frequentBangDataListProvider);
|
// ref.watch(frequentBangDataListProvider);
|
||||||
|
|
||||||
return frequentBangsAsync.when(
|
// return frequentBangsAsync.when(
|
||||||
data: (availableBangs) => SizedBox(
|
// data: (availableBangs) => SizedBox(
|
||||||
height: 48,
|
// height: 48,
|
||||||
child: Row(
|
// child: Row(
|
||||||
children: [
|
// children: [
|
||||||
if (selectedBang != null || availableBangs.isNotEmpty)
|
// if (selectedBang != null || availableBangs.isNotEmpty)
|
||||||
Expanded(
|
// Expanded(
|
||||||
child: SelectableChips(
|
// child: SelectableChips(
|
||||||
itemId: (bang) => bang.trigger,
|
// itemId: (bang) => bang.trigger,
|
||||||
itemAvatar: (bang) => BangIcon(bang, iconSize: 20),
|
// itemAvatar: (bang) => BangIcon(bang, iconSize: 20),
|
||||||
itemLabel: (bang) => Text(bang.websiteName),
|
// itemLabel: (bang) => Text(bang.websiteName),
|
||||||
availableItems: availableBangs,
|
// availableItems: availableBangs,
|
||||||
selectedItem: selectedBang,
|
// selectedItem: selectedBang,
|
||||||
onSelected: (bang) {
|
// onSelected: (bang) {
|
||||||
ref
|
// ref
|
||||||
.read(selectedBangTriggerProvider().notifier)
|
// .read(selectedBangTriggerProvider().notifier)
|
||||||
.setTrigger(bang.trigger);
|
// .setTrigger(bang.trigger);
|
||||||
},
|
// },
|
||||||
onDeleted: (bang) async {
|
// onDeleted: (bang) async {
|
||||||
if (ref.read(selectedBangTriggerProvider()) ==
|
// if (ref.read(selectedBangTriggerProvider()) ==
|
||||||
bang.trigger) {
|
// bang.trigger) {
|
||||||
ref
|
// ref
|
||||||
.read(
|
// .read(
|
||||||
selectedBangTriggerProvider().notifier,
|
// selectedBangTriggerProvider().notifier,
|
||||||
)
|
// )
|
||||||
.clearTrigger();
|
// .clearTrigger();
|
||||||
} else {
|
// } else {
|
||||||
final dialogResult = await showDialog<bool>(
|
// final dialogResult = await showDialog<bool>(
|
||||||
context: context,
|
// context: context,
|
||||||
builder: (context) => AlertDialog(
|
// builder: (context) => AlertDialog(
|
||||||
title: Text(
|
// title: Text(
|
||||||
'Reset usage frequency of !${bang.trigger}?',
|
// 'Reset usage frequency of !${bang.trigger}?',
|
||||||
),
|
// ),
|
||||||
content: const Text(
|
// content: const Text(
|
||||||
'This will remove the Bang from quick select.',
|
// 'This will remove the Bang from quick select.',
|
||||||
),
|
// ),
|
||||||
actions: <Widget>[
|
// actions: <Widget>[
|
||||||
TextButton(
|
// TextButton(
|
||||||
onPressed: () =>
|
// onPressed: () =>
|
||||||
Navigator.pop(context, false),
|
// Navigator.pop(context, false),
|
||||||
child: const Text('Cancel'),
|
// child: const Text('Cancel'),
|
||||||
),
|
// ),
|
||||||
TextButton(
|
// TextButton(
|
||||||
onPressed: () =>
|
// onPressed: () =>
|
||||||
Navigator.pop(context, true),
|
// Navigator.pop(context, true),
|
||||||
child: const Text('Reset'),
|
// child: const Text('Reset'),
|
||||||
),
|
// ),
|
||||||
],
|
// ],
|
||||||
),
|
// ),
|
||||||
);
|
// );
|
||||||
|
|
||||||
if (dialogResult == true) {
|
// if (dialogResult == true) {
|
||||||
await ref
|
// await ref
|
||||||
.read(bangDataRepositoryProvider.notifier)
|
// .read(bangDataRepositoryProvider.notifier)
|
||||||
.resetFrequency(bang.trigger);
|
// .resetFrequency(bang.trigger);
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
},
|
// },
|
||||||
),
|
// ),
|
||||||
)
|
// )
|
||||||
else
|
// else
|
||||||
Expanded(
|
// Expanded(
|
||||||
child: Text(
|
// child: Text(
|
||||||
"Press '>' to search Bangs.",
|
// "Press '>' to search Bangs.",
|
||||||
style: TextStyle(
|
// style: TextStyle(
|
||||||
color: Theme.of(context).hintColor,
|
// color: Theme.of(context).hintColor,
|
||||||
fontStyle: FontStyle.italic,
|
// fontStyle: FontStyle.italic,
|
||||||
),
|
// ),
|
||||||
),
|
// ),
|
||||||
),
|
// ),
|
||||||
IconButton(
|
// IconButton(
|
||||||
onPressed: () async {
|
// onPressed: () async {
|
||||||
await context.push(BangSearchRoute().location);
|
// await context.push(BangSearchRoute().location);
|
||||||
},
|
// },
|
||||||
icon: const Icon(Icons.chevron_right),
|
// icon: const Icon(Icons.chevron_right),
|
||||||
),
|
// ),
|
||||||
],
|
// ],
|
||||||
),
|
// ),
|
||||||
),
|
// ),
|
||||||
error: (error, stackTrace) => const SizedBox.shrink(),
|
// error: (error, stackTrace) => const SizedBox.shrink(),
|
||||||
loading: () => const SizedBox(
|
// loading: () => const SizedBox(
|
||||||
height: 48,
|
// height: 48,
|
||||||
width: double.infinity,
|
// width: double.infinity,
|
||||||
),
|
// ),
|
||||||
);
|
// );
|
||||||
},
|
// },
|
||||||
),
|
// ),
|
||||||
Consumer(
|
// Consumer(
|
||||||
builder: (context, ref, child) {
|
// builder: (context, ref, child) {
|
||||||
const maxOptionsHeight = SearchField.defaultMaxOptionsHeight;
|
// const maxOptionsHeight = SearchField.defaultMaxOptionsHeight;
|
||||||
|
|
||||||
final openDirection = ref.watch(
|
// final openDirection = ref.watch(
|
||||||
bottomSheetExtendProvider.select((value) {
|
// bottomSheetExtendProvider.select((value) {
|
||||||
final extend = value.valueOrNull;
|
// final extend = value.valueOrNull;
|
||||||
if (extend == null ||
|
// if (extend == null ||
|
||||||
(MediaQuery.of(context).size.height * (1 - extend)) >
|
// (MediaQuery.of(context).size.height * (1 - extend)) >
|
||||||
maxOptionsHeight) {
|
// maxOptionsHeight) {
|
||||||
return OptionsViewOpenDirection.up;
|
// return OptionsViewOpenDirection.up;
|
||||||
} else {
|
// } else {
|
||||||
return OptionsViewOpenDirection.down;
|
// return OptionsViewOpenDirection.down;
|
||||||
}
|
// }
|
||||||
}),
|
// }),
|
||||||
);
|
// );
|
||||||
|
|
||||||
return SearchField(
|
// return SearchField(
|
||||||
textController: textController,
|
// textController: textController,
|
||||||
activeBang: activeBang,
|
// activeBang: activeBang,
|
||||||
openDirection: openDirection,
|
// openDirection: openDirection,
|
||||||
onFieldSubmitted: (_) async {
|
// onFieldSubmitted: (_) async {
|
||||||
await submitSearch();
|
// await submitSearch();
|
||||||
},
|
// },
|
||||||
);
|
// );
|
||||||
},
|
// },
|
||||||
),
|
// ),
|
||||||
const SizedBox(
|
// const SizedBox(
|
||||||
height: 12,
|
// height: 12,
|
||||||
),
|
// ),
|
||||||
SizedBox(
|
// SizedBox(
|
||||||
width: double.infinity,
|
// width: double.infinity,
|
||||||
child: FilledButton.icon(
|
// child: FilledButton.icon(
|
||||||
onPressed: submitSearch,
|
// onPressed: submitSearch,
|
||||||
label: const Text('Search'),
|
// label: const Text('Search'),
|
||||||
icon: const Icon(MdiIcons.cloudSearch),
|
// icon: const Icon(MdiIcons.cloudSearch),
|
||||||
),
|
// ),
|
||||||
),
|
// ),
|
||||||
],
|
// ],
|
||||||
),
|
// ),
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|||||||
@@ -3,4 +3,5 @@ abstract interface class IQueryBuilder {
|
|||||||
int get ftsMinTokenLength;
|
int get ftsMinTokenLength;
|
||||||
|
|
||||||
String buildFtsQuery(String input);
|
String buildFtsQuery(String input);
|
||||||
|
String buildLikeQuery(String input);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import 'dart:math' as math;
|
|||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:lensai/features/search/domain/entities/abstract/i_query_builder.dart';
|
import 'package:lensai/features/search/domain/entities/abstract/i_query_builder.dart';
|
||||||
import 'package:lensai/features/search/domain/entities/bareword.dart';
|
import 'package:lensai/features/search/domain/entities/bareword.dart';
|
||||||
|
import 'package:lensai/features/search/domain/unix_tokenizer.dart';
|
||||||
|
|
||||||
typedef _Phrase = List<Bareword>;
|
typedef _Phrase = List<Bareword>;
|
||||||
|
|
||||||
@@ -228,7 +229,26 @@ mixin PrefixQueryBuilderMixin implements IQueryBuilder {
|
|||||||
tokenLimit: ftsTokenLimit,
|
tokenLimit: ftsTokenLimit,
|
||||||
);
|
);
|
||||||
|
|
||||||
return queryBuilder.build();
|
if (queryBuilder.hasTokens) {
|
||||||
|
return queryBuilder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String buildLikeQuery(String input) {
|
||||||
|
final likeQueryBuilder = UnixLikeQueryBuilder.tokenize(
|
||||||
|
input: input,
|
||||||
|
minTokenLength: 1,
|
||||||
|
tokenLimit: 5,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (likeQueryBuilder.hasTokens) {
|
||||||
|
return likeQueryBuilder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -241,6 +261,25 @@ mixin TrigramQueryBuilderMixin implements IQueryBuilder {
|
|||||||
tokenLimit: ftsTokenLimit,
|
tokenLimit: ftsTokenLimit,
|
||||||
);
|
);
|
||||||
|
|
||||||
return queryBuilder.build();
|
if (queryBuilder.hasTokens) {
|
||||||
|
return queryBuilder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String buildLikeQuery(String input) {
|
||||||
|
final likeQueryBuilder = UnixLikeQueryBuilder.tokenize(
|
||||||
|
input: input,
|
||||||
|
minTokenLength: 1,
|
||||||
|
tokenLimit: 5,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (likeQueryBuilder.hasTokens) {
|
||||||
|
return likeQueryBuilder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import 'package:lensai/features/geckoview/features/tabs/presentation/widgets/con
|
|||||||
import 'package:lensai/features/search/domain/providers/search_suggestions.dart';
|
import 'package:lensai/features/search/domain/providers/search_suggestions.dart';
|
||||||
import 'package:lensai/features/search/presentation/widgets/bang_chips.dart';
|
import 'package:lensai/features/search/presentation/widgets/bang_chips.dart';
|
||||||
import 'package:lensai/features/search/presentation/widgets/search_field.dart';
|
import 'package:lensai/features/search/presentation/widgets/search_field.dart';
|
||||||
|
import 'package:lensai/features/search/presentation/widgets/search_suggestion_list.dart';
|
||||||
import 'package:lensai/presentation/hooks/listenable_callback.dart';
|
import 'package:lensai/presentation/hooks/listenable_callback.dart';
|
||||||
import 'package:lensai/presentation/widgets/failure_widget.dart';
|
import 'package:lensai/presentation/widgets/failure_widget.dart';
|
||||||
import 'package:skeletonizer/skeletonizer.dart';
|
import 'package:skeletonizer/skeletonizer.dart';
|
||||||
@@ -166,76 +167,9 @@ class SearchScreen extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
HookConsumer(
|
SearchSuggestionList(
|
||||||
builder: (context, ref, child) {
|
searchTextController: searchTextController,
|
||||||
useListenableSelector(
|
submitSearch: submitSearch,
|
||||||
searchTextController,
|
|
||||||
() => searchTextController.text.isNotEmpty,
|
|
||||||
);
|
|
||||||
|
|
||||||
final suggestions =
|
|
||||||
useStream(ref.watch(searchSuggestionsProvider()));
|
|
||||||
|
|
||||||
final searchHistory = ref.watch(searchHistoryProvider);
|
|
||||||
|
|
||||||
final searchText = searchTextController.text;
|
|
||||||
|
|
||||||
if ((!searchText.isNotEmpty || !suggestions.hasData) &&
|
|
||||||
(searchHistory.value?.isNotEmpty ?? false)) {
|
|
||||||
final entries = searchHistory.value!;
|
|
||||||
|
|
||||||
return SliverList.builder(
|
|
||||||
itemCount: entries.length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final query = entries[index].searchQuery;
|
|
||||||
|
|
||||||
return ListTile(
|
|
||||||
leading: const Icon(Icons.history),
|
|
||||||
title: Text(query),
|
|
||||||
onLongPress: () {
|
|
||||||
searchTextController.text = query;
|
|
||||||
},
|
|
||||||
onTap: () async {
|
|
||||||
await submitSearch(query);
|
|
||||||
},
|
|
||||||
trailing: IconButton(
|
|
||||||
onPressed: () async {
|
|
||||||
await ref
|
|
||||||
.read(bangDataRepositoryProvider.notifier)
|
|
||||||
.removeSearchEntry(query);
|
|
||||||
},
|
|
||||||
icon: const Icon(Icons.close),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
final prioritizedSuggestions = [
|
|
||||||
if (searchText.isNotEmpty) searchText,
|
|
||||||
if (suggestions.data != null)
|
|
||||||
...suggestions.data!
|
|
||||||
.whereNot((suggestion) => suggestion == searchText),
|
|
||||||
];
|
|
||||||
|
|
||||||
return SliverList.builder(
|
|
||||||
itemCount: prioritizedSuggestions.length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final suggestion = prioritizedSuggestions[index];
|
|
||||||
|
|
||||||
return ListTile(
|
|
||||||
leading: const Icon(Icons.search),
|
|
||||||
title: Text(suggestion),
|
|
||||||
onLongPress: () {
|
|
||||||
searchTextController.text = suggestion;
|
|
||||||
},
|
|
||||||
onTap: () async {
|
|
||||||
await submitSearch(suggestion);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
const SliverToBoxAdapter(
|
const SliverToBoxAdapter(
|
||||||
child: Divider(),
|
child: Divider(),
|
||||||
@@ -274,7 +208,9 @@ class SearchScreen extends HookConsumerWidget {
|
|||||||
child: tabs.when(
|
child: tabs.when(
|
||||||
data: (data) {
|
data: (data) {
|
||||||
if (data == null) {
|
if (data == null) {
|
||||||
return const SizedBox.shrink();
|
return const SliverToBoxAdapter(
|
||||||
|
child: SizedBox.shrink(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return SliverList.builder(
|
return SliverList.builder(
|
||||||
@@ -283,12 +219,14 @@ class SearchScreen extends HookConsumerWidget {
|
|||||||
final result = data[index];
|
final result = data[index];
|
||||||
|
|
||||||
final headHasMatch =
|
final headHasMatch =
|
||||||
result.title.contains(_matchPrefix);
|
result.title?.contains(_matchPrefix) ?? false;
|
||||||
final bodyResult =
|
final bodyResult =
|
||||||
result.extractedContent ?? result.fullContent;
|
result.extractedContent ?? result.fullContent;
|
||||||
|
|
||||||
return ListTile(
|
return ListTile(
|
||||||
title: MarkdownBody(data: result.title),
|
title: (result.title != null)
|
||||||
|
? MarkdownBody(data: result.title!)
|
||||||
|
: null,
|
||||||
subtitle: (!headHasMatch && bodyResult != null)
|
subtitle: (!headHasMatch && bodyResult != null)
|
||||||
? MarkdownBody(data: bodyResult)
|
? MarkdownBody(data: bodyResult)
|
||||||
: null,
|
: null,
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:lensai/features/bangs/data/models/bang_data.dart';
|
import 'package:lensai/features/bangs/data/models/bang_data.dart';
|
||||||
import 'package:lensai/features/bangs/presentation/widgets/bang_icon.dart';
|
import 'package:lensai/features/bangs/presentation/widgets/bang_icon.dart';
|
||||||
import 'package:lensai/features/geckoview/features/browser/presentation/widgets/speech_to_text_button.dart';
|
|
||||||
import 'package:lensai/features/user/domain/repositories/settings.dart';
|
import 'package:lensai/features/user/domain/repositories/settings.dart';
|
||||||
|
import 'package:lensai/presentation/widgets/speech_to_text_button.dart';
|
||||||
|
|
||||||
class SearchField extends HookConsumerWidget {
|
class SearchField extends HookConsumerWidget {
|
||||||
final TextEditingController textEditingController;
|
final TextEditingController textEditingController;
|
||||||
@@ -64,14 +64,6 @@ class SearchField extends HookConsumerWidget {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// autovalidateMode: AutovalidateMode.onUserInteraction,
|
|
||||||
// validator: (value) {
|
|
||||||
// if (value?.isEmpty ?? true) {
|
|
||||||
// return '';
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return null;
|
|
||||||
// },
|
|
||||||
onTapOutside: (focusNode != null)
|
onTapOutside: (focusNode != null)
|
||||||
? (event) {
|
? (event) {
|
||||||
focusNode!.unfocus();
|
focusNode!.unfocus();
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:lensai/features/bangs/domain/providers/bangs.dart';
|
||||||
|
import 'package:lensai/features/bangs/domain/repositories/data.dart';
|
||||||
|
import 'package:lensai/features/search/domain/providers/search_suggestions.dart';
|
||||||
|
|
||||||
|
class SearchSuggestionList extends HookConsumerWidget {
|
||||||
|
final TextEditingController searchTextController;
|
||||||
|
final Future<void> Function(String query) submitSearch;
|
||||||
|
final bool showHistory;
|
||||||
|
|
||||||
|
const SearchSuggestionList({
|
||||||
|
required this.searchTextController,
|
||||||
|
required this.submitSearch,
|
||||||
|
this.showHistory = true,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
useListenableSelector(
|
||||||
|
searchTextController,
|
||||||
|
() => searchTextController.text.isNotEmpty,
|
||||||
|
);
|
||||||
|
|
||||||
|
final suggestions = useStream(ref.watch(searchSuggestionsProvider()));
|
||||||
|
|
||||||
|
final searchHistory = ref.watch(searchHistoryProvider);
|
||||||
|
|
||||||
|
final searchText = searchTextController.text;
|
||||||
|
|
||||||
|
if (showHistory &&
|
||||||
|
(!searchText.isNotEmpty || !suggestions.hasData) &&
|
||||||
|
(searchHistory.value?.isNotEmpty ?? false)) {
|
||||||
|
final entries = searchHistory.value!;
|
||||||
|
|
||||||
|
return SliverList.builder(
|
||||||
|
itemCount: entries.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final query = entries[index].searchQuery;
|
||||||
|
|
||||||
|
return ListTile(
|
||||||
|
leading: const Icon(Icons.history),
|
||||||
|
title: Text(query),
|
||||||
|
onLongPress: () {
|
||||||
|
searchTextController.text = query;
|
||||||
|
},
|
||||||
|
onTap: () async {
|
||||||
|
await submitSearch(query);
|
||||||
|
},
|
||||||
|
trailing: IconButton(
|
||||||
|
onPressed: () async {
|
||||||
|
await ref
|
||||||
|
.read(bangDataRepositoryProvider.notifier)
|
||||||
|
.removeSearchEntry(query);
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.close),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final prioritizedSuggestions = [
|
||||||
|
if (searchText.isNotEmpty) searchText,
|
||||||
|
if (suggestions.data != null)
|
||||||
|
...suggestions.data!.whereNot((suggestion) => suggestion == searchText),
|
||||||
|
];
|
||||||
|
|
||||||
|
return SliverList.builder(
|
||||||
|
itemCount: prioritizedSuggestions.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final suggestion = prioritizedSuggestions[index];
|
||||||
|
|
||||||
|
return ListTile(
|
||||||
|
leading: const Icon(Icons.search),
|
||||||
|
title: Text(suggestion),
|
||||||
|
onLongPress: () {
|
||||||
|
searchTextController.text = suggestion;
|
||||||
|
},
|
||||||
|
onTap: () async {
|
||||||
|
await submitSearch(suggestion);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,7 +8,7 @@ import 'package:lensai/features/settings/presentation/controllers/save_settings.
|
|||||||
import 'package:lensai/features/settings/presentation/widgets/bang_group_list_tile.dart';
|
import 'package:lensai/features/settings/presentation/widgets/bang_group_list_tile.dart';
|
||||||
import 'package:lensai/features/settings/presentation/widgets/custom_list_tile.dart';
|
import 'package:lensai/features/settings/presentation/widgets/custom_list_tile.dart';
|
||||||
import 'package:lensai/features/user/data/models/settings.dart';
|
import 'package:lensai/features/user/data/models/settings.dart';
|
||||||
import 'package:lensai/features/user/domain/providers/cache.dart';
|
import 'package:lensai/features/user/domain/providers.dart';
|
||||||
import 'package:lensai/features/user/domain/repositories/cache.dart';
|
import 'package:lensai/features/user/domain/repositories/cache.dart';
|
||||||
import 'package:lensai/features/user/domain/repositories/settings.dart';
|
import 'package:lensai/features/user/domain/repositories/settings.dart';
|
||||||
|
|
||||||
@@ -149,11 +149,6 @@ class SettingsScreen extends HookConsumerWidget {
|
|||||||
const SizedBox(
|
const SizedBox(
|
||||||
height: 16,
|
height: 16,
|
||||||
),
|
),
|
||||||
_buildSection(theme, 'Content Blocking'),
|
|
||||||
_buildSubSection(theme, 'Lists'),
|
|
||||||
const SizedBox(
|
|
||||||
height: 16,
|
|
||||||
),
|
|
||||||
_buildSection(theme, 'Bangs'),
|
_buildSection(theme, 'Bangs'),
|
||||||
CustomListTile(
|
CustomListTile(
|
||||||
title: 'Bang Frequencies',
|
title: 'Bang Frequencies',
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:drift/native.dart';
|
import 'package:drift/native.dart';
|
||||||
import 'package:lensai/features/user/data/database/database.dart';
|
import 'package:lensai/features/user/data/database/database.dart';
|
||||||
@@ -13,7 +15,7 @@ part 'providers.g.dart';
|
|||||||
|
|
||||||
@Riverpod(keepAlive: true)
|
@Riverpod(keepAlive: true)
|
||||||
UserDatabase userDatabase(Ref ref) {
|
UserDatabase userDatabase(Ref ref) {
|
||||||
return UserDatabase(
|
final db = UserDatabase(
|
||||||
LazyDatabase(() async {
|
LazyDatabase(() async {
|
||||||
// put the database file, called db.sqlite here, into the documents folder
|
// put the database file, called db.sqlite here, into the documents folder
|
||||||
// for your app.
|
// for your app.
|
||||||
@@ -35,4 +37,10 @@ UserDatabase userDatabase(Ref ref) {
|
|||||||
return NativeDatabase.createInBackground(file);
|
return NativeDatabase.createInBackground(file);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
ref.onDispose(() {
|
||||||
|
unawaited(db.close());
|
||||||
|
});
|
||||||
|
|
||||||
|
return db;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ part of 'providers.dart';
|
|||||||
// RiverpodGenerator
|
// RiverpodGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$userDatabaseHash() => r'a84f951e1673e34b6857e1618cd7d80497c6f3a1';
|
String _$userDatabaseHash() => r'717de293750f5da91dc7da3f964bd07f5dd61e10';
|
||||||
|
|
||||||
/// See also [userDatabase].
|
/// See also [userDatabase].
|
||||||
@ProviderFor(userDatabase)
|
@ProviderFor(userDatabase)
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
|
import 'package:lensai/features/user/data/providers.dart';
|
||||||
|
import 'package:pocketbase/pocketbase.dart';
|
||||||
|
import 'package:riverpod/riverpod.dart';
|
||||||
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
|
||||||
|
part 'providers.g.dart';
|
||||||
|
|
||||||
|
const _authKey = 'pb_auth';
|
||||||
|
|
||||||
|
@Riverpod()
|
||||||
|
Stream<double> iconCacheSizeMegabytes(Ref ref) {
|
||||||
|
final repository = ref.watch(userDatabaseProvider);
|
||||||
|
return repository.cacheDao.iconCacheSize().watchSingle();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Riverpod()
|
||||||
|
Future<String?> _storedAuthData(Ref ref) {
|
||||||
|
const secureStorage = FlutterSecureStorage();
|
||||||
|
return secureStorage.read(key: _authKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Riverpod(keepAlive: true)
|
||||||
|
AsyncAuthStore authStore(Ref ref) {
|
||||||
|
const secureStorage = FlutterSecureStorage();
|
||||||
|
|
||||||
|
final intial =
|
||||||
|
ref.watch(_storedAuthDataProvider.select((value) => value.valueOrNull));
|
||||||
|
|
||||||
|
return AsyncAuthStore(
|
||||||
|
initial: intial,
|
||||||
|
save: (data) async {
|
||||||
|
await secureStorage.write(key: _authKey, value: data);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Riverpod()
|
||||||
|
Stream<AuthStoreEvent> authState(Ref ref) {
|
||||||
|
final authStore = ref.watch(authStoreProvider);
|
||||||
|
return authStore.onChange;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Riverpod(keepAlive: true)
|
||||||
|
PocketBase pocketBase(Ref ref) {
|
||||||
|
final authStore = ref.watch(authStoreProvider);
|
||||||
|
|
||||||
|
return PocketBase('http://192.168.2.104:8090', authStore: authStore);
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'providers.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// RiverpodGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
String _$iconCacheSizeMegabytesHash() =>
|
||||||
|
r'43162fce3ce8b04a5a55ed52021b274afbde2861';
|
||||||
|
|
||||||
|
/// See also [iconCacheSizeMegabytes].
|
||||||
|
@ProviderFor(iconCacheSizeMegabytes)
|
||||||
|
final iconCacheSizeMegabytesProvider =
|
||||||
|
AutoDisposeStreamProvider<double>.internal(
|
||||||
|
iconCacheSizeMegabytes,
|
||||||
|
name: r'iconCacheSizeMegabytesProvider',
|
||||||
|
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||||
|
? null
|
||||||
|
: _$iconCacheSizeMegabytesHash,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
|
// ignore: unused_element
|
||||||
|
typedef IconCacheSizeMegabytesRef = AutoDisposeStreamProviderRef<double>;
|
||||||
|
String _$storedAuthDataHash() => r'5f7e3ef6233a2036f7ce3728131901a46b1e548e';
|
||||||
|
|
||||||
|
/// See also [_storedAuthData].
|
||||||
|
@ProviderFor(_storedAuthData)
|
||||||
|
final _storedAuthDataProvider = AutoDisposeFutureProvider<String?>.internal(
|
||||||
|
_storedAuthData,
|
||||||
|
name: r'_storedAuthDataProvider',
|
||||||
|
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||||
|
? null
|
||||||
|
: _$storedAuthDataHash,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
|
// ignore: unused_element
|
||||||
|
typedef _StoredAuthDataRef = AutoDisposeFutureProviderRef<String?>;
|
||||||
|
String _$authStoreHash() => r'766c001bbb8d7dc4e148eff3816c414d78e77f26';
|
||||||
|
|
||||||
|
/// See also [authStore].
|
||||||
|
@ProviderFor(authStore)
|
||||||
|
final authStoreProvider = Provider<AsyncAuthStore>.internal(
|
||||||
|
authStore,
|
||||||
|
name: r'authStoreProvider',
|
||||||
|
debugGetCreateSourceHash:
|
||||||
|
const bool.fromEnvironment('dart.vm.product') ? null : _$authStoreHash,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
|
// ignore: unused_element
|
||||||
|
typedef AuthStoreRef = ProviderRef<AsyncAuthStore>;
|
||||||
|
String _$authStateHash() => r'ffa411ae0abc5c7bb3e8a93e783bd8398ce7f666';
|
||||||
|
|
||||||
|
/// See also [authState].
|
||||||
|
@ProviderFor(authState)
|
||||||
|
final authStateProvider = AutoDisposeStreamProvider<AuthStoreEvent>.internal(
|
||||||
|
authState,
|
||||||
|
name: r'authStateProvider',
|
||||||
|
debugGetCreateSourceHash:
|
||||||
|
const bool.fromEnvironment('dart.vm.product') ? null : _$authStateHash,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
|
// ignore: unused_element
|
||||||
|
typedef AuthStateRef = AutoDisposeStreamProviderRef<AuthStoreEvent>;
|
||||||
|
String _$pocketBaseHash() => r'9e1dc8057e300bc7ca697da9f68ec61e70fdbaab';
|
||||||
|
|
||||||
|
/// See also [pocketBase].
|
||||||
|
@ProviderFor(pocketBase)
|
||||||
|
final pocketBaseProvider = Provider<PocketBase>.internal(
|
||||||
|
pocketBase,
|
||||||
|
name: r'pocketBaseProvider',
|
||||||
|
debugGetCreateSourceHash:
|
||||||
|
const bool.fromEnvironment('dart.vm.product') ? null : _$pocketBaseHash,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
|
// ignore: unused_element
|
||||||
|
typedef PocketBaseRef = ProviderRef<PocketBase>;
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import 'package:lensai/features/user/data/providers.dart';
|
|
||||||
import 'package:riverpod/riverpod.dart';
|
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
|
||||||
|
|
||||||
part 'cache.g.dart';
|
|
||||||
|
|
||||||
@Riverpod()
|
|
||||||
Stream<double> iconCacheSizeMegabytes(Ref ref) {
|
|
||||||
final repository = ref.watch(userDatabaseProvider);
|
|
||||||
return repository.cacheDao.iconCacheSize().watchSingle();
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import 'package:lensai/features/user/domain/providers.dart';
|
||||||
|
import 'package:lensai/features/user/extensions/client_exception.dart';
|
||||||
|
import 'package:pocketbase/pocketbase.dart';
|
||||||
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
|
||||||
|
part 'auth.g.dart';
|
||||||
|
|
||||||
|
class AuthException implements Exception {
|
||||||
|
final String message;
|
||||||
|
|
||||||
|
AuthException(this.message);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Riverpod()
|
||||||
|
class AuthRepository extends _$AuthRepository {
|
||||||
|
late PocketBase _pb;
|
||||||
|
|
||||||
|
Future<RecordAuth> authWithPassword(String user, String password) async {
|
||||||
|
try {
|
||||||
|
return await _pb.collection('users').authWithPassword(user, password);
|
||||||
|
} on ClientException catch (e) {
|
||||||
|
throw AuthException(e.errorMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<RecordModel> createUserWithPassword(
|
||||||
|
String email,
|
||||||
|
String password,
|
||||||
|
) async {
|
||||||
|
try {
|
||||||
|
return await _pb.collection('users').create(
|
||||||
|
body: {
|
||||||
|
"email": email,
|
||||||
|
"password": password,
|
||||||
|
"passwordConfirm": password,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} on ClientException catch (e) {
|
||||||
|
throw AuthException(e.errorMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void build() {
|
||||||
|
_pb = ref.watch(pocketBaseProvider);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'auth.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// RiverpodGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
String _$authRepositoryHash() => r'90ea6f082ef968831763c7f7ee31863f1fe329fe';
|
||||||
|
|
||||||
|
/// See also [AuthRepository].
|
||||||
|
@ProviderFor(AuthRepository)
|
||||||
|
final authRepositoryProvider =
|
||||||
|
AutoDisposeNotifierProvider<AuthRepository, void>.internal(
|
||||||
|
AuthRepository.new,
|
||||||
|
name: r'authRepositoryProvider',
|
||||||
|
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||||
|
? null
|
||||||
|
: _$authRepositoryHash,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
typedef _$AuthRepository = AutoDisposeNotifier<void>;
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import 'package:lensai/extensions/string.dart';
|
||||||
|
import 'package:pocketbase/pocketbase.dart';
|
||||||
|
|
||||||
|
extension ClientExceptionExtension on ClientException {
|
||||||
|
String get errorMessage {
|
||||||
|
var message = "${response["message"] as String? ?? ""}\n\n";
|
||||||
|
final data = response["data"] as Map<String, dynamic>;
|
||||||
|
try {
|
||||||
|
data.forEach((key, value) {
|
||||||
|
final detailedMessage = (value as Map<String, dynamic>)["message"];
|
||||||
|
if (detailedMessage != null) {
|
||||||
|
message += '${key.toCapitalized()}: $detailedMessage\n';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (_) {}
|
||||||
|
try {
|
||||||
|
if (originalError != null) {
|
||||||
|
message += ' $originalError';
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
|
||||||
|
return message.trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import 'package:lensai/features/user/domain/repositories/auth.dart';
|
||||||
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
|
||||||
|
part 'controllers.g.dart';
|
||||||
|
|
||||||
|
@Riverpod()
|
||||||
|
class AuthController extends _$AuthController {
|
||||||
|
@override
|
||||||
|
FutureOr<void> build() {}
|
||||||
|
|
||||||
|
Future<void> authWithPassword(String user, String password) async {
|
||||||
|
state = const AsyncLoading();
|
||||||
|
state = await AsyncValue.guard(
|
||||||
|
() => ref
|
||||||
|
.read(authRepositoryProvider.notifier)
|
||||||
|
.authWithPassword(user, password),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> registerWithPassword(String user, String password) async {
|
||||||
|
state = const AsyncLoading();
|
||||||
|
state = await AsyncValue.guard(() {
|
||||||
|
final repo = ref.read(authRepositoryProvider.notifier);
|
||||||
|
return repo
|
||||||
|
.createUserWithPassword(user, password)
|
||||||
|
.then((_) => repo.authWithPassword(user, password));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void clearState() {
|
||||||
|
state = const AsyncValue.data(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'controllers.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// RiverpodGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
String _$authControllerHash() => r'5b56651948683d669f29946fe02bdb004f436afb';
|
||||||
|
|
||||||
|
/// See also [AuthController].
|
||||||
|
@ProviderFor(AuthController)
|
||||||
|
final authControllerProvider =
|
||||||
|
AutoDisposeAsyncNotifierProvider<AuthController, void>.internal(
|
||||||
|
AuthController.new,
|
||||||
|
name: r'authControllerProvider',
|
||||||
|
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||||
|
? null
|
||||||
|
: _$authControllerHash,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
typedef _$AuthController = AutoDisposeAsyncNotifier<void>;
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user