refactor: remove tokens and detections from IGC text data model (#2528)

* refactor: remove tokens and detections from IGC text data model

* generated

* refactor: initial work to remove tokens from span_details and IT responses

* refactor: add xp field to construct use class, rewrite function for turning choreo record into construct uses

* refactor: add translation assistance construct use type

* refactor: move analytics feedback to popup above messages

* generated

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
pull/1817/head
ggurdin 6 months ago committed by GitHub
parent 0027ce8536
commit b25676a58d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -4530,7 +4530,8 @@
"updateNow": "Update Now",
"updateLater": "Later",
"constructUseWaDesc": "Used without help",
"constructUseGaDesc": "Grammar mistake",
"constructUseGaDesc": "Grammar assistance",
"constructUseTaDesc": "Translation assistance",
"constructUseUnkDesc": "Unknown",
"constructUseCorITDesc": "Correct in translation",
"constructUseIgnITDesc": "Ignored in translation",
@ -4865,5 +4866,7 @@
"enterSpaceCode": "Enter the Space Code",
"shareSpaceLink": "Share link to space",
"byUsingPangeaChat": "By using Pangea Chat, I agree to the ",
"details": "Details"
"details": "Details",
"newVocab": "New vocab",
"newGrammar": "New grammar concepts"
}

@ -5191,7 +5191,6 @@
"enterCodeToJoin": "Ingrese el código para unirse",
"updateLater": "Más tarde",
"constructUseWaDesc": "Usado sin ayuda",
"constructUseGaDesc": "Error gramatical",
"constructUseUnkDesc": "Desconocido",
"constructUseCorITDesc": "Correcto en la traducción",
"constructUseIgnITDesc": "Ignorado en la traducción",

@ -3377,7 +3377,6 @@
"updateNow": "Cập nhật ngay",
"updateLater": "Để sau",
"constructUseWaDesc": "Dùng không cần trợ giúp",
"constructUseGaDesc": "Có lỗi ngữ pháp",
"constructUseUnkDesc": "Không xác định",
"constructUseCorITDesc": "Đúng trong dịch",
"constructUseIgnITDesc": "Bỏ qua trong dịch",

@ -27,6 +27,7 @@ import 'package:fluffychat/pages/chat/chat_view.dart';
import 'package:fluffychat/pages/chat/event_info_dialog.dart';
import 'package:fluffychat/pages/chat/recording_dialog.dart';
import 'package:fluffychat/pages/chat_details/chat_details.dart';
import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart';
import 'package:fluffychat/pangea/analytics_misc/constructs_model.dart';
import 'package:fluffychat/pangea/analytics_misc/gain_points_animation.dart';
import 'package:fluffychat/pangea/analytics_misc/level_up.dart';
@ -36,6 +37,7 @@ import 'package:fluffychat/pangea/chat/widgets/event_too_large_dialog.dart';
import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart';
import 'package:fluffychat/pangea/choreographer/enums/edit_type.dart';
import 'package:fluffychat/pangea/choreographer/models/choreo_record.dart';
import 'package:fluffychat/pangea/choreographer/widgets/igc/message_analytics_feedback.dart';
import 'package:fluffychat/pangea/choreographer/widgets/igc/pangea_text_controller.dart';
import 'package:fluffychat/pangea/common/controllers/pangea_controller.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
@ -819,18 +821,46 @@ class ChatController extends State<ChatPageWithRoom>
);
if (msgEventId != null && originalSent != null && tokensSent != null) {
final List<OneConstructUse> constructs = [
...originalSent.vocabAndMorphUses(
choreo: choreo,
tokens: tokensSent.tokens,
metadata: metadata,
),
];
final newGrammarConstructs =
pangeaController.getAnalytics.newConstructCount(
constructs,
ConstructTypeEnum.morph,
);
final newVocabConstructs =
pangeaController.getAnalytics.newConstructCount(
constructs,
ConstructTypeEnum.vocab,
);
OverlayUtil.showOverlay(
overlayKey: "msg_analytics_feedback_$msgEventId",
followerAnchor: Alignment.bottomRight,
targetAnchor: Alignment.topRight,
context: context,
child: MessageAnalyticsFeedback(
overlayId: "msg_analytics_feedback_$msgEventId",
newGrammarConstructs: newGrammarConstructs,
newVocabConstructs: newVocabConstructs,
),
transformTargetId: msgEventId,
ignorePointer: true,
);
pangeaController.putAnalytics.setState(
AnalyticsStream(
eventId: msgEventId,
targetID: msgEventId,
roomId: room.id,
constructs: [
...originalSent.vocabAndMorphUses(
choreo: choreo,
tokens: tokensSent.tokens,
metadata: metadata,
),
],
constructs: constructs,
),
);
}

