feat: implement background LLM pattern analysis on Insights screen
Adds a prompt builder that serialises 7-day mood/activity data, wires InsightNotifier to call the on-device LLM directly (no user interaction), caches results by prompt hash, and renders the analysis as a passive card. Includes loading, empty (<2 entries), and error states with a force-refresh button.
This commit is contained in:
@@ -3,71 +3,87 @@ import 'package:crypto/crypto.dart';
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../database/app_database.dart';
|
||||
import '../llm/prompt_builder.dart';
|
||||
import 'database_provider.dart';
|
||||
import 'llm_provider.dart';
|
||||
|
||||
// ── Insight type enum ─────────────────────────────────────────────────────────
|
||||
|
||||
enum InsightType { weekly, monthly, query }
|
||||
|
||||
// ── Cached insight lookup ─────────────────────────────────────────────────────
|
||||
|
||||
final cachedInsightProvider =
|
||||
FutureProvider.autoDispose.family<LlmInsight?, String>((ref, promptHash) {
|
||||
return ref.watch(insightDaoProvider).getCached(promptHash);
|
||||
});
|
||||
|
||||
// ── Insight notifier — triggers generation and caches results ─────────────────
|
||||
// ── Insight notifier — fetches data, builds prompt, calls LLM, caches result ──
|
||||
|
||||
class InsightNotifier extends Notifier<AsyncValue<String?>> {
|
||||
@override
|
||||
AsyncValue<String?> build() => const AsyncData(null);
|
||||
|
||||
String _hashPrompt(String prompt) =>
|
||||
sha256.convert(utf8.encode(prompt)).toString();
|
||||
|
||||
Future<void> generateInsight({
|
||||
required String prompt,
|
||||
required InsightType type,
|
||||
DateTime? rangeStart,
|
||||
DateTime? rangeEnd,
|
||||
InsightType type = InsightType.weekly,
|
||||
bool forceRefresh = false,
|
||||
}) async {
|
||||
state = const AsyncLoading();
|
||||
|
||||
final hash = _hashPrompt(prompt);
|
||||
final dao = ref.read(insightDaoProvider);
|
||||
try {
|
||||
// Ensure model is initialised before generating
|
||||
await ref.read(llmReadyProvider.future);
|
||||
if (!ref.mounted) return;
|
||||
|
||||
final cached = await dao.getCached(hash);
|
||||
if (!ref.mounted) return;
|
||||
if (cached != null) {
|
||||
state = AsyncData(cached.responseText);
|
||||
return;
|
||||
final now = DateTime.now();
|
||||
final end = DateTime(now.year, now.month, now.day, 23, 59, 59);
|
||||
final start = end.subtract(const Duration(days: 6));
|
||||
|
||||
final dao = ref.read(moodDaoProvider);
|
||||
final entries = await dao.getEntriesWithActivitiesInRange(start, end);
|
||||
if (!ref.mounted) return;
|
||||
|
||||
if (entries.length < 2) {
|
||||
state = const AsyncData(null);
|
||||
return;
|
||||
}
|
||||
|
||||
final prompt = PromptBuilder.buildWeeklyPrompt(entries);
|
||||
final hash = _hashPrompt(prompt);
|
||||
|
||||
if (!forceRefresh) {
|
||||
final cached = await ref.read(insightDaoProvider).getCached(hash);
|
||||
if (!ref.mounted) return;
|
||||
if (cached != null) {
|
||||
state = AsyncData(cached.responseText);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
final llm = ref.read(llmServiceProvider);
|
||||
final buffer = StringBuffer();
|
||||
await for (final token in llm.generateStream(prompt)) {
|
||||
if (!ref.mounted) return;
|
||||
buffer.write(token);
|
||||
state = AsyncData(buffer.toString());
|
||||
}
|
||||
|
||||
if (!ref.mounted) return;
|
||||
final result = buffer.toString().trim();
|
||||
await ref.read(insightDaoProvider).upsertInsight(
|
||||
LlmInsightsCompanion.insert(
|
||||
type: type.name,
|
||||
promptHash: hash,
|
||||
responseText: result,
|
||||
rangeStart: Value(start),
|
||||
rangeEnd: Value(end),
|
||||
),
|
||||
);
|
||||
if (!ref.mounted) return;
|
||||
state = AsyncData(result);
|
||||
} catch (e, st) {
|
||||
if (!ref.mounted) return;
|
||||
state = AsyncError(e, st);
|
||||
}
|
||||
|
||||
// Generation happens in LlmNotifier (see llm_provider.dart)
|
||||
state = const AsyncData(null);
|
||||
}
|
||||
|
||||
Future<void> saveInsight({
|
||||
required String promptHash,
|
||||
required String responseText,
|
||||
required InsightType type,
|
||||
DateTime? rangeStart,
|
||||
DateTime? rangeEnd,
|
||||
}) async {
|
||||
await ref.read(insightDaoProvider).upsertInsight(
|
||||
LlmInsightsCompanion.insert(
|
||||
type: type.name,
|
||||
promptHash: promptHash,
|
||||
responseText: responseText,
|
||||
rangeStart: Value(rangeStart),
|
||||
rangeEnd: Value(rangeEnd),
|
||||
),
|
||||
);
|
||||
if (!ref.mounted) return;
|
||||
state = AsyncData(responseText);
|
||||
}
|
||||
|
||||
void clear() => state = const AsyncData(null);
|
||||
|
||||
static String _hashPrompt(String prompt) =>
|
||||
sha256.convert(utf8.encode(prompt)).toString();
|
||||
}
|
||||
|
||||
final insightNotifierProvider =
|
||||
|
||||
Reference in New Issue
Block a user