Files
rely/lib/app/data/relationship_repository_ai_digest.dart
T
2026-05-19 19:17:35 +02:00

174 lines
5.5 KiB
Dart

part of 'relationship_repository.dart';
extension LocalRepositoryAiDigestOperations on LocalRepository {
Future<void> saveAiSuggestionDrafts(List<AiSuggestionDraft> drafts) async {
if (drafts.isEmpty) {
return;
}
final LocalDataState current = _requireState();
final Set<String> existingKeys = current.aiSuggestionDrafts
.map(_aiDraftDedupeKey)
.toSet();
final List<AiSuggestionDraft> uniqueDrafts = drafts
.where((AiSuggestionDraft draft) {
if (!_personExists(current.people, draft.personId)) {
return false;
}
return existingKeys.add(_aiDraftDedupeKey(draft));
})
.toList(growable: false);
if (uniqueDrafts.isEmpty) {
return;
}
await _setState(
current.copyWith(
aiSuggestionDrafts: <AiSuggestionDraft>[
...uniqueDrafts,
...current.aiSuggestionDrafts,
],
),
);
}
Future<void> acceptAiSuggestionDraft(String draftId) async {
final LocalDataState current = _requireState();
final AiSuggestionDraft? draft = current.aiSuggestionDrafts
.where((AiSuggestionDraft item) => item.id == draftId)
.cast<AiSuggestionDraft?>()
.firstOrNull;
if (draft == null || draft.status != AiSuggestionStatus.pending) {
return;
}
if (!_personExists(current.people, draft.personId)) {
await _setAiSuggestionStatus(draftId, AiSuggestionStatus.dismissed);
return;
}
final DateTime now = DateTime.now();
final List<AiSuggestionDraft> drafts = current.aiSuggestionDrafts
.map(
(AiSuggestionDraft item) => item.id == draftId
? item.copyWith(status: AiSuggestionStatus.accepted)
: item,
)
.toList(growable: false);
switch (draft.kind) {
case AiSuggestionKind.giftIdea:
case AiSuggestionKind.eventIdea:
final RelationshipIdea idea = RelationshipIdea(
id: 'i-${_uuid.v4()}',
personId: draft.personId,
type: draft.kind == AiSuggestionKind.giftIdea
? IdeaType.gift
: IdeaType.event,
title: draft.title,
details: _aiIdeaDetails(draft),
createdAt: now,
);
await _setState(
current.copyWith(
aiSuggestionDrafts: drafts,
ideas: <RelationshipIdea>[idea, ...current.ideas],
),
);
await _enqueueChange(
_buildEnvelope(
entityType: _ideaEntityType(idea.type),
entityId: idea.id,
op: ChangeOperation.upsert,
payload: _ideaPayload(idea),
),
);
case AiSuggestionKind.checkIn:
final DashboardTask task = DashboardTask(
id: 't-${_uuid.v4()}',
title: draft.title,
description: _aiDraftDetails(draft),
when: draft.suggestedFor ?? now.add(const Duration(days: 1)),
);
await _setState(
current.copyWith(
aiSuggestionDrafts: drafts,
tasks: <DashboardTask>[task, ...current.tasks],
),
);
case AiSuggestionKind.reminder:
final ReminderRule reminder = ReminderRule(
id: 'r-${_uuid.v4()}',
personId: draft.personId,
title: draft.title,
cadence: ReminderCadence.weekly,
nextAt: draft.suggestedFor ?? now.add(const Duration(days: 7)),
);
await _setState(
current.copyWith(
aiSuggestionDrafts: drafts,
reminders: <ReminderRule>[reminder, ...current.reminders],
),
);
await _enqueueChange(
_buildEnvelope(
entityType: 'reminderRule',
entityId: reminder.id,
op: ChangeOperation.upsert,
payload: _reminderPayload(reminder),
),
);
}
}
Future<void> dismissAiSuggestionDraft(String draftId) {
return _setAiSuggestionStatus(draftId, AiSuggestionStatus.dismissed);
}
Future<void> _setAiSuggestionStatus(
String draftId,
AiSuggestionStatus status,
) async {
final LocalDataState current = _requireState();
final List<AiSuggestionDraft> drafts = current.aiSuggestionDrafts
.map(
(AiSuggestionDraft item) =>
item.id == draftId ? item.copyWith(status: status) : item,
)
.toList(growable: false);
await _setState(current.copyWith(aiSuggestionDrafts: drafts));
}
}
String _aiDraftDedupeKey(AiSuggestionDraft draft) {
return <String>[
draft.personId,
draft.kind.name,
draft.title.trim().toLowerCase(),
draft.details.trim().toLowerCase(),
].join('|');
}
String _aiDraftDetails(AiSuggestionDraft draft) {
final String reason = draft.reason == null || draft.reason!.trim().isEmpty
? ''
: '\n\nWhy: ${draft.reason!.trim()}';
return '${draft.details.trim()}$reason'.trim();
}
String _aiIdeaDetails(AiSuggestionDraft draft) {
final List<String> parts = <String>[
_aiDraftDetails(draft),
if (draft.eventLocation != null && draft.eventLocation!.trim().isNotEmpty)
'Location: ${draft.eventLocation!.trim()}',
if (draft.eventUrl != null && draft.eventUrl!.trim().isNotEmpty)
'Event: ${draft.eventUrl!.trim()}',
if (draft.sourceUrls.isNotEmpty) 'Sources: ${draft.sourceUrls.join(', ')}',
].where((String value) => value.trim().isNotEmpty).toList(growable: false);
return parts.join('\n\n').trim();
}
extension<T> on Iterable<T> {
T? get firstOrNull => isEmpty ? null : first;
}