fluffychat/lib/pangea/analytics_practice/analytics_practice_data_service.dart
ggurdin 117a03089e
5720 vocab practice should have feedback flag (#5761)
* chore: split up analytics activity page widgets into separate files

* started analytics practice refactor

* refactor how UI updates are triggered in analytics practice page

* some fixes
2026-02-20 13:25:21 -05:00

210 lines
6.1 KiB
Dart

import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart';
import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart';
import 'package:fluffychat/pangea/lemmas/lemma_info_repo.dart';
import 'package:fluffychat/pangea/practice_activities/practice_activity_model.dart';
import 'package:fluffychat/pangea/toolbar/message_practice/message_audio_card.dart';
import 'package:fluffychat/widgets/future_loading_dialog.dart';
import 'package:fluffychat/widgets/matrix.dart';
class AnalyticsPracticeChoice {
final String choiceId;
final String choiceText;
final String? choiceEmoji;
const AnalyticsPracticeChoice({
required this.choiceId,
required this.choiceText,
this.choiceEmoji,
});
}
class AnalyticsPracticeDataService {
final Map<String, Map<String, String>> _choiceTexts = {};
final Map<String, Map<String, String?>> _choiceEmojis = {};
final Map<String, PangeaAudioFile> _audioFiles = {};
final Map<String, String> _audioTranslations = {};
void clear() {
_choiceTexts.clear();
_choiceEmojis.clear();
_audioFiles.clear();
_audioTranslations.clear();
}
String _getChoiceText(String key, String choiceId, ConstructTypeEnum type) {
if (type == ConstructTypeEnum.morph) {
return choiceId;
}
if (_choiceTexts.containsKey(key) &&
_choiceTexts[key]!.containsKey(choiceId)) {
return _choiceTexts[key]![choiceId]!;
}
final cId = ConstructIdentifier.fromString(choiceId);
return cId?.lemma ?? choiceId;
}
String? _getChoiceEmoji(String key, String choiceId, ConstructTypeEnum type) {
if (type == ConstructTypeEnum.morph) return null;
return _choiceEmojis[key]?[choiceId];
}
PangeaAudioFile? getAudioFile(MultipleChoicePracticeActivityModel activity) {
if (activity is! VocabAudioPracticeActivityModel) return null;
if (activity.eventId == null) return null;
return _audioFiles[activity.eventId];
}
String? getAudioTranslation(MultipleChoicePracticeActivityModel activity) {
if (activity is! VocabAudioPracticeActivityModel) return null;
if (activity.eventId == null) return null;
final translation = _audioTranslations[activity.eventId];
return translation;
}
List<AnalyticsPracticeChoice> filteredChoices(
MultipleChoicePracticeActivityModel activity,
ConstructTypeEnum type,
) {
final content = activity.multipleChoiceContent;
final choices = content.choices.toList();
final answer = content.answers.first;
final filtered = <AnalyticsPracticeChoice>[];
final seenTexts = <String>{};
for (final id in choices) {
final text = _getChoiceText(activity.storageKey, id, type);
if (seenTexts.contains(text)) {
if (id != answer) {
continue;
}
final index = filtered.indexWhere(
(choice) => choice.choiceText == text,
);
if (index != -1) {
filtered[index] = AnalyticsPracticeChoice(
choiceId: id,
choiceText: text,
choiceEmoji: _getChoiceEmoji(activity.storageKey, id, type),
);
}
continue;
}
seenTexts.add(text);
filtered.add(
AnalyticsPracticeChoice(
choiceId: id,
choiceText: text,
choiceEmoji: _getChoiceEmoji(activity.storageKey, id, type),
),
);
}
return filtered;
}
void _setLemmaInfo(
String requestKey,
Map<String, String> texts,
Map<String, String?> emojis,
) {
_choiceTexts.putIfAbsent(requestKey, () => {});
_choiceEmojis.putIfAbsent(requestKey, () => {});
_choiceTexts[requestKey]!.addAll(texts);
_choiceEmojis[requestKey]!.addAll(emojis);
}
void _setAudioInfo(
String eventId,
PangeaAudioFile audioFile,
String translation,
) {
_audioFiles[eventId] = audioFile;
_audioTranslations[eventId] = translation;
}
Future<void> prefetchActivityInfo(
MultipleChoicePracticeActivityModel activity,
) async {
// Prefetch lemma info for meaning activities before marking ready
if (activity is VocabMeaningPracticeActivityModel) {
final choices = activity.multipleChoiceContent.choices.toList();
await _prefetchLemmaInfo(activity.storageKey, choices);
}
// Prefetch audio for audio activities before marking ready
if (activity is VocabAudioPracticeActivityModel) {
await _prefetchAudioInfo(activity);
}
}
Future<void> _prefetchAudioInfo(
VocabAudioPracticeActivityModel activity,
) async {
final eventId = activity.eventId;
final roomId = activity.roomId;
if (eventId == null || roomId == null) {
throw Exception();
}
final client = MatrixState.pangeaController.matrixState.client;
final room = client.getRoomById(roomId);
if (room == null) {
throw Exception();
}
final event = await room.getEventById(eventId);
if (event == null) {
throw Exception();
}
final pangeaEvent = PangeaMessageEvent(
event: event,
timeline: await room.getTimeline(),
ownMessage: event.senderId == client.userID,
);
// Prefetch the audio file
final audioFile = await pangeaEvent.requestTextToSpeech(
activity.langCode,
MatrixState.pangeaController.userController.voice,
);
if (audioFile.duration == null || audioFile.duration! <= 2000) {
throw "Audio file too short";
}
// Prefetch the translation
final translation = await pangeaEvent.requestRespresentationByL1();
_setAudioInfo(eventId, audioFile, translation);
}
Future<void> _prefetchLemmaInfo(
String requestKey,
List<String> choiceIds,
) async {
final texts = <String, String>{};
final emojis = <String, String?>{};
for (final id in choiceIds) {
final cId = ConstructIdentifier.fromString(id);
if (cId == null) continue;
final res = await cId.getLemmaInfo({});
if (res.isError) {
LemmaInfoRepo.clearCache(cId.lemmaInfoRequest({}));
throw Exception();
}
texts[id] = res.result!.meaning;
emojis[id] = res.result!.emoji.firstOrNull;
}
_setLemmaInfo(requestKey, texts, emojis);
}
}