2026-04-26 10:39:51 +10:00
|
|
|
import 'dart:convert';
|
|
|
|
|
import 'package:crypto/crypto.dart';
|
|
|
|
|
import 'package:drift/drift.dart';
|
|
|
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
|
|
|
import '../database/app_database.dart';
|
2026-04-26 14:37:37 +10:00
|
|
|
import '../llm/prompt_builder.dart';
|
2026-04-26 10:39:51 +10:00
|
|
|
import 'database_provider.dart';
|
2026-04-26 14:37:37 +10:00
|
|
|
import 'llm_provider.dart';
|
2026-04-26 10:39:51 +10:00
|
|
|
|
|
|
|
|
// ── Insight type enum ─────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
enum InsightType { weekly, monthly, query }
|
|
|
|
|
|
2026-04-26 14:37:37 +10:00
|
|
|
// ── Insight notifier — fetches data, builds prompt, calls LLM, caches result ──
|
2026-04-26 10:39:51 +10:00
|
|
|
|
|
|
|
|
class InsightNotifier extends Notifier<AsyncValue<String?>> {
|
|
|
|
|
@override
|
|
|
|
|
AsyncValue<String?> build() => const AsyncData(null);
|
|
|
|
|
|
|
|
|
|
Future<void> generateInsight({
|
2026-04-26 14:37:37 +10:00
|
|
|
InsightType type = InsightType.weekly,
|
|
|
|
|
bool forceRefresh = false,
|
2026-04-26 10:39:51 +10:00
|
|
|
}) async {
|
|
|
|
|
state = const AsyncLoading();
|
|
|
|
|
|
2026-04-26 14:37:37 +10:00
|
|
|
try {
|
|
|
|
|
// Ensure model is initialised before generating
|
|
|
|
|
await ref.read(llmReadyProvider.future);
|
|
|
|
|
if (!ref.mounted) return;
|
2026-04-26 10:39:51 +10:00
|
|
|
|
2026-04-26 14:37:37 +10:00
|
|
|
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);
|
2026-04-26 10:39:51 +10:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void clear() => state = const AsyncData(null);
|
2026-04-26 14:37:37 +10:00
|
|
|
|
|
|
|
|
static String _hashPrompt(String prompt) =>
|
|
|
|
|
sha256.convert(utf8.encode(prompt)).toString();
|
2026-04-26 10:39:51 +10:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
final insightNotifierProvider =
|
|
|
|
|
NotifierProvider.autoDispose<InsightNotifier, AsyncValue<String?>>(
|
|
|
|
|
InsightNotifier.new);
|