@ -26,13 +26,13 @@ class LemmaUsageDots extends StatelessWidget {
List<bool> sortedUses(LearningSkillsEnum category) {
final List<bool> useList = [];
for (final OneConstructUse use in construct.uses) {
if (use.useType.pointValue == 0) {
if (use.xp == 0) {
continue;
}
// If the use type matches the given category, save to list
// Usage with positive XP is saved as true, else false
if (category == use.useType.skillsEnumType) {
useList.add(use.useType.pointValue > 0);
useList.add(use.xp > 0);
}
}
return useList;

@ -29,7 +29,7 @@ class LemmaUseExampleMessages extends StatelessWidget {
if (use.useType.skillsEnumType != LearningSkillsEnum.writing ||
use.metadata.eventId == null ||
use.form == null ||
use.pointValue <= 0) {
use.xp <= 0) {
continue;
}

@ -124,7 +124,7 @@ class AnalyticsSummaryModel {
use.useType != ConstructUseTypeEnum.wa &&
use.useType != ConstructUseTypeEnum.ga &&
use.useType != ConstructUseTypeEnum.unk &&
use.pointValue != 0,
use.xp != 0,
percent: 0.8,
context: context,
);

@ -354,7 +354,7 @@ class LemmasToUsesWrapper {
final uses = entry.value.toList();
for (final use in uses) {
use.pointValue > 0 ? correctUses.add(use) : incorrectUses.add(use);
use.xp > 0 ? correctUses.add(use) : incorrectUses.add(use);
}
final totalUses = correctUses.length + incorrectUses.length;

@ -24,7 +24,7 @@ class ConstructUses {
int get points {
return uses.fold<int>(
0,
(total, use) => total + use.useType.pointValue,
(total, use) => total + use.xp,
);
}
@ -46,8 +46,8 @@ class ConstructUses {
return _category!.toLowerCase();
}
bool get hasCorrectUse => uses.any((use) => use.pointValue > 0);
bool get hasIncorrectUse => uses.any((use) => use.pointValue < 0);
bool get hasCorrectUse => uses.any((use) => use.xp > 0);
bool get hasIncorrectUse => uses.any((use) => use.xp < 0);
ConstructIdentifier get id => ConstructIdentifier(
lemma: lemma,

@ -10,10 +10,12 @@ enum ConstructUseTypeEnum {
/// produced in chat by user, igc was run, and we've judged it to be a correct use
wa,
/// produced in chat by user, igc was run, and we've judged it to be a incorrect use
/// Note: if the IGC match is ignored, this is not counted as an incorrect use
/// produced during IGC
ga,
/// produced during IT
ta,
/// produced in chat by user and igc was not run
unk,
@ -78,6 +80,8 @@ extension ConstructUseTypeExtension on ConstructUseTypeEnum {
return L10n.of(context).constructUseWaDesc;
case ConstructUseTypeEnum.ga:
return L10n.of(context).constructUseGaDesc;
case ConstructUseTypeEnum.ta:
return L10n.of(context).constructUseTaDesc;
case ConstructUseTypeEnum.unk:
return L10n.of(context).constructUseUnkDesc;
case ConstructUseTypeEnum.corIt:
@ -149,6 +153,7 @@ extension ConstructUseTypeExtension on ConstructUseTypeEnum {
case ConstructUseTypeEnum.incIGC:
case ConstructUseTypeEnum.corIGC:
case ConstructUseTypeEnum.ga:
case ConstructUseTypeEnum.ta:
return Icons.spellcheck;
case ConstructUseTypeEnum.corPA:
case ConstructUseTypeEnum.incPA:
@ -223,9 +228,10 @@ extension ConstructUseTypeExtension on ConstructUseTypeEnum {
case ConstructUseTypeEnum.ignMM:
case ConstructUseTypeEnum.unk:
case ConstructUseTypeEnum.nan:
case ConstructUseTypeEnum.ga:
case ConstructUseTypeEnum.ta:
return 0;
case ConstructUseTypeEnum.ga:
case ConstructUseTypeEnum.incMM:
case ConstructUseTypeEnum.incIt:
case ConstructUseTypeEnum.incIGC:
@ -244,6 +250,7 @@ extension ConstructUseTypeExtension on ConstructUseTypeEnum {
switch (this) {
case ConstructUseTypeEnum.wa:
case ConstructUseTypeEnum.ga:
case ConstructUseTypeEnum.ta:
case ConstructUseTypeEnum.unk:
case ConstructUseTypeEnum.corIt:
case ConstructUseTypeEnum.ignIt:
@ -283,6 +290,7 @@ extension ConstructUseTypeExtension on ConstructUseTypeEnum {
switch (this) {
case ConstructUseTypeEnum.wa:
case ConstructUseTypeEnum.ga:
case ConstructUseTypeEnum.ta:
case ConstructUseTypeEnum.unk:
case ConstructUseTypeEnum.corIt:
case ConstructUseTypeEnum.ignIt:
@ -323,6 +331,7 @@ extension ConstructUseTypeExtension on ConstructUseTypeEnum {
switch (this) {
case ConstructUseTypeEnum.wa:
case ConstructUseTypeEnum.ga:
case ConstructUseTypeEnum.ta:
case ConstructUseTypeEnum.unk:
case ConstructUseTypeEnum.pvm:
return AnalyticsSummaryEnum.numWordsTyped;

@ -90,6 +90,8 @@ class OneConstructUse {
String? id;
ConstructUseMetaData metadata;
int xp;
OneConstructUse({
required this.useType,
required this.lemma,
@ -97,6 +99,7 @@ class OneConstructUse {
required this.metadata,
required category,
required this.form,
required this.xp,
this.id,
}) {
if (category is MorphFeaturesEnum) {
@ -117,8 +120,10 @@ class OneConstructUse {
? ConstructTypeUtil.fromString(json['constructType'])
: ConstructTypeEnum.vocab;
final useType = ConstructUseTypeUtil.fromString(json['useType']);
return OneConstructUse(
useType: ConstructUseTypeUtil.fromString(json['useType']),
useType: useType,
lemma: json['lemma'],
form: json['form'],
category: getCategory(json, constructType),
@ -129,6 +134,7 @@ class OneConstructUse {
roomId: json['chatId'],
timeStamp: DateTime.parse(json['timeStamp']),
),
xp: json['xp'] ?? useType.pointValue,
);
}
@ -142,6 +148,7 @@ class OneConstructUse {
'constructType': constructType.string,
'categories': category,
'id': id,
'xp': xp,
};
String get category {
@ -196,11 +203,9 @@ class OneConstructUse {
return room.getEventById(metadata.eventId!);
}
int get pointValue => useType.pointValue;
Color pointValueColor(BuildContext context) {
if (pointValue == 0) return Theme.of(context).colorScheme.primary;
return pointValue > 0 ? AppConfig.gold : Colors.red;
if (xp == 0) return Theme.of(context).colorScheme.primary;
return xp > 0 ? AppConfig.gold : Colors.red;
}
ConstructIdentifier get identifier => ConstructIdentifier(

@ -171,7 +171,7 @@ class GetAnalyticsController extends BaseController {
_updateAnalyticsStream(
points: analyticsUpdate.newConstructs.fold<int>(
0,
(previousValue, element) => previousValue + element.pointValue,
(previousValue, element) => previousValue + element.xp,
),
targetID: analyticsUpdate.targetID,
newConstructs: [...newUnlockedMorphs, ...newUnlockedVocab],
@ -398,6 +398,29 @@ class GetAnalyticsController extends BaseController {
_cache.add(entry);
}
int newConstructCount(
List<OneConstructUse> newConstructs,
ConstructTypeEnum type,
) {
final uses = newConstructs.where((c) => c.constructType == type);
final Map<ConstructIdentifier, int> constructPoints = {};
for (final use in uses) {
constructPoints[use.identifier] ??= 0;
constructPoints[use.identifier] =
constructPoints[use.identifier]! + use.xp;
}
int newConstructCount = 0;
for (final entry in constructPoints.entries) {
final construct = constructListModel.getConstructUses(entry.key);
if (construct == null || construct.points == entry.value) {
newConstructCount++;
}
}
return newConstructCount;
}
// Future<GenerateConstructSummaryResult?>
// _generateLevelUpAnalyticsAndSaveToStateEvent(
// final int lowerLevel,

@ -12,7 +12,6 @@ import 'package:fluffychat/pangea/common/constants/local.key.dart';
import 'package:fluffychat/pangea/common/controllers/base_controller.dart';
import 'package:fluffychat/pangea/common/controllers/pangea_controller.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
import 'package:fluffychat/pangea/learning_settings/models/language_model.dart';
import 'package:fluffychat/widgets/matrix.dart';
@ -122,17 +121,17 @@ class PutAnalyticsController extends BaseController<AnalyticsStream> {
final String? eventID = data.eventId;
final String? roomID = data.roomId;
List<OneConstructUse> constructs = [];
if (roomID != null) {
constructs = _getDraftUses(roomID);
}
final List<OneConstructUse> constructs = [];
// if (roomID != null) {
// constructs = _getDraftUses(roomID);
// }
constructs.addAll(data.constructs);
if (kDebugMode) {
for (final use in constructs) {
debugPrint(
"_onNewAnalyticsData filtered use: ${use.constructType.string} ${use.useType.string} ${use.lemma} ${use.useType.pointValue}",
"_onNewAnalyticsData filtered use: ${use.constructType.string} ${use.useType.string} ${use.lemma} ${use.xp}",
);
}
}
@ -161,87 +160,87 @@ class PutAnalyticsController extends BaseController<AnalyticsStream> {
});
}
void addDraftUses(
List<PangeaToken> tokens,
String roomID,
ConstructUseTypeEnum useType, {
String? targetID,
}) {
final metadata = ConstructUseMetaData(
roomId: roomID,
timeStamp: DateTime.now(),
);
// we only save those with saveVocab == true
final tokensToSave =
tokens.where((token) => token.lemma.saveVocab).toList();
// get all our vocab constructs
final uses = tokensToSave
.map(
(token) => OneConstructUse(
useType: useType,
lemma: token.lemma.text,
form: token.text.content,
constructType: ConstructTypeEnum.vocab,
metadata: metadata,
category: token.pos,
),
)
.toList();
// get all our grammar constructs
for (final token in tokensToSave) {
uses.add(
OneConstructUse(
useType: useType,
lemma: token.pos,
form: token.text.content,
category: "POS",
constructType: ConstructTypeEnum.morph,
metadata: metadata,
),
);
for (final entry in token.morph.entries) {
uses.add(
OneConstructUse(
useType: useType,
lemma: entry.value,
form: token.text.content,
category: entry.key,
constructType: ConstructTypeEnum.morph,
metadata: metadata,
),
);
}
}
if (kDebugMode) {
for (final use in uses) {
debugPrint(
"Draft use: ${use.constructType.string} ${use.useType.string} ${use.lemma} ${use.useType.pointValue}",
);
}
}
final level = _pangeaController.getAnalytics.constructListModel.level;
// the list 'uses' gets altered in the _addLocalMessage method,
// so copy it here to that the list of new uses is accurate
final List<OneConstructUse> newUses = List.from(uses);
_addLocalMessage('draft$roomID', uses).then(
(_) => _decideWhetherToUpdateAnalyticsRoom(
level,
targetID,
newUses,
),
);
}
List<OneConstructUse> _getDraftUses(String roomID) {
final currentCache = _pangeaController.getAnalytics.messagesSinceUpdate;
return currentCache['draft$roomID'] ?? [];
}
// void addDraftUses(
// List<PangeaToken> tokens,
// String roomID,
// ConstructUseTypeEnum useType, {
// String? targetID,
// }) {
// final metadata = ConstructUseMetaData(
// roomId: roomID,
// timeStamp: DateTime.now(),
// );
// // we only save those with saveVocab == true
// final tokensToSave =
// tokens.where((token) => token.lemma.saveVocab).toList();
// // get all our vocab constructs
// final uses = tokensToSave
// .map(
// (token) => OneConstructUse(
// useType: useType,
// lemma: token.lemma.text,
// form: token.text.content,
// constructType: ConstructTypeEnum.vocab,
// metadata: metadata,
// category: token.pos,
// ),
// )
// .toList();
// // get all our grammar constructs
// for (final token in tokensToSave) {
// uses.add(
// OneConstructUse(
// useType: useType,
// lemma: token.pos,
// form: token.text.content,
// category: "POS",
// constructType: ConstructTypeEnum.morph,
// metadata: metadata,
// ),
// );
// for (final entry in token.morph.entries) {
// uses.add(
// OneConstructUse(
// useType: useType,
// lemma: entry.value,
// form: token.text.content,
// category: entry.key,
// constructType: ConstructTypeEnum.morph,
// metadata: metadata,
// ),
// );
// }
// }
// if (kDebugMode) {
// for (final use in uses) {
// debugPrint(
// "Draft use: ${use.constructType.string} ${use.useType.string} ${use.lemma} ${use.useType.pointValue}",
// );
// }
// }
// final level = _pangeaController.getAnalytics.constructListModel.level;
// // the list 'uses' gets altered in the _addLocalMessage method,
// // so copy it here to that the list of new uses is accurate
// final List<OneConstructUse> newUses = List.from(uses);
// _addLocalMessage('draft$roomID', uses).then(
// (_) => _decideWhetherToUpdateAnalyticsRoom(
// level,
// targetID,
// newUses,
// ),
// );
// }
// List<OneConstructUse> _getDraftUses(String roomID) {
// final currentCache = _pangeaController.getAnalytics.messagesSinceUpdate;
// return currentCache['draft$roomID'] ?? [];
// }
void _clearDraftUses(String roomID) {
final currentCache = _pangeaController.getAnalytics.messagesSinceUpdate;

@ -155,7 +155,7 @@ class LevelBarPopup extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
children: [
Text(
"${use.pointValue > 0 ? '+' : ''}${use.pointValue}",
"${use.xp > 0 ? '+' : ''}${use.xp}",
style: TextStyle(
fontWeight: FontWeight.w900,
fontSize: 14,

@ -1,219 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:http/http.dart' as http;
import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart';
import 'package:fluffychat/pangea/analytics_misc/construct_use_type_enum.dart';
import 'package:fluffychat/pangea/analytics_misc/constructs_model.dart';
import 'package:fluffychat/pangea/choreographer/constants/choreo_constants.dart';
import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart';
import 'package:fluffychat/pangea/choreographer/controllers/error_service.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
import 'package:fluffychat/widgets/matrix.dart';
import '../repo/similarity_repo.dart';
class AlternativeTranslator {
final Choreographer choreographer;
bool showAlternativeTranslations = false;
bool loadingAlternativeTranslations = false;
bool showTranslationFeedback = false;
String? userTranslation;
FeedbackKey? translationFeedbackKey;
List<String> translations = [];
SimilartyResponseModel? similarityResponse;
AlternativeTranslator(this.choreographer);
void clear() {
userTranslation = null;
showAlternativeTranslations = false;
loadingAlternativeTranslations = false;
showTranslationFeedback = false;
translationFeedbackKey = null;
translations = [];
similarityResponse = null;
}
double get _percentCorrectChoices {
final totalSteps = choreographer.choreoRecord.itSteps.length;
if (totalSteps == 0) return 0.0;
final int correctFirstAttempts = choreographer.itController.completedITSteps
.where(
(step) => !step.continuances.any(
(c) =>
c.level != ChoreoConstants.levelThresholdForGreen &&
c.wasClicked,
),
)
.length;
final double percentage = (correctFirstAttempts / totalSteps) * 100;
return percentage;
}
int get starRating {
final double percent = _percentCorrectChoices;
if (percent == 100) return 5;
if (percent >= 80) return 4;
if (percent >= 60) return 3;
if (percent >= 40) return 2;
if (percent > 0) return 1;
return 0;
}
Future<void> setTranslationFeedback() async {
try {
choreographer.startLoading();
translationFeedbackKey = FeedbackKey.loadingPleaseWait;
showTranslationFeedback = true;
userTranslation = choreographer.currentText;
final double percentCorrect = _percentCorrectChoices;
// Set feedback based on percentage
if (percentCorrect == 100) {
translationFeedbackKey = FeedbackKey.allCorrect;
} else if (percentCorrect >= 80) {
translationFeedbackKey = FeedbackKey.newWayAllGood;
} else {
translationFeedbackKey = FeedbackKey.othersAreBetter;
}
} catch (err, stack) {
if (err is! http.Response) {
ErrorHandler.logError(
e: err,
s: stack,
data: {
"sourceText": choreographer.itController.sourceText,
"currentText": choreographer.currentText,
"userL1": choreographer.l1LangCode,
"userL2": choreographer.l2LangCode,
"goldRouteTranslation":
choreographer.itController.goldRouteTracker.fullTranslation,
},
);
}
choreographer.errorService.setError(
ChoreoError(type: ChoreoErrorType.unknown, raw: err),
);
} finally {
choreographer.stopLoading();
}
}
List<OneConstructUse> get _itStepConstructs {
final metadata = ConstructUseMetaData(
roomId: choreographer.roomId,
timeStamp: DateTime.now(),
);
final List<OneConstructUse> constructs = [];
for (final step in choreographer.choreoRecord.itSteps) {
for (final continuance in step.continuances) {
final ConstructUseTypeEnum useType = continuance.wasClicked &&
continuance.level == ChoreoConstants.levelThresholdForGreen
? ConstructUseTypeEnum.corIt
: continuance.wasClicked
? ConstructUseTypeEnum.incIt
: ConstructUseTypeEnum.ignIt;
final tokens = continuance.tokens.where((t) => t.lemma.saveVocab);
constructs.addAll(
tokens.map(
(token) => OneConstructUse(
useType: useType,
lemma: token.lemma.text,
constructType: ConstructTypeEnum.vocab,
metadata: metadata,
category: token.pos,
form: token.text.content,
),
),
);
for (final token in tokens) {
constructs.add(
OneConstructUse(
useType: useType,
lemma: token.pos,
form: token.text.content,
category: "POS",
constructType: ConstructTypeEnum.morph,
metadata: metadata,
),
);
for (final entry in token.morph.entries) {
constructs.add(
OneConstructUse(
useType: useType,
lemma: entry.value,
form: token.text.content,
category: entry.key,
constructType: ConstructTypeEnum.morph,
metadata: metadata,
),
);
}
}
}
}
return constructs;
}
int countNewConstructs(ConstructTypeEnum type) {
final vocabUses = _itStepConstructs.where((c) => c.constructType == type);
final Map<ConstructIdentifier, int> constructPoints = {};
for (final use in vocabUses) {
constructPoints[use.identifier] ??= 0;
constructPoints[use.identifier] =
constructPoints[use.identifier]! + use.pointValue;
}
final constructListModel =
MatrixState.pangeaController.getAnalytics.constructListModel;
int newConstructCount = 0;
for (final entry in constructPoints.entries) {
final construct = constructListModel.getConstructUses(entry.key);
if (construct?.points == entry.value) {
newConstructCount++;
}
}
return newConstructCount;
}
String getDefaultFeedback(BuildContext context) {
final l10n = L10n.of(context);
switch (translationFeedbackKey) {
case FeedbackKey.allCorrect:
return l10n.perfectTranslation;
case FeedbackKey.newWayAllGood:
return l10n.greatJobTranslation;
case FeedbackKey.othersAreBetter:
if (_percentCorrectChoices >= 60) {
return l10n.goodJobTranslation;
}
if (_percentCorrectChoices >= 40) {
return l10n.makingProgress;
}
return l10n.keepPracticing;
case FeedbackKey.loadingPleaseWait:
return l10n.letMeThink;
case FeedbackKey.allDone:
return l10n.allDone;
default:
return l10n.loadingPleaseWait;
}
}
}
enum FeedbackKey {
allCorrect,
newWayAllGood,
othersAreBetter,
loadingPleaseWait,
allDone,
}
extension FeedbackKeyExtension on FeedbackKey {}

@ -7,13 +7,13 @@ import 'package:flutter/material.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:fluffychat/pages/chat/chat.dart';
import 'package:fluffychat/pangea/choreographer/controllers/alternative_translator.dart';
import 'package:fluffychat/pangea/choreographer/controllers/igc_controller.dart';
import 'package:fluffychat/pangea/choreographer/enums/assistance_state_enum.dart';
import 'package:fluffychat/pangea/choreographer/enums/edit_type.dart';
import 'package:fluffychat/pangea/choreographer/models/choreo_record.dart';
import 'package:fluffychat/pangea/choreographer/models/it_step.dart';
import 'package:fluffychat/pangea/choreographer/models/pangea_match_model.dart';
import 'package:fluffychat/pangea/choreographer/repo/language_detection_repo.dart';
import 'package:fluffychat/pangea/choreographer/utils/input_paste_listener.dart';
import 'package:fluffychat/pangea/choreographer/widgets/igc/pangea_text_controller.dart';
import 'package:fluffychat/pangea/choreographer/widgets/igc/paywall_card.dart';
@ -24,6 +24,7 @@ import 'package:fluffychat/pangea/common/utils/overlay.dart';
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
import 'package:fluffychat/pangea/events/models/representation_content_model.dart';
import 'package:fluffychat/pangea/events/models/tokens_event_content_model.dart';
import 'package:fluffychat/pangea/events/repo/token_api_models.dart';
import 'package:fluffychat/pangea/learning_settings/constants/language_constants.dart';
import 'package:fluffychat/pangea/learning_settings/models/language_model.dart';
import 'package:fluffychat/pangea/spaces/models/space_model.dart';
@ -41,7 +42,6 @@ class Choreographer {
late PangeaTextController _textController;
late ITController itController;
late IgcController igc;
late AlternativeTranslator altTranslator;
late ErrorService errorService;
late TtsController tts;
@ -71,7 +71,6 @@ class Choreographer {
itController = ITController(this);
igc = IgcController(this);
errorService = ErrorService(this);
altTranslator = AlternativeTranslator(this);
_textController.addListener(_onChangeListener);
_trialStream = pangeaController
.subscriptionController.trialActivationStream.stream
@ -144,6 +143,7 @@ class Choreographer {
return;
}
chatController.sendFakeMessage();
final PangeaRepresentation? originalWritten =
choreoRecord.includedIT && itController.sourceText != null
? PangeaRepresentation(
@ -154,59 +154,43 @@ class Choreographer {
)
: null;
// we've got a rather elaborate method of updating tokens after matches are accepted
// so we need to check if the reconstructed text matches the current text
// if not, let's get the tokens again and log an error
if (igc.igcTextData?.tokens != null &&
PangeaToken.reconstructText(igc.igcTextData!.tokens).trim() !=
currentText.trim()) {
if (kDebugMode) {
PangeaToken.reconstructText(
igc.igcTextData!.tokens,
debugWalkThrough: true,
);
}
ErrorHandler.logError(
m: "reconstructed text not working",
s: StackTrace.current,
data: {
"igcTextData": igc.igcTextData?.toJson(),
"choreoRecord": choreoRecord.toJson(),
},
);
await igc.getIGCTextData(onlyTokensAndLanguageDetection: true);
}
// TODO - why does both it and igc need to be enabled for choreo to be applicable?
// final ChoreoRecord? applicableChoreo =
// isITandIGCEnabled && igc.igcTextData != null ? choreoRecord : null;
// if tokens OR language detection are not available, we should get them
// notes
// 1) we probably need to move this to after we clear the input field
// or the user could experience some lag here.
// 2) that this call is being made after we've determined if we have an applicable choreo in order to
// say whether correction was run on the message. we may eventually want
// to edit the useType after
if ((l2Lang != null && l1Lang != null) &&
(igc.igcTextData?.tokens == null ||
igc.igcTextData?.detectedLanguage == null)) {
await igc.getIGCTextData(onlyTokensAndLanguageDetection: true);
}
final detectionResp = await LanguageDetectionRepo.get(
MatrixState.pangeaController.userController.accessToken,
request: LanguageDetectionRequest(
text: currentText,
senderl1: l1LangCode,
senderl2: l2LangCode,
),
);
final detections = detectionResp.detections;
final detectedLanguage =
detections.firstOrNull?.langCode ?? LanguageKeys.unknownLanguage;
final PangeaRepresentation originalSent = PangeaRepresentation(
langCode:
igc.igcTextData?.detectedLanguage ?? LanguageKeys.unknownLanguage,
langCode: detectedLanguage,
text: currentText,
originalSent: true,
originalWritten: originalWritten == null,
);
final PangeaMessageTokens? tokensSent = igc.igcTextData?.tokens != null
List<PangeaToken>? res;
if (l1LangCode != null && l2LangCode != null) {
res = await pangeaController.messageData.getTokens(
repEventId: null,
room: chatController.room,
req: TokensRequestModel(
fullText: currentText,
langCode: detectedLanguage,
senderL1: l1LangCode!,
senderL2: l2LangCode!,
),
);
}
final PangeaMessageTokens? tokensSent = res != null
? PangeaMessageTokens(
tokens: igc.igcTextData!.tokens,
detections: igc.igcTextData!.detections.detections,
tokens: res,
detections: detections,
)
: null;
@ -235,7 +219,7 @@ class Choreographer {
}
choreoMode = ChoreoMode.it;
itController.initializeIT(
ITStartData(_textController.text, igc.igcTextData?.detectedLanguage),
ITStartData(_textController.text, null),
);
itMatch.status = PangeaMatchStatus.accepted;
@ -284,9 +268,7 @@ class Choreographer {
() => getLanguageHelp(),
);
} else {
getLanguageHelp(
onlyTokensAndLanguageDetection: ChoreoMode.it == choreoMode,
);
getLanguageHelp();
}
//Note: we don't set the keyboard type on each keyboard stroke so this is how we default to
@ -300,7 +282,6 @@ class Choreographer {
/// or if autoIGC is not enabled and the user has not manually requested it.
/// [onlyTokensAndLanguageDetection] will
Future<void> getLanguageHelp({
bool onlyTokensAndLanguageDetection = false,
bool manual = false,
}) async {
try {
@ -320,17 +301,13 @@ class Choreographer {
// if getting language assistance after finishing IT,
// reset the itController
if (choreoMode == ChoreoMode.it &&
itController.isTranslationDone &&
!onlyTokensAndLanguageDetection) {
if (choreoMode == ChoreoMode.it && itController.isTranslationDone) {
itController.clear();
}
await (isRunningIT
? itController.getTranslationData(_useCustomInput)
: igc.getIGCTextData(
onlyTokensAndLanguageDetection: onlyTokensAndLanguageDetection,
));
: igc.getIGCTextData());
} catch (err, stack) {
ErrorHandler.logError(
e: err,
@ -343,7 +320,6 @@ class Choreographer {
"itEnabled": itEnabled,
"isAutoIGCEnabled": isAutoIGCEnabled,
"isTranslationDone": itController.isTranslationDone,
"onlyTokensAndLanguageDetection": onlyTokensAndLanguageDetection,
"useCustomInput": _useCustomInput,
},
);
@ -365,18 +341,6 @@ class Choreographer {
giveInputFocus();
}
void onPredictorSelect(String text) {
//TODO - add some kind of record of this
// choreoRecord.addRecord(_textController.text, step: step);
// TODO - probably give it a different type of edit type
_textController.setSystemText(
"${_textController.text} $text",
EditType.other,
);
giveInputFocus();
}
Future<void> onReplacementSelect({
required int matchIndex,
required int choiceIndex,
@ -546,21 +510,6 @@ class Choreographer {
}
}
void onSelectAlternativeTranslation(String translation) {
// PTODO - add some kind of record of this
// choreoRecord.addRecord(_textController.text, match);
_textController.setSystemText(
translation,
EditType.alternativeTranslation,
);
altTranslator.clear();
altTranslator.translationFeedbackKey = FeedbackKey.allDone;
altTranslator.showTranslationFeedback = true;
giveInputFocus();
setState();
}
void giveInputFocus() {
Future.delayed(Duration.zero, () {
chatController.inputFocus.requestFocus();
@ -586,22 +535,6 @@ class Choreographer {
_resetDebounceTimer();
}
void onMatchError({int? cursorOffset}) {
if (cursorOffset == null) {
igc.igcTextData?.matches.clear();
} else {
final int? matchIndex = igc.igcTextData?.getTopMatchIndexForOffset(
cursorOffset,
);
matchIndex == -1 || matchIndex == null
? igc.igcTextData?.matches.clear()
: igc.igcTextData?.matches.removeAt(matchIndex);
}
setState();
giveInputFocus();
}
Future<void> onPaste(value) async {
choreoRecord.pastedStrings.add(value);
}
@ -698,33 +631,12 @@ class Choreographer {
chatController.room,
);
// bool get itAutoPlayEnabled {
// return pangeaController.userController.profile.userSettings.itAutoPlay;
// }
bool get definitionsEnabled =>
pangeaController.permissionsController.isToolEnabled(
ToolSetting.definitions,
chatController.room,
);
bool get immersionMode =>
pangeaController.permissionsController.isToolEnabled(
ToolSetting.immersionMode,
chatController.room,
);
// bool get translationEnabled =>
// pangeaController.permissionsController.isToolEnabled(
// ToolSetting.translations,
// chatController.room,
// );
bool get isITandIGCEnabled =>
pangeaController.permissionsController.isWritingAssistanceEnabled(
chatController.room,
);
bool get isAutoIGCEnabled =>
pangeaController.permissionsController.isToolEnabled(
ToolSetting.autoIGC,

@ -72,32 +72,21 @@ class IgcController {
});
}
Future<void> getIGCTextData({
required bool onlyTokensAndLanguageDetection,
}) async {
Future<void> getIGCTextData() async {
try {
if (choreographer.currentText.isEmpty) return clear();
// if tokenizing on message send, tokenization might take a while
// so add a fake event to the timeline to visually indicate that the message is being sent
if (onlyTokensAndLanguageDetection &&
choreographer.choreoMode != ChoreoMode.it) {
choreographer.chatController.sendFakeMessage();
}
debugPrint('getIGCTextData called with ${choreographer.currentText}');
debugPrint(
'getIGCTextData called with tokensOnly = $onlyTokensAndLanguageDetection',
);
final IGCRequestBody reqBody = IGCRequestBody(
fullText: choreographer.currentText,
userId: choreographer.pangeaController.userController.userId!,
userL1: choreographer.l1LangCode!,
userL2: choreographer.l2LangCode!,
enableIGC: choreographer.igcEnabled && !onlyTokensAndLanguageDetection,
enableIT: choreographer.itEnabled && !onlyTokensAndLanguageDetection,
prevMessages: prevMessages(),
enableIGC: choreographer.igcEnabled &&
choreographer.choreoMode != ChoreoMode.it,
enableIT: choreographer.itEnabled &&
choreographer.choreoMode != ChoreoMode.it,
prevMessages: _prevMessages(),
);
if (_cacheClearTimer == null || !_cacheClearTimer!.isActive) {
@ -142,37 +131,7 @@ class IgcController {
}
}
// If there are any matches that don't match up with token offsets,
// this indicates and choreographer bug. Remove them.
final tokens = igcTextData!.tokens;
final List<PangeaMatch> confirmedMatches = List.from(filteredMatches);
for (final match in filteredMatches) {
final substring = match.match.fullText.characters
.skip(match.match.offset)
.take(match.match.length);
final trimmed = substring.toString().trim().characters;
final matchOffset = (match.match.offset + match.match.length) -
(substring.length - trimmed.length);
final hasStartMatch = tokens.any(
(token) => token.text.offset == match.match.offset,
);
final hasEndMatch = tokens.any(
(token) => token.text.offset + token.text.length == matchOffset,
);
if (!hasStartMatch || !hasEndMatch) {
confirmedMatches.clear();
ErrorHandler.logError(
m: "Match offset and/or length do not tokens with matching offset and length. This is a choreographer bug.",
data: {
"match": match.toJson(),
"tokens": tokens.map((e) => e.toJson()).toList(),
},
);
break;
}
}
igcTextData!.matches = confirmedMatches;
igcTextData!.matches = filteredMatches;
choreographer.acceptNormalizationMatches();
// TODO - for each new match,
@ -200,7 +159,6 @@ class IgcController {
e: err,
s: stack,
data: {
"onlyTokensAndLanguageDetection": onlyTokensAndLanguageDetection,
"currentText": choreographer.currentText,
"userL1": choreographer.l1LangCode,
"userL2": choreographer.l2LangCode,
@ -275,7 +233,7 @@ class IgcController {
/// Get the content of previous text and audio messages in chat.
/// Passed to IGC request to add context.
List<PreviousMessage> prevMessages({int numMessages = 5}) {
List<PreviousMessage> _prevMessages({int numMessages = 5}) {
final List<Event> events = choreographer.chatController.visibleEvents
.where(
(e) =>

@ -2,17 +2,14 @@ import 'dart:async';
import 'dart:developer';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:fluffychat/pangea/analytics_misc/construct_use_type_enum.dart';
import 'package:fluffychat/pangea/choreographer/constants/choreo_constants.dart';
import 'package:fluffychat/pangea/choreographer/controllers/error_service.dart';
import 'package:fluffychat/pangea/choreographer/enums/edit_type.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
import 'package:fluffychat/widgets/matrix.dart';
import '../models/custom_input_translation_model.dart';
import '../models/it_response_model.dart';
@ -56,7 +53,6 @@ class ITController {
goldRouteTracker = GoldRouteTracker.defaultTracker;
payLoadIds = [];
choreographer.altTranslator.clear();
choreographer.choreoMode = ChoreoMode.igc;
choreographer.setState();
}
@ -164,11 +160,10 @@ class ITController {
if (isTranslationDone) {
nextITStep = null;
choreographer.altTranslator.setTranslationFeedback();
choreographer.getLanguageHelp(onlyTokensAndLanguageDetection: true);
closeIT();
} else {
nextITStep = Completer<CurrentITStep?>();
final nextStep = await getNextTranslationData();
final nextStep = await _getNextTranslationData();
nextITStep?.complete(nextStep);
}
} catch (e, s) {
@ -192,7 +187,7 @@ class ITController {
}
}
Future<CurrentITStep?> getNextTranslationData() async {
Future<CurrentITStep?> _getNextTranslationData() async {
if (sourceText == null) {
ErrorHandler.logError(
e: Exception("sourceText is null in getNextTranslationData"),
@ -300,20 +295,6 @@ class ITController {
);
}
// MessageServiceModel? messageServiceModelWithMessageId() =>
// usedInteractiveTranslation
// ? MessageServiceModel(
// classId: choreographer.classId,
// roomId: choreographer.roomId,
// message: choreographer.currentText,
// messageId: null,
// payloadIds: payLoadIds,
// userId: choreographer.userId!,
// l1Lang: sourceLangCode,
// l2Lang: targetLangCode,
// )
// : null;
//maybe we store IT data in the same format? make a specific kind of match?
void selectTranslation(int chosenIndex) {
if (currentITStep == null) return;
@ -323,20 +304,6 @@ class ITController {
showChoiceFeedback = true;
// Get a list of the choices that the user did not click
final List<PangeaToken>? ignoredTokens = currentITStep?.continuances
.where((e) => !e.wasClicked)
.map((e) => e.tokens)
.expand((e) => e)
.toList();
// Save those choices' tokens to local construct analytics as ignored tokens
choreographer.pangeaController.putAnalytics.addDraftUses(
ignoredTokens ?? [],
choreographer.roomId,
ConstructUseTypeEnum.ignIt,
);
Future.delayed(
const Duration(
milliseconds: ChoreoConstants.millisecondsToDisplayFeedback,
@ -357,8 +324,6 @@ class ITController {
payLoadIds.add(res.payloadId);
}
bool get usedInteractiveTranslation => sourceText != null;
bool get isTranslationDone => currentITStep != null && currentITStep!.isFinal;
bool get isOpen => _isOpen;
@ -371,41 +336,12 @@ class ITController {
bool get isLoading => choreographer.isFetching;
String latestChoiceFeedback(BuildContext context) =>
completedITSteps.isNotEmpty
? completedITSteps.last.choiceFeedback(context)
: "";
// String translationFeedback(BuildContext context) =>
// completedITSteps.isNotEmpty
// ? completedITSteps.last.translationFeedback(context)
// : "";
bool get showAlternativeTranslationsOption => completedITSteps.isNotEmpty
? completedITSteps.last.showAlternativeTranslationOption &&
sourceText != null
: false;
setIsEditingSourceText(bool value) {
void setIsEditingSourceText(bool value) {
_isEditingSourceText = value;
choreographer.setState();
}
bool get isEditingSourceText => _isEditingSourceText;
int get correctChoices =>
completedITSteps.where((element) => element.isCorrect).length;
int get wildcardChoices =>
completedITSteps.where((element) => element.isYellow).length;
int get incorrectChoices =>
completedITSteps.where((element) => element.isWrong).length;
int get customChoices =>
completedITSteps.where((element) => element.isCustom).length;
bool get allCorrect => completedITSteps.every((element) => element.isCorrect);
}
class ITStartData {

@ -177,6 +177,17 @@ class ChoreoRecordStep {
data[_stepKey] = itStep?.toJson();
return data;
}
List<String>? get choices {
if (itStep != null) {
return itStep!.continuances.map((e) => e.text).toList().cast<String>();
}
return acceptedOrIgnoredMatch?.match.choices
?.map((e) => e.value)
.toList()
.cast<String>();
}
}
// Example flow

@ -6,25 +6,19 @@ import 'package:flutter/material.dart';
import 'package:collection/collection.dart';
import 'package:fluffychat/pangea/choreographer/models/choreo_record.dart';
import 'package:fluffychat/pangea/choreographer/models/language_detection_model.dart';
import 'package:fluffychat/pangea/choreographer/models/pangea_match_model.dart';
import 'package:fluffychat/pangea/choreographer/models/span_data.dart';
import 'package:fluffychat/pangea/choreographer/repo/language_detection_repo.dart';
import 'package:fluffychat/pangea/common/constants/model_keys.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/events/event_wrappers/pangea_representation_event.dart';
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
import 'package:fluffychat/pangea/events/models/representation_content_model.dart';
import 'package:fluffychat/pangea/learning_settings/constants/language_constants.dart';
import 'package:fluffychat/widgets/matrix.dart';
// import 'package:language_tool/language_tool.dart';
class IGCTextData {
LanguageDetectionResponse detections;
String originalInput;
String? fullTextCorrection;
List<PangeaToken> tokens;
List<PangeaMatch> matches;
String userL1;
String userL2;
@ -33,10 +27,8 @@ class IGCTextData {
bool loading = false;
IGCTextData({
required this.detections,
required this.originalInput,
required this.fullTextCorrection,
required this.tokens,
required this.matches,
required this.userL1,
required this.userL2,
@ -45,25 +37,7 @@ class IGCTextData {
});
factory IGCTextData.fromJson(Map<String, dynamic> json) {
// changing this to allow for use of the LanguageDetectionResponse methods
// TODO - change API after we're sure all clients are updated. not urgent.
final LanguageDetectionResponse detections =
json[_detectionsKey] is Iterable
? LanguageDetectionResponse.fromJson({
"detections": json[_detectionsKey],
"full_text": json["original_input"],
})
: LanguageDetectionResponse.fromJson(
json[_detectionsKey] as Map<String, dynamic>,
);
return IGCTextData(
tokens: (json[_tokensKey] as Iterable)
.map<PangeaToken>(
(e) => PangeaToken.fromJson(e as Map<String, dynamic>),
)
.toList()
.cast<PangeaToken>(),
matches: json[_matchesKey] != null
? (json[_matchesKey] as Iterable)
.map<PangeaMatch>(
@ -74,7 +48,6 @@ class IGCTextData {
.toList()
.cast<PangeaMatch>()
: [],
detections: detections,
originalInput: json["original_input"],
fullTextCorrection: json["full_text_correction"],
userL1: json[ModelKey.userL1],
@ -90,7 +63,6 @@ class IGCTextData {
String userL2,
) {
final PangeaRepresentation content = event.content;
final List<PangeaToken> tokens = event.tokens ?? [];
final List<PangeaMatch> matches = event.choreo?.choreoSteps
.map((step) => step.acceptedOrIgnoredMatch)
.whereType<PangeaMatch>()
@ -102,38 +74,9 @@ class IGCTextData {
originalInput = matches.first.match.fullText;
}
final defaultDetections = LanguageDetectionResponse(
detections: [
LanguageDetection(langCode: content.langCode, confidence: 1),
],
fullText: content.text,
);
LanguageDetectionResponse detections = defaultDetections;
if (event.detections != null) {
try {
detections = LanguageDetectionResponse.fromJson({
"detections": event.detections,
"full_text": content.text,
});
} catch (e, s) {
ErrorHandler.logError(
e: e,
s: s,
m: "Error parsing detections in IGCTextData.fromRepresentationEvent",
data: {
"detections": event.detections,
"full_text": content.text,
},
);
}
}
return IGCTextData(
detections: detections,
originalInput: originalInput,
fullTextCorrection: content.text,
tokens: tokens,
matches: matches,
userL1: userL1,
userL2: userL2,
@ -142,15 +85,11 @@ class IGCTextData {
);
}
static const String _tokensKey = "tokens";
static const String _matchesKey = "matches";
static const String _detectionsKey = "detections";
Map<String, dynamic> toJson() => {
_detectionsKey: detections.toJson(),
"original_input": originalInput,
"full_text_correction": fullTextCorrection,
_tokensKey: tokens.map((e) => e.toJson()).toList(),
_matchesKey: matches.map((e) => e.toJson()).toList(),
ModelKey.userL1: userL1,
ModelKey.userL2: userL2,
@ -158,15 +97,6 @@ class IGCTextData {
"enable_igc": enableIGC,
};
/// if we haven't run IGC or IT or there are no matches, we use the highest validated detection
/// from [LanguageDetectionResponse.highestValidatedDetection]
/// if we have run igc/it and there are no matches, we can relax the threshold
/// and use the highest confidence detection
String get detectedLanguage {
return detections.detections.firstOrNull?.langCode ??
LanguageKeys.unknownLanguage;
}
// reconstruct fullText based on accepted match
//update offsets in existing matches to reflect the change
//if existing matches overlap with the accepted one, remove them??
@ -198,72 +128,8 @@ class IGCTextData {
final fullText = newStart + replacement.value.characters + newEnd;
originalInput = fullText.toString();
int startIndex;
int endIndex;
// replace the tokens that are part of the match
// with the tokens in the replacement
// start is inclusive
try {
startIndex = tokenIndexByOffset(pangeaMatch.match.offset);
// end is exclusive, hence the +1
// use pangeaMatch.matchContent.trim().length instead of pangeaMatch.match.length since pangeaMatch.match.length may include leading/trailing spaces
endIndex = tokenIndexByOffset(
pangeaMatch.match.offset + pangeaMatch.matchContent.trim().length,
) +
1;
} catch (err, s) {
matches.removeAt(matchIndex);
for (final match in matches) {
match.match.fullText = originalInput;
if (match.match.offset > pangeaMatch.match.offset) {
match.match.offset +=
replacement.value.length - pangeaMatch.match.length;
}
}
ErrorHandler.logError(
e: err,
s: s,
data: {
"cursorOffset": pangeaMatch.match.offset,
"match": pangeaMatch.match.toJson(),
"tokens": tokens.map((e) => e.toJson()).toString(),
},
);
return;
}
// for all tokens after the replacement, update their offsets
for (int i = endIndex; i < tokens.length; i++) {
tokens[i].text.offset +=
replacement.value.length - pangeaMatch.match.length;
}
// clone the list for debugging purposes
final List<PangeaToken> newTokens = List.from(tokens);
// replace the tokens in the list
newTokens.replaceRange(startIndex, endIndex, replacement.tokens);
final String newFullText = PangeaToken.reconstructText(newTokens);
if (newFullText.trim() != originalInput.trim() && kDebugMode) {
PangeaToken.reconstructText(newTokens, debugWalkThrough: true);
ErrorHandler.logError(
m: "reconstructed text not working",
s: StackTrace.current,
data: {
"originalInput": originalInput,
"newFullText": newFullText,
"match": pangeaMatch.match.toJson(),
},
);
}
tokens = newTokens;
//update offsets in existing matches to reflect the change
//Question - remove matches that overlap with the accepted one?
// update offsets in existing matches to reflect the change
// Question - remove matches that overlap with the accepted one?
// see case of "quiero ver un fix"
matches.removeAt(matchIndex);
@ -276,23 +142,6 @@ class IGCTextData {
}
}
void removeMatchByOffset(int offset) {
final int index = getTopMatchIndexForOffset(offset);
if (index != -1) {
matches.removeAt(index);
}
}
int tokenIndexByOffset(int cursorOffset) {
final tokenIndex = tokens.indexWhere(
(token) => token.start <= cursorOffset && cursorOffset <= token.end,
);
if (tokenIndex < 0) {
throw "No token found for cursor offset";
}
return tokenIndex;
}
List<int> matchIndicesByOffset(int offset) {
final List<int> matchesForOffset = [];
for (final (index, match) in matches.indexed) {
@ -314,34 +163,6 @@ class IGCTextData {
return matchesForToken[matchIndex];
}
PangeaMatch? getTopMatchForToken(PangeaToken token) {
final int topMatchIndex = getTopMatchIndexForOffset(token.text.offset);
if (topMatchIndex == -1) return null;
return matches[topMatchIndex];
}
int getAfterTokenSpacingByIndex(int tokenIndex) {
final int endOfToken = tokens[tokenIndex].end;
if (tokenIndex + 1 < tokens.length) {
final spaceBetween = tokens[tokenIndex + 1].text.offset - endOfToken;
if (spaceBetween < 0) {
ErrorHandler.logError(
m: "weird token lengths for ${tokens[tokenIndex].text.content} and ${tokens[tokenIndex + 1].text.content}",
data: {
"fullText": originalInput,
"tokens": tokens.map((e) => e.toJson()).toString(),
},
);
return 0;
}
return spaceBetween;
} else {
return originalInput.length - endOfToken;
}
}
static TextStyle underlineStyle(Color color) => TextStyle(
decoration: TextDecoration.underline,
decorationColor: color,
@ -459,19 +280,4 @@ class IGCTextData {
return items;
}
List<PangeaToken> matchTokens(int matchIndex) {
if (matchIndex >= matches.length) {
return [];
}
final PangeaMatch match = matches[matchIndex];
final List<PangeaToken> tokensForMatch = [];
for (final token in tokens) {
if (match.isOffsetInMatchSpan(token.text.offset)) {
tokensForMatch.add(token);
}
}
return tokensForMatch;
}
}

@ -4,8 +4,6 @@ import 'package:collection/collection.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:fluffychat/pangea/choreographer/constants/choreo_constants.dart';
import 'package:fluffychat/pangea/common/constants/model_keys.dart';
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
class ITResponseModel {
String fullTextTranslation;
@ -80,7 +78,7 @@ class Continuance {
double probability;
int level;
String text;
List<PangeaToken> tokens;
// List<PangeaToken> tokens;
/// saving this in a full json form
String description;
@ -100,18 +98,18 @@ class Continuance {
required this.inDictionary,
required this.hasInfo,
required this.gold,
required this.tokens,
// required this.tokens,
});
factory Continuance.fromJson(Map<String, dynamic> json) {
final List<PangeaToken> tokensInternal = (json[ModelKey.tokens] != null)
? (json[ModelKey.tokens] as Iterable)
.map<PangeaToken>(
(e) => PangeaToken.fromJson(e as Map<String, dynamic>),
)
.toList()
.cast<PangeaToken>()
: [];
// final List<PangeaToken> tokensInternal = (json[ModelKey.tokens] != null)
// ? (json[ModelKey.tokens] as Iterable)
// .map<PangeaToken>(
// (e) => PangeaToken.fromJson(e as Map<String, dynamic>),
// )
// .toList()
// .cast<PangeaToken>()
// : [];
return Continuance(
probability: json['probability'].toDouble(),
level: json['level'],
@ -122,7 +120,7 @@ class Continuance {
wasClicked: json['clkd'] ?? false,
hasInfo: json['has_info'] ?? false,
gold: json['gold'] ?? false,
tokens: tokensInternal,
// tokens: tokensInternal,
);
}
@ -132,7 +130,7 @@ class Continuance {
data['level'] = level;
data['text'] = text;
data['clkd'] = wasClicked;
data[ModelKey.tokens] = tokens.map((e) => e.toJson()).toList();
// data[ModelKey.tokens] = tokens.map((e) => e.toJson()).toList();
if (!condensed) {
data['description'] = description;

@ -8,8 +8,6 @@ import 'package:flutter/material.dart';
import 'package:collection/collection.dart';
import 'package:fluffychat/pangea/common/constants/model_keys.dart';
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
import '../enums/span_choice_type.dart';
import '../enums/span_data_type.dart';
@ -75,7 +73,7 @@ class SpanChoice {
bool selected;
String? feedback;
DateTime? timestamp;
List<PangeaToken> tokens;
// List<PangeaToken> tokens;
SpanChoice({
required this.value,
@ -83,18 +81,18 @@ class SpanChoice {
this.feedback,
this.selected = false,
this.timestamp,
this.tokens = const [],
// this.tokens = const [],
});
factory SpanChoice.fromJson(Map<String, dynamic> json) {
final List<PangeaToken> tokensInternal = (json[ModelKey.tokens] != null)
? (json[ModelKey.tokens] as Iterable)
.map<PangeaToken>(
(e) => PangeaToken.fromJson(e as Map<String, dynamic>),
)
.toList()
.cast<PangeaToken>()
: [];
// final List<PangeaToken> tokensInternal = (json[ModelKey.tokens] != null)
// ? (json[ModelKey.tokens] as Iterable)
// .map<PangeaToken>(
// (e) => PangeaToken.fromJson(e as Map<String, dynamic>),
// )
// .toList()
// .cast<PangeaToken>()
// : [];
return SpanChoice(
value: json['value'] as String,
type: json['type'] != null
@ -107,7 +105,7 @@ class SpanChoice {
selected: json['selected'] ?? false,
timestamp:
json['timestamp'] != null ? DateTime.parse(json['timestamp']) : null,
tokens: tokensInternal,
// tokens: tokensInternal,
);
}
@ -117,7 +115,7 @@ class SpanChoice {
'selected': selected,
'feedback': feedback,
'timestamp': timestamp?.toIso8601String(),
'tokens': tokens.map((e) => e.toJson()).toList(),
// 'tokens': tokens.map((e) => e.toJson()).toList(),
};
String feedbackToDisplay(BuildContext context) {

@ -2,14 +2,9 @@ import 'dart:convert';
import 'package:http/http.dart';
import 'package:fluffychat/pangea/choreographer/models/language_detection_model.dart';
import 'package:fluffychat/pangea/choreographer/models/pangea_match_model.dart';
import 'package:fluffychat/pangea/choreographer/repo/language_detection_repo.dart';
import 'package:fluffychat/pangea/choreographer/repo/span_data_repo.dart';
import 'package:fluffychat/pangea/common/config/environment.dart';
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
import 'package:fluffychat/pangea/events/models/pangea_token_text_model.dart';
import 'package:fluffychat/pangea/lemmas/lemma.dart';
import '../../common/constants/model_keys.dart';
import '../../common/network/requests.dart';
import '../../common/network/urls.dart';
@ -41,42 +36,6 @@ class IgcRepo {
await Future.delayed(const Duration(seconds: 2));
final IGCTextData igcTextData = IGCTextData(
detections: LanguageDetectionResponse(
detections: [LanguageDetection(langCode: "en", confidence: 0.99)],
fullText: "This be a sample text",
),
tokens: [
PangeaToken(
text: PangeaTokenText(content: "This", offset: 0, length: 4),
lemma: Lemma(form: "This", text: "this", saveVocab: true),
pos: "DET",
morph: {},
),
PangeaToken(
text: PangeaTokenText(content: "be", offset: 5, length: 2),
lemma: Lemma(form: "be", text: "be", saveVocab: true),
pos: "VERB",
morph: {},
),
PangeaToken(
text: PangeaTokenText(content: "a", offset: 8, length: 1),
lemma: Lemma(form: "a", text: "a", saveVocab: true),
pos: "DET",
morph: {},
),
PangeaToken(
text: PangeaTokenText(content: "sample", offset: 10, length: 6),
lemma: Lemma(form: "sample", text: "sample", saveVocab: true),
pos: "NOUN",
morph: {},
),
PangeaToken(
text: PangeaTokenText(content: "text", offset: 17, length: 4),
lemma: Lemma(form: "text", text: "text", saveVocab: true),
pos: "NOUN",
morph: {},
),
],
matches: [
PangeaMatch(
match: spanDataRepomockSpan,

@ -8,7 +8,6 @@ import '../../common/network/requests.dart';
import '../../common/network/urls.dart';
import '../models/custom_input_translation_model.dart';
import '../models/it_response_model.dart';
import 'system_choice_translation_model.dart';
class ITRepo {
static Future<ITResponseModel> customInputTranslate(
@ -26,19 +25,19 @@ class ITRepo {
return ITResponseModel.fromJson(json);
}
static Future<ITResponseModel> systemChoiceTranslate(
SystemChoiceRequestModel subseqText,
) async {
final Requests req = Requests(
choreoApiKey: Environment.choreoApiKey,
accessToken: MatrixState.pangeaController.userController.accessToken,
);
// static Future<ITResponseModel> systemChoiceTranslate(
// SystemChoiceRequestModel subseqText,
// ) async {
// final Requests req = Requests(
// choreoApiKey: Environment.choreoApiKey,
// accessToken: MatrixState.pangeaController.userController.accessToken,
// );
final Response res =
await req.post(url: PApiUrls.subseqStep, body: subseqText.toJson());
// final Response res =
// await req.post(url: PApiUrls.subseqStep, body: subseqText.toJson());
final decodedBody = jsonDecode(utf8.decode(res.bodyBytes).toString());
// final decodedBody = jsonDecode(utf8.decode(res.bodyBytes).toString());
return ITResponseModel.fromJson(decodedBody);
}
// return ITResponseModel.fromJson(decodedBody);
// }
}

@ -0,0 +1,46 @@
import 'dart:convert';
import 'package:http/http.dart';
import 'package:fluffychat/pangea/common/config/environment.dart';
import 'package:fluffychat/pangea/common/network/requests.dart';
import 'package:fluffychat/pangea/common/network/urls.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/events/repo/token_api_models.dart';
class TokensRepo {
static Future<TokensResponseModel> get(
String? accessToken, {
required TokensRequestModel request,
}) async {
final Requests req = Requests(
choreoApiKey: Environment.choreoApiKey,
accessToken: accessToken,
);
final Response res = await req.post(
url: PApiUrls.tokenize,
body: request.toJson(),
);
final TokensResponseModel response = TokensResponseModel.fromJson(
jsonDecode(
utf8.decode(res.bodyBytes).toString(),
),
);
if (response.tokens.isEmpty) {
ErrorHandler.logError(
e: Exception(
"empty tokens in tokenize response return",
),
data: {
"accessToken": accessToken,
"request": request.toJson(),
},
);
}
return response;
}
}

@ -0,0 +1,315 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pangea/analytics_details_popup/analytics_details_popup.dart';
import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart';
import 'package:fluffychat/pangea/analytics_summary/progress_indicators_enum.dart';
import 'package:fluffychat/widgets/matrix.dart';
class MessageAnalyticsFeedback extends StatefulWidget {
final String overlayId;
final int newGrammarConstructs;
final int newVocabConstructs;
const MessageAnalyticsFeedback({
required this.overlayId,
required this.newGrammarConstructs,
required this.newVocabConstructs,
super.key,
});
@override
State<MessageAnalyticsFeedback> createState() =>
MessageAnalyticsFeedbackState();
}
class MessageAnalyticsFeedbackState extends State<MessageAnalyticsFeedback>
with TickerProviderStateMixin {
late AnimationController _vocabController;
late AnimationController _grammarController;
late AnimationController _bubbleController;
late Animation<double> _vocabOpacity;
late Animation<double> _grammarOpacity;
late Animation<double> _scaleAnimation;
late Animation<double> _opacityAnimation;
static const counterDelay = Duration(milliseconds: 400);
@override
void initState() {
super.initState();
_grammarController = AnimationController(
vsync: this,
duration: FluffyThemes.animationDuration,
);
_grammarOpacity = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(parent: _grammarController, curve: Curves.easeInOut),
);
_vocabController = AnimationController(
vsync: this,
duration: FluffyThemes.animationDuration,
);
_vocabOpacity = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(parent: _vocabController, curve: Curves.easeInOut),
);
_bubbleController = AnimationController(
vsync: this,
duration: FluffyThemes.animationDuration,
);
_scaleAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(parent: _bubbleController, curve: Curves.easeInOut),
);
_opacityAnimation = Tween<double>(begin: 0.0, end: 0.9).animate(
CurvedAnimation(parent: _bubbleController, curve: Curves.easeInOut),
);
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) _bubbleController.forward();
Future.delayed(counterDelay, () {
if (mounted) {
_vocabController.forward();
_grammarController.forward();
}
});
Future.delayed(const Duration(milliseconds: 4000), () {
if (!mounted) return;
_bubbleController.reverse().then((_) {
MatrixState.pAnyState.closeOverlay(widget.overlayId);
});
});
});
}
@override
void dispose() {
_vocabController.dispose();
_grammarController.dispose();
_bubbleController.dispose();
super.dispose();
}
void _showAnalyticsDialog(ConstructTypeEnum? type) {
showDialog<AnalyticsPopupWrapper>(
context: context,
builder: (context) => AnalyticsPopupWrapper(
view: type ?? ConstructTypeEnum.vocab,
),
);
}
@override
Widget build(BuildContext context) {
if (widget.newVocabConstructs <= 0 && widget.newGrammarConstructs <= 0) {
return const SizedBox.shrink();
}
final theme = Theme.of(context);
return Material(
type: MaterialType.transparency,
child: InkWell(
onTap: () => _showAnalyticsDialog(null),
child: ScaleTransition(
scale: _scaleAnimation,
alignment: Alignment.bottomRight,
child: AnimatedBuilder(
animation: _bubbleController,
builder: (context, child) {
return Container(
decoration: BoxDecoration(
color: theme.colorScheme.surfaceContainerHighest
.withAlpha((_opacityAnimation.value * 255).round()),
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(16.0),
topRight: Radius.circular(16.0),
bottomLeft: Radius.circular(16.0),
bottomRight: Radius.circular(4.0),
),
),
padding: const EdgeInsets.symmetric(
vertical: 8.0,
horizontal: 16.0,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (widget.newVocabConstructs > 0)
NewConstructsBadge(
controller: _vocabController,
opacityAnimation: _vocabOpacity,
newConstructs: widget.newVocabConstructs,
type: ConstructTypeEnum.vocab,
tooltip: L10n.of(context).newVocab,
onTap: () => _showAnalyticsDialog(
ConstructTypeEnum.vocab,
),
),
if (widget.newGrammarConstructs > 0)
NewConstructsBadge(
controller: _grammarController,
opacityAnimation: _grammarOpacity,
newConstructs: widget.newGrammarConstructs,
type: ConstructTypeEnum.morph,
tooltip: L10n.of(context).newGrammar,
onTap: () => _showAnalyticsDialog(
ConstructTypeEnum.morph,
),
),
],
),
);
},
),
),
),
);
}
}
class NewConstructsBadge extends StatelessWidget {
final AnimationController controller;
final Animation<double> opacityAnimation;
final int newConstructs;
final ConstructTypeEnum type;
final String tooltip;
final VoidCallback onTap;
const NewConstructsBadge({
required this.controller,
required this.opacityAnimation,
required this.newConstructs,
required this.type,
required this.tooltip,
required this.onTap,
super.key,
});
@override
Widget build(BuildContext context) {
return InkWell(
onTap: onTap,
child: Tooltip(
message: tooltip,
child: AnimatedBuilder(
animation: controller,
builder: (context, child) {
return Opacity(
opacity: opacityAnimation.value,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Symbols.toys_and_games,
color: ProgressIndicatorEnum.morphsUsed.color(context),
size: 24,
),
const SizedBox(width: 4.0),
AnimatedCounter(
key: ValueKey("$type-counter"),
endValue: newConstructs,
startAnimation: opacityAnimation.value > 0.9,
style: TextStyle(
color: ProgressIndicatorEnum.morphsUsed.color(context),
fontWeight: FontWeight.bold,
),
),
],
),
),
);
},
),
),
);
}
}
class AnimatedCounter extends StatefulWidget {
final int endValue;
final TextStyle? style;
final bool startAnimation;
const AnimatedCounter({
super.key,
required this.endValue,
this.style,
this.startAnimation = true,
});
@override
State<AnimatedCounter> createState() => _AnimatedCounterState();
}
class _AnimatedCounterState extends State<AnimatedCounter>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<int> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: FluffyThemes.animationDuration,
);
_animation = IntTween(
begin: 0,
end: widget.endValue,
).animate(
CurvedAnimation(
parent: _controller,
curve: Curves.easeOutCubic,
),
);
if (widget.startAnimation) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) _controller.forward();
});
}
}
@override
void didUpdateWidget(AnimatedCounter oldWidget) {
super.didUpdateWidget(oldWidget);
if (!oldWidget.startAnimation && widget.startAnimation && !_hasAnimated) {
_controller.forward();
}
}
bool get _hasAnimated => _controller.isCompleted || _controller.isAnimating;
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _animation,
builder: (context, child) {
return Text(
"+ ${_animation.value}",
style: widget.style,
);
},
);
}
}

@ -72,15 +72,6 @@ class PangeaTextController extends TextEditingController {
return;
}
int tokenIndex;
try {
tokenIndex = choreographer.igc.igcTextData!.tokenIndexByOffset(
selection.baseOffset,
);
} catch (_) {
return;
}
final int matchIndex =
choreographer.igc.igcTextData!.getTopMatchIndexForOffset(
selection.baseOffset,
@ -102,9 +93,7 @@ class PangeaTextController extends TextEditingController {
matchIndex: matchIndex,
onReplacementSelect: choreographer.onReplacementSelect,
// may not need this
onSentenceRewrite: ((sentenceRewrite) async {
debugPrint("onSentenceRewrite $tokenIndex $sentenceRewrite");
}),
onSentenceRewrite: ((sentenceRewrite) async {}),
onIgnore: () => choreographer.onIgnoreMatch(
cursorOffset: selection.baseOffset,
),

@ -6,14 +6,12 @@ import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pangea/analytics_misc/construct_use_type_enum.dart';
import 'package:fluffychat/pangea/bot/utils/bot_style.dart';
import 'package:fluffychat/pangea/choreographer/enums/span_data_type.dart';
import 'package:fluffychat/pangea/choreographer/models/span_data.dart';
import 'package:fluffychat/pangea/choreographer/utils/match_copy.dart';
import 'package:fluffychat/pangea/choreographer/widgets/igc/card_error_widget.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
import 'package:fluffychat/pangea/toolbar/controllers/tts_controller.dart';
import '../../../../widgets/matrix.dart';
import '../../../bot/widgets/bot_face_svg.dart';
@ -153,18 +151,6 @@ class SpanCardState extends State<SpanCard> {
Future<void> onChoiceSelect(int index) async {
selectedChoiceIndex = index;
if (selectedChoice != null) {
if (!selectedChoice!.selected) {
MatrixState.pangeaController.putAnalytics.addDraftUses(
selectedChoice!.tokens,
widget.roomId,
selectedChoice!.isBestCorrection
? ConstructUseTypeEnum.corIGC
: ConstructUseTypeEnum.incIGC,
targetID:
"${selectedChoice!.value}${widget.scm.pangeaMatch?.hashCode.toString()}",
);
}
selectedChoice!.timestamp = DateTime.now();
selectedChoice!.selected = true;
setState(
@ -175,28 +161,7 @@ class SpanCardState extends State<SpanCard> {
}
}
/// Returns the list of distractor choices that are not selected
List<SpanChoice>? get ignoredMatches => widget.scm.pangeaMatch?.match.choices
?.where((choice) => choice.isDistractor && !choice.selected)
.toList();
/// Returns the list of tokens from choices that are not selected
List<PangeaToken>? get ignoredTokens => ignoredMatches
?.expand((choice) => choice.tokens)
.toList()
.cast<PangeaToken>();
/// Adds the ignored tokens to locally cached analytics
void addIgnoredTokenUses() {
MatrixState.pangeaController.putAnalytics.addDraftUses(
ignoredTokens ?? [],
widget.roomId,
ConstructUseTypeEnum.ignIGC,
);
}
Future<void> onReplaceSelected() async {
addIgnoredTokenUses();
await widget.scm.onReplacementSelect(
matchIndex: widget.scm.matchIndex,
choiceIndex: selectedChoiceIndex!,
@ -205,8 +170,6 @@ class SpanCardState extends State<SpanCard> {
}
void onIgnoreMatch() {
addIgnoredTokenUses();
Future.delayed(
Duration.zero,
() {

@ -4,14 +4,11 @@ import 'dart:developer';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart';
import 'package:fluffychat/pangea/analytics_misc/construct_use_type_enum.dart';
import 'package:fluffychat/pangea/choreographer/constants/choreo_constants.dart';
import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart';
import 'package:fluffychat/pangea/choreographer/controllers/it_controller.dart';
import 'package:fluffychat/pangea/choreographer/widgets/it_bar_buttons.dart';
import 'package:fluffychat/pangea/choreographer/widgets/it_feedback_card.dart';
import 'package:fluffychat/pangea/choreographer/widgets/translation_finished_flow.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/instructions/instructions_enum.dart';
import 'package:fluffychat/pangea/instructions/instructions_inline_tooltip.dart';
@ -230,22 +227,7 @@ class ITBarState extends State<ITBar> with SingleTickerProviderStateMixin {
controller: itController,
)
: itController.isTranslationDone
? TranslationFeedback(
controller: itController,
vocabCount: itController
.choreographer.altTranslator
.countNewConstructs(
ConstructTypeEnum.vocab,
),
grammarCount: itController
.choreographer.altTranslator
.countNewConstructs(
ConstructTypeEnum.morph,
),
feedbackText: itController
.choreographer.altTranslator
.getDefaultFeedback(context),
)
? const SizedBox()
: ITChoices(controller: itController),
),
),
@ -394,17 +376,6 @@ class ITChoices extends StatelessWidget {
continuance.feedbackText(context),
);
}
if (!continuance.wasClicked) {
controller.choreographer.pangeaController.putAnalytics.addDraftUses(
continuance.tokens,
controller.choreographer.roomId,
continuance.level > 1
? ConstructUseTypeEnum.incIt
: ConstructUseTypeEnum.corIt,
targetID:
"${continuance.text.trim()}${controller.currentITStep.hashCode.toString()}",
);
}
controller.currentITStep!.continuances[index].wasClicked = true;
controller.choreographer.setState();
}

@ -94,10 +94,7 @@ class StartIGCButtonState extends State<StartIGCButton>
);
return;
case AssistanceState.notFetched:
await widget.controller.choreographer.getLanguageHelp(
onlyTokensAndLanguageDetection: false,
manual: true,
);
await widget.controller.choreographer.getLanguageHelp(manual: true);
_showFirstMatch();
return;
case AssistanceState.fetched:

@ -1,379 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:fluffychat/pangea/analytics_summary/progress_indicators_enum.dart';
import 'package:fluffychat/pangea/choreographer/widgets/it_feedback_stars.dart';
import '../../bot/utils/bot_style.dart';
import '../../common/utils/error_handler.dart';
import '../controllers/it_controller.dart';
class TranslationFeedback extends StatefulWidget {
final int vocabCount;
final int grammarCount;
final String feedbackText;
final ITController controller;
const TranslationFeedback({
super.key,
required this.controller,
required this.vocabCount,
required this.grammarCount,
required this.feedbackText,
});
@override
State<TranslationFeedback> createState() => _TranslationFeedbackState();
}
class _TranslationFeedbackState extends State<TranslationFeedback>
with TickerProviderStateMixin {
late final int starRating;
late final int vocabCount;
late final int grammarCount;
// Animation controllers for each component
late AnimationController _starsController;
late AnimationController _vocabController;
late AnimationController _grammarController;
// Animations for opacity and scale
late Animation<double> _starsOpacity;
late Animation<double> _starsScale;
late Animation<double> _vocabOpacity;
late Animation<double> _grammarOpacity;
// Constants for animation timing
static const vocabDelay = Duration(milliseconds: 800);
static const grammarDelay = Duration(milliseconds: 1400);
// Duration for each individual animation
static const elementAnimDuration = Duration(milliseconds: 800);
@override
void initState() {
super.initState();
vocabCount = widget.vocabCount;
grammarCount = widget.grammarCount;
final altTranslator = widget.controller.choreographer.altTranslator;
starRating = altTranslator.starRating;
// Initialize animation controllers
_starsController = AnimationController(
vsync: this,
duration: elementAnimDuration,
);
_vocabController = AnimationController(
vsync: this,
duration: elementAnimDuration,
);
_grammarController = AnimationController(
vsync: this,
duration: elementAnimDuration,
);
// Define animations
_starsOpacity = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(parent: _starsController, curve: Curves.easeInOut),
);
_starsScale = Tween<double>(begin: 0.5, end: 1.0).animate(
CurvedAnimation(parent: _starsController, curve: Curves.elasticOut),
);
_vocabOpacity = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(parent: _vocabController, curve: Curves.easeInOut),
);
_grammarOpacity = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(parent: _grammarController, curve: Curves.easeInOut),
);
// Start animations with appropriate delays
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
// Start stars animation immediately
_starsController.forward();
// Start vocab animation after delay if there's vocab to show
if (vocabCount > 0) {
Future.delayed(vocabDelay, () {
if (mounted) _vocabController.forward();
});
}
// Start grammar animation after delay if there's grammar to show
if (grammarCount > 0) {
Future.delayed(grammarDelay, () {
if (mounted) _grammarController.forward();
});
}
}
});
}
@override
void dispose() {
_starsController.dispose();
_vocabController.dispose();
_grammarController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
try {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
// Animated stars
AnimatedBuilder(
animation: _starsController,
builder: (context, child) {
return Opacity(
opacity: _starsOpacity.value,
child: Transform.scale(
scale: _starsScale.value,
child: Padding(
padding: const EdgeInsets.only(bottom: 16.0),
child: FillingStars(rating: starRating),
),
),
);
},
),
if (vocabCount > 0 || grammarCount > 0)
Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (vocabCount > 0)
AnimatedBuilder(
animation: _vocabController,
builder: (context, child) {
return Opacity(
opacity: _vocabOpacity.value,
child: Padding(
padding:
const EdgeInsets.symmetric(horizontal: 8.0),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Symbols.dictionary,
color: ProgressIndicatorEnum.wordsUsed
.color(context),
size: 24,
),
const SizedBox(width: 4.0),
AnimatedCounter(
key: const ValueKey("vocabCounter"),
endValue: vocabCount,
// Only start counter animation when opacity animation is complete
startAnimation: _vocabOpacity.value > 0.9,
style: TextStyle(
color: ProgressIndicatorEnum.wordsUsed
.color(context),
fontWeight: FontWeight.bold,
),
),
],
),
),
);
},
),
if (grammarCount > 0)
AnimatedBuilder(
animation: _grammarController,
builder: (context, child) {
return Opacity(
opacity: _grammarOpacity.value,
child: Padding(
padding:
const EdgeInsets.symmetric(horizontal: 8.0),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Symbols.toys_and_games,
color: ProgressIndicatorEnum.morphsUsed
.color(context),
size: 24,
),
const SizedBox(width: 4.0),
AnimatedCounter(
key: const ValueKey("grammarCounter"),
endValue: grammarCount,
// Only start counter animation when opacity animation is complete
startAnimation: _grammarOpacity.value > 0.9,
style: TextStyle(
color: ProgressIndicatorEnum.morphsUsed
.color(context),
fontWeight: FontWeight.bold,
),
),
],
),
),
);
},
),
],
),
)
else
AnimatedBuilder(
animation: _starsController,
builder: (context, child) {
return Opacity(
opacity: _starsOpacity.value,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Text(
widget.feedbackText,
textAlign: TextAlign.center,
style: BotStyle.text(context),
),
),
);
},
),
const SizedBox(height: 8.0),
],
);
} catch (err, stack) {
debugPrint("Error in TranslationFeedback: $err");
ErrorHandler.logError(
e: err,
s: stack,
data: {},
);
// Fallback to a simple message if anything goes wrong
return Center(child: Text(L10n.of(context).niceJob));
}
}
}
class AnimatedCounter extends StatefulWidget {
final int endValue;
final TextStyle? style;
final Duration duration;
final String prefix;
final bool startAnimation;
const AnimatedCounter({
super.key,
required this.endValue,
this.style,
this.duration = const Duration(milliseconds: 1500),
this.prefix = "+ ",
this.startAnimation = true,
});
@override
State<AnimatedCounter> createState() => _AnimatedCounterState();
}
class _AnimatedCounterState extends State<AnimatedCounter>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<int> _animation;
bool _hasAnimated = false;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: widget.duration,
);
_animation = IntTween(
begin: 0,
end: widget.endValue,
).animate(
CurvedAnimation(
parent: _controller,
curve: Curves.easeOutCubic,
),
);
// Only start animation if startAnimation is true
if (widget.startAnimation) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
_controller.forward();
_hasAnimated = true;
}
});
}
}
@override
void didUpdateWidget(AnimatedCounter oldWidget) {
super.didUpdateWidget(oldWidget);
// Start animation when startAnimation changes to true
if (!oldWidget.startAnimation && widget.startAnimation && !_hasAnimated) {
_controller.forward();
_hasAnimated = true;
}
if (oldWidget.endValue != widget.endValue) {
if (_hasAnimated) {
_animation = IntTween(
begin: _animation.value,
end: widget.endValue,
).animate(
CurvedAnimation(
parent: _controller,
curve: Curves.easeOutCubic,
),
);
_controller.forward(from: 0.0);
} else if (widget.startAnimation) {
_animation = IntTween(
begin: 0,
end: widget.endValue,
).animate(
CurvedAnimation(
parent: _controller,
curve: Curves.easeOutCubic,
),
);
_controller.forward();
_hasAnimated = true;
}
}
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _animation,
builder: (context, child) {
return Text(
"${widget.prefix}${_animation.value}",
style: widget.style,
);
},
);
}
}

@ -1,18 +1,14 @@
import 'dart:async';
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:http/http.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/pangea/choreographer/repo/full_text_translation_repo.dart';
import 'package:fluffychat/pangea/common/config/environment.dart';
import 'package:fluffychat/pangea/choreographer/repo/tokens_repo.dart';
import 'package:fluffychat/pangea/common/controllers/base_controller.dart';
import 'package:fluffychat/pangea/common/controllers/pangea_controller.dart';
import 'package:fluffychat/pangea/common/network/requests.dart';
import 'package:fluffychat/pangea/common/network/urls.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart';
import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart';
@ -56,42 +52,6 @@ class MessageDataController extends BaseController {
super.dispose();
}
/// get tokens from the server
static Future<TokensResponseModel> _fetchTokens(
String accessToken,
TokensRequestModel request,
) async {
final Requests req = Requests(
choreoApiKey: Environment.choreoApiKey,
accessToken: accessToken,
);
final Response res = await req.post(
url: PApiUrls.tokenize,
body: request.toJson(),
);
final TokensResponseModel response = TokensResponseModel.fromJson(
jsonDecode(
utf8.decode(res.bodyBytes).toString(),
),
);
if (response.tokens.isEmpty) {
ErrorHandler.logError(
e: Exception(
"empty tokens in tokenize response return",
),
data: {
"accessToken": accessToken,
"request": request.toJson(),
},
);
}
return response;
}
/// get tokens from the server
/// if repEventId is not null, send the tokens to the room
Future<List<PangeaToken>> _getTokens({
@ -99,9 +59,9 @@ class MessageDataController extends BaseController {
required TokensRequestModel req,
required Room? room,
}) async {
final TokensResponseModel res = await _fetchTokens(
final TokensResponseModel res = await TokensRepo.get(
_pangeaController.userController.accessToken,
req,
request: req,
);
if (repEventId != null && room != null) {
room
@ -234,9 +194,9 @@ class MessageDataController extends BaseController {
required TokensRequestModel req,
required Room room,
}) async {
final TokensResponseModel res = await _fetchTokens(
final TokensResponseModel res = await TokensRepo.get(
_pangeaController.userController.accessToken,
req,
request: req,
);
try {

@ -189,6 +189,7 @@ class PangeaToken {
OneConstructUse toVocabUse(
ConstructUseTypeEnum type,
ConstructUseMetaData metadata,
int xp,
) {
return OneConstructUse(
useType: type,
@ -197,17 +198,19 @@ class PangeaToken {
constructType: ConstructTypeEnum.vocab,
metadata: metadata,
category: pos,
xp: xp,
);
}
List<OneConstructUse> allUses(
ConstructUseTypeEnum type,
ConstructUseMetaData metadata,
int xp,
) {
final List<OneConstructUse> uses = [];
if (!lemma.saveVocab) return uses;
uses.add(toVocabUse(type, metadata));
uses.add(toVocabUse(type, metadata, xp));
for (final morphFeature in morph.keys) {
uses.add(
OneConstructUse(
@ -217,6 +220,7 @@ class PangeaToken {
constructType: ConstructTypeEnum.morph,
metadata: metadata,
category: morphFeature,
xp: xp,
),
);
}

@ -1,10 +1,12 @@
import 'dart:math';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart';
import 'package:fluffychat/pangea/analytics_misc/construct_use_type_enum.dart';
import 'package:fluffychat/pangea/analytics_misc/constructs_model.dart';
import 'package:fluffychat/pangea/choreographer/models/choreo_record.dart';
import 'package:fluffychat/pangea/choreographer/models/pangea_match_model.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
import 'package:fluffychat/pangea/toolbar/models/speech_to_text_models.dart';
import 'package:fluffychat/widgets/matrix.dart';
@ -132,162 +134,138 @@ class PangeaRepresentation {
)
.toList();
}
for (final token in tokensToSave) {
uses.addAll(
_getUsesForToken(
token,
metadata,
choreo: choreo,
),
);
if (choreo == null ||
(choreo.choreoSteps.isEmpty && choreo.itSteps.isEmpty)) {
for (final token in tokensToSave) {
uses.addAll(
token.allUses(
ConstructUseTypeEnum.wa,
metadata,
ConstructUseTypeEnum.wa.pointValue,
),
);
}
return uses;
}
return uses;
}
for (final token in tokensToSave) {
ChoreoRecordStep? tokenStep;
for (final step in choreo.choreoSteps) {
final igcMatch = step.acceptedOrIgnoredMatch;
final itStep = step.itStep;
if (itStep == null && igcMatch == null) {
continue;
}
/// Returns a [OneConstructUse] for the given [token]
/// If there is no [choreo], the [token] is
/// considered to be a [ConstructUseTypeEnum.wa] as long as it matches the target language.
/// Later on, we may want to consider putting it in some category of like 'pending'
///
/// For both vocab and morph constructs, we should
/// 1) give wa if no assistance was used
/// 2) give ga if IGC was used and
/// 3) make no use if IT was used
List<OneConstructUse> _getUsesForToken(
PangeaToken token,
ConstructUseMetaData metadata, {
ChoreoRecord? choreo,
}) {
final List<OneConstructUse> uses = [];
final lemma = token.lemma;
final content = token.text.content;
if (choreo == null) {
uses.add(
OneConstructUse(
useType: ConstructUseTypeEnum.wa,
lemma: token.pos,
form: token.text.content,
category: "POS",
constructType: ConstructTypeEnum.morph,
metadata: metadata,
),
);
for (final entry in token.morph.entries) {
uses.add(
OneConstructUse(
useType: ConstructUseTypeEnum.wa,
lemma: entry.value,
form: token.text.content,
category: entry.key,
constructType: ConstructTypeEnum.morph,
metadata: metadata,
),
final choices = step.choices;
if (choices == null || choices.isEmpty) {
continue;
}
final stepContainsToken = choices.any(
(choice) => choice.contains(token.text.content),
);
// if the step contains the token, and the token hasn't been assigned a step
// (or the assigned step is an IGC step, but an IT step contains the token)
// then assign the token to the step
if (stepContainsToken &&
(tokenStep == null ||
(tokenStep.itStep == null && step.itStep != null))) {
tokenStep = step;
}
}
if (lemma.saveVocab) {
uses.add(
token.toVocabUse(
if (tokenStep == null ||
tokenStep.acceptedOrIgnoredMatch?.status ==
PangeaMatchStatus.automatic) {
// if the token wasn't found in any IT or IGC step, so it was wa
uses.addAll(
token.allUses(
ConstructUseTypeEnum.wa,
metadata,
ConstructUseTypeEnum.wa.pointValue,
),
);
continue;
}
return uses;
}
for (final step in choreo.choreoSteps) {
/// if 1) accepted match 2) token is in the replacement and 3) replacement
/// is in the overall step text, then token was a ga
final bool isAcceptedMatch =
step.acceptedOrIgnoredMatch?.status == PangeaMatchStatus.accepted;
// if the token was in an IT match, return no uses
if (step.itStep != null) return [];
// if this step was not accepted, continue
if (!isAcceptedMatch) continue;
if (isAcceptedMatch &&
step.acceptedOrIgnoredMatch?.match.choices != null) {
final choices = step.acceptedOrIgnoredMatch!.match.choices!;
final bool stepContainedToken = choices.any(
(choice) =>
// if this choice contains the token's content
choice.value.contains(content),
if (tokenStep.acceptedOrIgnoredMatch != null &&
tokenStep.acceptedOrIgnoredMatch?.status !=
PangeaMatchStatus.accepted) {
uses.addAll(
token.allUses(
ConstructUseTypeEnum.ga,
metadata,
0,
),
);
if (stepContainedToken) {
// give ga if IGC was used
uses.add(
token.toVocabUse(
ConstructUseTypeEnum.ga,
metadata,
),
);
continue;
}
uses.add(
OneConstructUse(
useType: ConstructUseTypeEnum.ga,
lemma: token.pos,
form: token.text.content,
category: "POS",
constructType: ConstructTypeEnum.morph,
metadata: metadata,
),
if (tokenStep.itStep != null) {
final selectedChoices = tokenStep.itStep!.continuances
.where((choice) => choice.wasClicked)
.length;
if (selectedChoices == 0) {
ErrorHandler.logError(
e: "No selected choices for IT step",
data: {
"token": token.text.content,
"step": tokenStep.toJson(),
},
);
continue;
}
final corITPoints = ConstructUseTypeEnum.corIt.pointValue;
final incITPoints = ConstructUseTypeEnum.incIt.pointValue;
final xp = max(
0,
corITPoints + (incITPoints * (selectedChoices - 1)),
);
for (final entry in token.morph.entries) {
uses.add(
OneConstructUse(
useType: ConstructUseTypeEnum.ga,
lemma: entry.value,
form: token.text.content,
category: entry.key,
constructType: ConstructTypeEnum.morph,
metadata: metadata,
),
);
}
return uses;
uses.addAll(
token.allUses(
ConstructUseTypeEnum.ta,
metadata,
xp,
),
);
} else if (tokenStep.acceptedOrIgnoredMatch!.match.choices != null) {
final selectedChoices = tokenStep.acceptedOrIgnoredMatch!.match.choices!
.where((choice) => choice.selected)
.length;
if (selectedChoices == 0) {
ErrorHandler.logError(
e: "No selected choices for IGC step",
data: {
"token": token.text.content,
"step": tokenStep.toJson(),
},
);
continue;
}
}
}
uses.add(
OneConstructUse(
useType: ConstructUseTypeEnum.wa,
lemma: token.pos,
form: token.text.content,
category: "POS",
constructType: ConstructTypeEnum.morph,
metadata: metadata,
),
);
final corIGCPoints = ConstructUseTypeEnum.corIGC.pointValue;
final incIGCPoints = ConstructUseTypeEnum.incIGC.pointValue;
final xp = max(
0,
corIGCPoints + (incIGCPoints * (selectedChoices - 1)),
);
// the token wasn't found in any IT or IGC step, so it was wa
for (final entry in token.morph.entries) {
uses.add(
OneConstructUse(
useType: ConstructUseTypeEnum.wa,
lemma: entry.value,
form: token.text.content,
category: entry.key,
constructType: ConstructTypeEnum.morph,
metadata: metadata,
),
);
}
if (lemma.saveVocab) {
uses.add(
token.toVocabUse(
ConstructUseTypeEnum.wa,
metadata,
),
);
uses.addAll(
token.allUses(
ConstructUseTypeEnum.ga,
metadata,
xp,
),
);
}
}
return uses;
}
}

@ -8,6 +8,7 @@ import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart';
import 'package:fluffychat/pangea/analytics_misc/construct_use_type_enum.dart';
import 'package:fluffychat/pangea/analytics_misc/constructs_model.dart';
import 'package:fluffychat/pangea/analytics_misc/put_analytics_controller.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
@ -103,13 +104,15 @@ class PracticeActivityModel {
// "onMultipleChoiceSelect: ${choice.form} ${responseUseType(choice)}",
// );
final constructUseType =
practiceTarget.record.responses.last.useType(activityType);
MatrixState.pangeaController.putAnalytics.setState(
AnalyticsStream(
eventId: event?.eventId,
roomId: event?.room.id,
constructs: [
OneConstructUse(
useType: practiceTarget.record.responses.last.useType(activityType),
useType: constructUseType,
lemma: choice.form.cId.lemma,
constructType: choice.form.cId.type,
metadata: ConstructUseMetaData(
@ -119,6 +122,7 @@ class PracticeActivityModel {
),
category: choice.form.cId.category,
form: choice.form.form,
xp: constructUseType.pointValue,
),
],
targetID: targetTokens.first.text.uniqueKey,
@ -179,14 +183,15 @@ class PracticeActivityModel {
// we don't take off points for incorrect emoji matches
if (ActivityTypeEnum.emoji != activityType || isCorrect) {
final constructUseType =
practiceTarget.record.responses.last.useType(activityType);
MatrixState.pangeaController.putAnalytics.setState(
AnalyticsStream(
eventId: event?.eventId,
roomId: event?.room.id,
constructs: [
OneConstructUse(
useType:
practiceTarget.record.responses.last.useType(activityType),
useType: constructUseType,
lemma: token.lemma.text,
constructType: ConstructTypeEnum.vocab,
metadata: ConstructUseMetaData(
@ -197,6 +202,7 @@ class PracticeActivityModel {
category: token.pos,
// in the case of a wrong answer, the cId doesn't match the token
form: token.text.content,
xp: constructUseType.pointValue,
),
],
targetID: token.text.uniqueKey,

@ -220,35 +220,41 @@ class ActivityRecordResponse {
case ActivityTypeEnum.wordFocusListening:
case ActivityTypeEnum.lemmaId:
final token = practiceActivity.targetTokens.first;
final constructUseType = useType(practiceActivity.activityType);
return [
OneConstructUse(
lemma: token.lemma.text,
form: token.text.content,
constructType: ConstructTypeEnum.vocab,
useType: useType(practiceActivity.activityType),
useType: constructUseType,
metadata: metadata,
category: token.pos,
xp: constructUseType.pointValue,
),
];
case ActivityTypeEnum.messageMeaning:
final constructUseType = useType(practiceActivity.activityType);
return practiceActivity.targetTokens
.expand(
(t) => t.allUses(
useType(practiceActivity.activityType),
constructUseType,
metadata,
constructUseType.pointValue,
),
)
.toList();
case ActivityTypeEnum.hiddenWordListening:
final constructUseType = useType(practiceActivity.activityType);
return practiceActivity.targetTokens
.map(
(token) => OneConstructUse(
lemma: token.lemma.text,
form: token.text.content,
constructType: ConstructTypeEnum.vocab,
useType: useType(practiceActivity.activityType),
useType: constructUseType,
metadata: metadata,
category: token.pos,
xp: constructUseType.pointValue,
),
)
.toList();
@ -273,13 +279,15 @@ class ActivityRecordResponse {
);
return null;
}
final constructUseType = useType(practiceActivity.activityType);
return OneConstructUse(
lemma: tag,
form: practiceActivity.targetTokens.first.text.content,
constructType: ConstructTypeEnum.morph,
useType: useType(practiceActivity.activityType),
useType: constructUseType,
metadata: metadata,
category: practiceActivity.morphFeature!,
xp: constructUseType.pointValue,
);
},
)

@ -1,6 +1,9 @@
import 'dart:developer';
import 'package:flutter/foundation.dart';
import 'package:collection/collection.dart';
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
import 'package:fluffychat/pangea/learning_settings/constants/language_constants.dart';
import 'package:fluffychat/pangea/lemmas/lemma_info_response.dart';
@ -9,7 +12,6 @@ import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart';
import 'package:fluffychat/pangea/practice_activities/practice_selection_repo.dart';
import 'package:fluffychat/pangea/practice_activities/practice_target.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:flutter/foundation.dart';
class PracticeSelection {
late String _userL2;
@ -42,8 +44,6 @@ class PracticeSelection {
_tokens.any((t) => t.lemma.saveVocab) &&
langCode.split("-")[0] == _userL2.split("-")[0];
String get messageText => PangeaToken.reconstructText(tokens);
Map<String, dynamic> toJson() => {
'createdAt': createdAt.toIso8601String(),
'lang_code': langCode,

@ -68,13 +68,6 @@ class MessageMorphInputBarContentState
super.didUpdateWidget(oldWidget);
}
String? get _correctChoice {
return widget.activity.multipleChoiceContent?.choices
.firstWhereOrNull((choice) {
return widget.activity.practiceTarget.wasCorrectChoice(choice) == true;
});
}
TextStyle? textStyle(BuildContext context) => overlay.maxWidth > 600
? Theme.of(context).textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.bold,

@ -114,10 +114,10 @@ packages:
dependency: "direct main"
description:
name: async
sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63
sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c"
url: "https://pub.dev"
source: hosted
version: "2.12.0"
version: "2.11.0"
audio_session:
dependency: transitive
description:
@ -218,10 +218,10 @@ packages:
dependency: transitive
description:
name: boolean_selector
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
version: "2.1.1"
cached_network_image:
dependency: "direct main"
description:
@ -266,10 +266,10 @@ packages:
dependency: "direct main"
description:
name: characters
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605"
url: "https://pub.dev"
source: hosted
version: "1.4.0"
version: "1.3.0"
charcode:
dependency: transitive
description:
@ -306,18 +306,18 @@ packages:
dependency: transitive
description:
name: clock
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf
url: "https://pub.dev"
source: hosted
version: "1.1.2"
version: "1.1.1"
collection:
dependency: "direct main"
description:
name: collection
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf
url: "https://pub.dev"
source: hosted
version: "1.19.1"
version: "1.19.0"
colorize:
dependency: transitive
description:
@ -506,10 +506,10 @@ packages:
dependency: transitive
description:
name: fake_async
sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc"
sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78"
url: "https://pub.dev"
source: hosted
version: "1.3.2"
version: "1.3.1"
fcm_shared_isolate:
dependency: "direct main"
description:
@ -529,10 +529,10 @@ packages:
dependency: transitive
description:
name: file
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c"
url: "https://pub.dev"
source: hosted
version: "7.0.1"
version: "7.0.0"
file_picker:
dependency: "direct main"
description:
@ -1390,18 +1390,18 @@ packages:
dependency: transitive
description:
name: leak_tracker
sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec
sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06"
url: "https://pub.dev"
source: hosted
version: "10.0.8"
version: "10.0.7"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573
sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379"
url: "https://pub.dev"
source: hosted
version: "3.0.9"
version: "3.0.8"
leak_tracker_testing:
dependency: transitive
description:
@ -1478,10 +1478,10 @@ packages:
dependency: transitive
description:
name: matcher
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb
url: "https://pub.dev"
source: hosted
version: "0.12.17"
version: "0.12.16+1"
material_color_utilities:
dependency: transitive
description:
@ -1511,10 +1511,10 @@ packages:
dependency: transitive
description:
name: meta
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7
url: "https://pub.dev"
source: hosted
version: "1.16.0"
version: "1.15.0"
mgrs_dart:
dependency: transitive
description:
@ -1695,10 +1695,10 @@ packages:
dependency: "direct main"
description:
name: path
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
url: "https://pub.dev"
source: hosted
version: "1.9.1"
version: "1.9.0"
path_parsing:
dependency: transitive
description:
@ -1823,10 +1823,10 @@ packages:
dependency: transitive
description:
name: platform
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65"
url: "https://pub.dev"
source: hosted
version: "3.1.6"
version: "3.1.5"
platform_detect:
dependency: transitive
description:
@ -1903,10 +1903,10 @@ packages:
dependency: transitive
description:
name: process
sha256: "107d8be718f120bbba9dcd1e95e3bd325b1b4a4f07db64154635ba03f2567a0d"
sha256: "21e54fd2faf1b5bdd5102afd25012184a6793927648ea81eea80552ac9405b32"
url: "https://pub.dev"
source: hosted
version: "5.0.3"
version: "5.0.2"
proj4dart:
dependency: transitive
description:
@ -2284,10 +2284,10 @@ packages:
dependency: transitive
description:
name: source_span
sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c"
sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
url: "https://pub.dev"
source: hosted
version: "1.10.1"
version: "1.10.0"
sprintf:
dependency: transitive
description:
@ -2364,26 +2364,26 @@ packages:
dependency: transitive
description:
name: stack_trace
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377"
url: "https://pub.dev"
source: hosted
version: "1.12.1"
version: "1.12.0"
stream_channel:
dependency: transitive
description:
name: stream_channel
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
url: "https://pub.dev"
source: hosted
version: "2.1.4"
version: "2.1.2"
string_scanner:
dependency: transitive
description:
name: string_scanner
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3"
url: "https://pub.dev"
source: hosted
version: "1.4.1"
version: "1.3.0"
string_validator:
dependency: transitive
description:
@ -2436,34 +2436,34 @@ packages:
dependency: transitive
description:
name: term_glyph
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84
url: "https://pub.dev"
source: hosted
version: "1.2.2"
version: "1.2.1"
test:
dependency: transitive
description:
name: test
sha256: "301b213cd241ca982e9ba50266bd3f5bd1ea33f1455554c5abb85d1be0e2d87e"
sha256: "713a8789d62f3233c46b4a90b174737b2c04cb6ae4500f2aa8b1be8f03f5e67f"
url: "https://pub.dev"
source: hosted
version: "1.25.15"
version: "1.25.8"
test_api:
dependency: transitive
description:
name: test_api
sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd
sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c"
url: "https://pub.dev"
source: hosted
version: "0.7.4"
version: "0.7.3"
test_core:
dependency: transitive
description:
name: test_core
sha256: "84d17c3486c8dfdbe5e12a50c8ae176d15e2a771b96909a9442b40173649ccaa"
sha256: "12391302411737c176b0b5d6491f466b0dd56d4763e347b6714efbaa74d7953d"
url: "https://pub.dev"
source: hosted
version: "0.6.8"
version: "0.6.5"
text_to_speech:
dependency: "direct main"
description:
@ -2757,10 +2757,10 @@ packages:
dependency: transitive
description:
name: vm_service
sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14"
sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b
url: "https://pub.dev"
source: hosted
version: "14.3.1"
version: "14.3.0"
wakelock_plus:
dependency: "direct main"
description:
@ -2890,5 +2890,5 @@ packages:
source: hosted
version: "3.1.3"
sdks:
dart: ">=3.7.0-0 <4.0.0"
dart: ">=3.6.0 <4.0.0"
flutter: ">=3.27.0"

Loading…
Cancel
Save