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.
119 lines
4.3 KiB
Dart
119 lines
4.3 KiB
Dart
import 'package:drift/drift.dart';
|
|
import '../app_database.dart';
|
|
import '../tables.dart';
|
|
|
|
part 'mood_dao.g.dart';
|
|
|
|
// Joined result — entry + its activities
|
|
class MoodEntryWithActivities {
|
|
final MoodEntry entry;
|
|
final List<Activity> activities;
|
|
final List<Tag> tags;
|
|
MoodEntryWithActivities({
|
|
required this.entry,
|
|
required this.activities,
|
|
required this.tags,
|
|
});
|
|
}
|
|
|
|
@DriftAccessor(
|
|
tables: [MoodEntries, EntryActivities, Activities, EntryTags, Tags])
|
|
class MoodDao extends DatabaseAccessor<AppDatabase> with _$MoodDaoMixin {
|
|
MoodDao(super.db);
|
|
|
|
// ── Writes ──────────────────────────────────────────────────────────────────
|
|
|
|
Future<int> insertEntry(MoodEntriesCompanion entry) =>
|
|
into(moodEntries).insert(entry);
|
|
|
|
Future<void> insertEntryWithActivities({
|
|
required MoodEntriesCompanion entry,
|
|
required List<int> activityIds,
|
|
required List<int> tagIds,
|
|
}) async {
|
|
await transaction(() async {
|
|
final entryId = await into(moodEntries).insert(entry);
|
|
for (final actId in activityIds) {
|
|
await into(entryActivities).insert(
|
|
EntryActivitiesCompanion.insert(entryId: entryId, activityId: actId),
|
|
);
|
|
}
|
|
for (final tagId in tagIds) {
|
|
await into(entryTags).insert(
|
|
EntryTagsCompanion.insert(entryId: entryId, tagId: tagId),
|
|
);
|
|
}
|
|
});
|
|
}
|
|
|
|
Future<void> deleteEntry(int id) =>
|
|
(delete(moodEntries)..where((t) => t.id.equals(id))).go();
|
|
|
|
Future<void> updateEntry(MoodEntriesCompanion entry) =>
|
|
(update(moodEntries)..where((t) => t.id.equals(entry.id.value)))
|
|
.write(entry);
|
|
|
|
// ── Reads ───────────────────────────────────────────────────────────────────
|
|
|
|
Stream<List<MoodEntry>> watchAllEntries() =>
|
|
(select(moodEntries)..orderBy([(t) => OrderingTerm.desc(t.timestamp)]))
|
|
.watch();
|
|
|
|
Stream<List<MoodEntry>> watchEntriesForDay(DateTime day) {
|
|
final start = DateTime(day.year, day.month, day.day);
|
|
final end = start.add(const Duration(days: 1));
|
|
return (select(moodEntries)
|
|
..where((t) => t.timestamp.isBetweenValues(start, end))
|
|
..orderBy([(t) => OrderingTerm.desc(t.timestamp)]))
|
|
.watch();
|
|
}
|
|
|
|
Future<List<MoodEntry>> getEntriesInRange(DateTime start, DateTime end) =>
|
|
(select(moodEntries)
|
|
..where((t) => t.timestamp.isBetweenValues(start, end))
|
|
..orderBy([(t) => OrderingTerm.asc(t.timestamp)]))
|
|
.get();
|
|
|
|
Future<bool> hasEntryToday() async {
|
|
final today = DateTime.now();
|
|
final start = DateTime(today.year, today.month, today.day);
|
|
final end = start.add(const Duration(days: 1));
|
|
return (await (select(moodEntries)
|
|
..where((t) => t.timestamp.isBetweenValues(start, end))
|
|
..limit(1))
|
|
.getSingleOrNull()) != null;
|
|
}
|
|
|
|
// Entries with their linked activities — used by LLM prompt builder
|
|
Future<List<MoodEntryWithActivities>> getEntriesWithActivitiesInRange(
|
|
DateTime start, DateTime end) async {
|
|
final entries = await getEntriesInRange(start, end);
|
|
return Future.wait(entries.map((e) async {
|
|
final links = await (select(entryActivities)
|
|
..where((t) => t.entryId.equals(e.id)))
|
|
.get();
|
|
final actIds = links.map((l) => l.activityId).toList();
|
|
final acts = actIds.isEmpty
|
|
? <Activity>[]
|
|
: await (select(activities)..where((t) => t.id.isIn(actIds))).get();
|
|
return MoodEntryWithActivities(entry: e, activities: acts, tags: []);
|
|
}));
|
|
}
|
|
|
|
// Average mood per day — used by calendar heatmap
|
|
Future<Map<DateTime, double>> getDailyAverages(
|
|
DateTime start, DateTime end) async {
|
|
final entries = await getEntriesInRange(start, end);
|
|
final Map<DateTime, List<int>> grouped = {};
|
|
for (final e in entries) {
|
|
final day =
|
|
DateTime(e.timestamp.year, e.timestamp.month, e.timestamp.day);
|
|
grouped.putIfAbsent(day, () => []).add(e.moodLevel);
|
|
}
|
|
return grouped.map(
|
|
(day, levels) =>
|
|
MapEntry(day, levels.reduce((a, b) => a + b) / levels.length),
|
|
);
|
|
}
|
|
}
|