Skip to content
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 59 additions & 36 deletions lib/services/first_aid_repository.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,31 @@ import 'package:flutter/services.dart' show rootBundle;

import '../database/app_database.dart';

class _FirstAidEntry {
final String id;
final String title;
final String body;
final String tags;
final String source;

// ⚡ Bolt: Cache lowercased strings at parse time to prevent redundant
// string allocations and `.toLowerCase()` calls during high-frequency
// search/autocomplete loops.
final String titleLower;
final String tagsLower;
final String searchHaystackLower;

_FirstAidEntry({
required this.id,
required this.title,
required this.body,
required this.tags,
required this.source,
}) : titleLower = title.toLowerCase(),
tagsLower = tags.toLowerCase(),
searchHaystackLower = '$title $body $tags'.toLowerCase();
}

/// Evidence-based first-aid text bundled in assets and indexed with SQLite FTS5
/// on the **same** PowerSync database as the rest of the app (no separate sqflite DB).
///
Expand All @@ -16,7 +41,7 @@ class FirstAidRepository {

static const String assetPath = 'assets/first_aid/corpus.json';

List<Map<String, dynamic>>? _corpusRows;
List<_FirstAidEntry>? _corpusRows;
bool _ftsReady = false;

static const String medicalDisclaimer =
Expand All @@ -29,7 +54,16 @@ class FirstAidRepository {
if (_corpusRows == null) {
final raw = await rootBundle.loadString(assetPath);
final decoded = jsonDecode(raw) as List<dynamic>;
_corpusRows = decoded.cast<Map<String, dynamic>>();
_corpusRows = decoded.map((e) {
final map = e as Map<String, dynamic>;
return _FirstAidEntry(
id: map['id'] as String? ?? '',
title: map['title'] as String? ?? '',
body: map['body'] as String? ?? '',
tags: map['tags'] as String? ?? '',
source: map['source'] as String? ?? '',
);
}).toList();
}
if (kIsWeb || !isDatabaseInitialized || _ftsReady) {
return;
Expand All @@ -43,25 +77,22 @@ CREATE VIRTUAL TABLE IF NOT EXISTS first_aid_fts USING fts5(
);
''');

final countRows =
await appDb.getAll('SELECT COUNT(*) AS c FROM first_aid_fts');
final countRows = await appDb.getAll(
'SELECT COUNT(*) AS c FROM first_aid_fts',
);
final count = (countRows.first['c'] as num?)?.toInt() ?? 0;

if (count == 0 && _corpusRows!.isNotEmpty) {
for (final row in _corpusRows!) {
await appDb.execute(
'''
INSERT INTO first_aid_fts (title, body, tags, source)
VALUES (?, ?, ?, ?)
''',
[
row['title'] as String? ?? '',
row['body'] as String? ?? '',
row['tags'] as String? ?? '',
row['source'] as String? ?? '',
],
);
}
// ⚡ Bolt: Use executeBatch instead of a loop of execute() calls
// to eliminate the N+1 query problem during DB initialization.
final parameterSets = _corpusRows!.map((row) {
return <Object?>[row.title, row.body, row.tags, row.source];
}).toList();

await appDb.executeBatch('''
INSERT INTO first_aid_fts (title, body, tags, source)
VALUES (?, ?, ?, ?)
''', parameterSets);
}
_ftsReady = true;
}
Expand All @@ -88,10 +119,8 @@ CREATE VIRTUAL TABLE IF NOT EXISTS first_aid_fts USING fts5(

final suggestions = <String>[];
for (final row in _corpusRows!) {
final title = row['title'] as String? ?? '';
final tags = row['tags'] as String? ?? '';
if (title.toLowerCase().contains(q) || tags.toLowerCase().contains(q)) {
suggestions.add(title);
if (row.titleLower.contains(q) || row.tagsLower.contains(q)) {
suggestions.add(row.title);
}
if (suggestions.length >= 6) break;
}
Expand All @@ -105,13 +134,11 @@ CREATE VIRTUAL TABLE IF NOT EXISTS first_aid_fts USING fts5(
}

var bestScore = -1;
Map<String, dynamic>? best;
_FirstAidEntry? best;
for (final row in _corpusRows!) {
final haystack =
'${row['title']} ${row['body']} ${row['tags']}'.toLowerCase();
var score = 0;
for (final t in tokens) {
if (haystack.contains(t)) score += 3;
if (row.searchHaystackLower.contains(t)) score += 3;
}
if (score > bestScore) {
bestScore = score;
Expand All @@ -124,9 +151,9 @@ CREATE VIRTUAL TABLE IF NOT EXISTS first_aid_fts USING fts5(
}

return _formatResult(
title: best['title'] as String? ?? 'Topic',
body: best['body'] as String? ?? '',
source: best['source'] as String? ?? '',
title: best.title,
body: best.body,
source: best.source,
);
}

Expand Down Expand Up @@ -186,19 +213,15 @@ CREATE VIRTUAL TABLE IF NOT EXISTS first_aid_fts USING fts5(
}

String _pickGeneralOrFirst() {
Map<String, dynamic>? row;
_FirstAidEntry? row;
for (final r in _corpusRows!) {
if ((r['id'] as String?) == 'general-road-emergency-india') {
if (r.id == 'general-road-emergency-india') {
row = r;
break;
}
}
row ??= _corpusRows!.first;
return _formatResult(
title: row['title'] as String? ?? '',
body: row['body'] as String? ?? '',
source: row['source'] as String? ?? '',
);
return _formatResult(title: row.title, body: row.body, source: row.source);
}

List<String> _tokenize(String q) {
Expand Down
Loading