grammar error practice UI elements
This commit is contained in:
parent
33b05f6f24
commit
8fb41cdc7a
12 changed files with 374 additions and 77 deletions
|
|
@ -5053,5 +5053,6 @@
|
|||
"constructUseCorGCDesc": "Correct grammar category practice",
|
||||
"constructUseIncGCDesc": "Incorrect grammar category practice",
|
||||
"constructUseCorGEDesc": "Correct grammar error practice",
|
||||
"constructUseIncGEDesc": "Incorrect grammar error practice"
|
||||
"constructUseIncGEDesc": "Incorrect grammar error practice",
|
||||
"fillInBlank": "Fill in the blank with the correct choice"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import 'package:fluffychat/pangea/analytics_data/level_up_analytics_service.dart
|
|||
import 'package:fluffychat/pangea/analytics_misc/client_analytics_extension.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/construct_use_model.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/construct_use_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/constructs_event.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/constructs_model.dart';
|
||||
import 'package:fluffychat/pangea/analytics_settings/analytics_settings_extension.dart';
|
||||
|
|
@ -233,12 +234,14 @@ class AnalyticsDataService {
|
|||
int? count,
|
||||
String? roomId,
|
||||
DateTime? since,
|
||||
ConstructUseTypeEnum? type,
|
||||
}) async {
|
||||
await _ensureInitialized();
|
||||
final uses = await _analyticsClientGetter.database.getUses(
|
||||
count: count,
|
||||
roomId: roomId,
|
||||
since: since,
|
||||
type: type,
|
||||
);
|
||||
|
||||
final blocked = blockedConstructs;
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import 'package:synchronized/synchronized.dart';
|
|||
import 'package:fluffychat/pangea/analytics_data/derived_analytics_data_model.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/construct_use_model.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/construct_use_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/constructs_event.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/constructs_model.dart';
|
||||
import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
|
||||
|
|
@ -197,6 +198,7 @@ class AnalyticsDatabase with DatabaseFileStorage {
|
|||
int? count,
|
||||
String? roomId,
|
||||
DateTime? since,
|
||||
ConstructUseTypeEnum? type,
|
||||
}) async {
|
||||
final stopwatch = Stopwatch()..start();
|
||||
final results = <OneConstructUse>[];
|
||||
|
|
@ -208,6 +210,9 @@ class AnalyticsDatabase with DatabaseFileStorage {
|
|||
if (roomId != null && use.metadata.roomId != roomId) {
|
||||
return true; // skip but continue
|
||||
}
|
||||
if (type != null && use.useType != type) {
|
||||
return true; // skip but continue
|
||||
}
|
||||
|
||||
results.add(use);
|
||||
return count == null || results.length < count;
|
||||
|
|
|
|||
|
|
@ -26,18 +26,28 @@ import 'package:fluffychat/pangea/toolbar/message_practice/practice_record_contr
|
|||
import 'package:fluffychat/widgets/future_loading_dialog.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
class PracticeChoice {
|
||||
class VocabPracticeChoice {
|
||||
final String choiceId;
|
||||
final String choiceText;
|
||||
final String? choiceEmoji;
|
||||
|
||||
const PracticeChoice({
|
||||
const VocabPracticeChoice({
|
||||
required this.choiceId,
|
||||
required this.choiceText,
|
||||
this.choiceEmoji,
|
||||
});
|
||||
}
|
||||
|
||||
class _PracticeQueueEntry {
|
||||
final MessageActivityRequest request;
|
||||
final Completer<MultipleChoicePracticeActivityModel> completer;
|
||||
|
||||
_PracticeQueueEntry({
|
||||
required this.request,
|
||||
required this.completer,
|
||||
});
|
||||
}
|
||||
|
||||
class SessionLoader extends AsyncLoader<AnalyticsPracticeSessionModel> {
|
||||
final ConstructTypeEnum type;
|
||||
SessionLoader({required this.type});
|
||||
|
|
@ -67,12 +77,10 @@ class AnalyticsPracticeState extends State<AnalyticsPractice>
|
|||
final ValueNotifier<AsyncState<MultipleChoicePracticeActivityModel>>
|
||||
activityState = ValueNotifier(const AsyncState.idle());
|
||||
|
||||
final Queue<
|
||||
MapEntry<PracticeTarget,
|
||||
Completer<MultipleChoicePracticeActivityModel>>> _queue = Queue();
|
||||
final Queue<_PracticeQueueEntry> _queue = Queue();
|
||||
|
||||
final ValueNotifier<PracticeTarget?> activityTarget =
|
||||
ValueNotifier<PracticeTarget?>(null);
|
||||
final ValueNotifier<MessageActivityRequest?> activityTarget =
|
||||
ValueNotifier<MessageActivityRequest?>(null);
|
||||
|
||||
final ValueNotifier<double> progressNotifier = ValueNotifier<double>(0.0);
|
||||
|
||||
|
|
@ -116,13 +124,13 @@ class AnalyticsPracticeState extends State<AnalyticsPractice>
|
|||
AnalyticsDataService get _analyticsService =>
|
||||
Matrix.of(context).analyticsDataService;
|
||||
|
||||
List<PracticeChoice> filteredChoices(
|
||||
List<VocabPracticeChoice> filteredChoices(
|
||||
MultipleChoicePracticeActivityModel activity,
|
||||
) {
|
||||
final content = activity.multipleChoiceContent;
|
||||
final choices = content.choices.toList();
|
||||
final answer = content.answers.first;
|
||||
final filtered = <PracticeChoice>[];
|
||||
final filtered = <VocabPracticeChoice>[];
|
||||
|
||||
final seenTexts = <String>{};
|
||||
for (final id in choices) {
|
||||
|
|
@ -137,7 +145,7 @@ class AnalyticsPracticeState extends State<AnalyticsPractice>
|
|||
(choice) => choice.choiceText == text,
|
||||
);
|
||||
if (index != -1) {
|
||||
filtered[index] = PracticeChoice(
|
||||
filtered[index] = VocabPracticeChoice(
|
||||
choiceId: id,
|
||||
choiceText: text,
|
||||
choiceEmoji: getChoiceEmoji(activity.storageKey, id),
|
||||
|
|
@ -148,7 +156,7 @@ class AnalyticsPracticeState extends State<AnalyticsPractice>
|
|||
|
||||
seenTexts.add(text);
|
||||
filtered.add(
|
||||
PracticeChoice(
|
||||
VocabPracticeChoice(
|
||||
choiceId: id,
|
||||
choiceText: text,
|
||||
choiceEmoji: getChoiceEmoji(activity.storageKey, id),
|
||||
|
|
@ -202,7 +210,7 @@ class AnalyticsPracticeState extends State<AnalyticsPractice>
|
|||
if (activityTarget.value == null) return;
|
||||
if (widget.type != ConstructTypeEnum.vocab) return;
|
||||
TtsController.tryToSpeak(
|
||||
activityTarget.value!.tokens.first.vocabConstructID.lemma,
|
||||
activityTarget.value!.target.tokens.first.vocabConstructID.lemma,
|
||||
langCode: MatrixState.pangeaController.userController.userL2!.langCode,
|
||||
);
|
||||
}
|
||||
|
|
@ -275,10 +283,10 @@ class AnalyticsPracticeState extends State<AnalyticsPractice>
|
|||
activityState.value = const AsyncState.loading();
|
||||
final nextActivityCompleter = _queue.removeFirst();
|
||||
|
||||
activityTarget.value = nextActivityCompleter.key;
|
||||
activityTarget.value = nextActivityCompleter.request;
|
||||
_playAudio();
|
||||
|
||||
final activity = await nextActivityCompleter.value.future;
|
||||
final activity = await nextActivityCompleter.completer.future;
|
||||
activityState.value = AsyncState.loaded(activity);
|
||||
}
|
||||
} catch (e) {
|
||||
|
|
@ -298,7 +306,7 @@ class AnalyticsPracticeState extends State<AnalyticsPractice>
|
|||
activityState.value = const AsyncState.loading();
|
||||
final req = requests.first;
|
||||
|
||||
activityTarget.value = req.target;
|
||||
activityTarget.value = req;
|
||||
_playAudio();
|
||||
|
||||
final res = await _fetchActivity(req);
|
||||
|
|
@ -314,10 +322,17 @@ class AnalyticsPracticeState extends State<AnalyticsPractice>
|
|||
_fillActivityQueue(requests.skip(1).toList());
|
||||
}
|
||||
|
||||
Future<void> _fillActivityQueue(List<MessageActivityRequest> requests) async {
|
||||
Future<void> _fillActivityQueue(
|
||||
List<MessageActivityRequest> requests,
|
||||
) async {
|
||||
for (final request in requests) {
|
||||
final completer = Completer<MultipleChoicePracticeActivityModel>();
|
||||
_queue.add(MapEntry(request.target, completer));
|
||||
_queue.add(
|
||||
_PracticeQueueEntry(
|
||||
request: request,
|
||||
completer: completer,
|
||||
),
|
||||
);
|
||||
|
||||
try {
|
||||
final res = await _fetchActivity(request);
|
||||
|
|
|
|||
|
|
@ -6,9 +6,32 @@ import 'package:fluffychat/pangea/analytics_practice/analytics_practice_constant
|
|||
import 'package:fluffychat/pangea/practice_activities/message_activity_request.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/practice_target.dart';
|
||||
|
||||
class AnalyticsActivityTarget {
|
||||
final PracticeTarget target;
|
||||
final GrammarErrorRequestInfo? grammarErrorInfo;
|
||||
|
||||
AnalyticsActivityTarget({
|
||||
required this.target,
|
||||
this.grammarErrorInfo,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'target': target.toJson(),
|
||||
'grammarErrorInfo': grammarErrorInfo?.toJson(),
|
||||
};
|
||||
|
||||
factory AnalyticsActivityTarget.fromJson(Map<String, dynamic> json) =>
|
||||
AnalyticsActivityTarget(
|
||||
target: PracticeTarget.fromJson(json['target']),
|
||||
grammarErrorInfo: json['grammarErrorInfo'] != null
|
||||
? GrammarErrorRequestInfo.fromJson(json['grammarErrorInfo'])
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
class AnalyticsPracticeSessionModel {
|
||||
final DateTime startedAt;
|
||||
final List<PracticeTarget> practiceTargets;
|
||||
final List<AnalyticsActivityTarget> practiceTargets;
|
||||
final String userL1;
|
||||
final String userL2;
|
||||
|
||||
|
|
@ -38,7 +61,8 @@ class AnalyticsPracticeSessionModel {
|
|||
userL1: userL1,
|
||||
userL2: userL2,
|
||||
activityQualityFeedback: null,
|
||||
target: target,
|
||||
target: target.target,
|
||||
grammarErrorInfo: target.grammarErrorInfo,
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
|
|
@ -59,8 +83,8 @@ class AnalyticsPracticeSessionModel {
|
|||
return AnalyticsPracticeSessionModel(
|
||||
startedAt: DateTime.parse(json['startedAt'] as String),
|
||||
practiceTargets: (json['practiceTargets'] as List<dynamic>)
|
||||
.map((e) => PracticeTarget.fromJson(e))
|
||||
.whereType<PracticeTarget>()
|
||||
.map((e) => AnalyticsActivityTarget.fromJson(e))
|
||||
.whereType<AnalyticsActivityTarget>()
|
||||
.toList(),
|
||||
userL1: json['userL1'] as String,
|
||||
userL2: json['userL2'] as String,
|
||||
|
|
|
|||
|
|
@ -1,14 +1,17 @@
|
|||
import 'dart:math';
|
||||
|
||||
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_practice/analytics_practice_constants.dart';
|
||||
import 'package:fluffychat/pangea/analytics_practice/analytics_practice_session_model.dart';
|
||||
import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
|
||||
import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.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 'package:fluffychat/pangea/morphs/morph_features_enum.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/message_activity_request.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/practice_target.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
|
|
@ -24,28 +27,23 @@ class AnalyticsPracticeSessionRepo {
|
|||
(_) => activityTypes[r.nextInt(activityTypes.length)],
|
||||
);
|
||||
|
||||
final List<PracticeTarget> targets = [];
|
||||
final List<AnalyticsActivityTarget> targets = [];
|
||||
|
||||
if (type == ConstructTypeEnum.vocab) {
|
||||
final constructs = await _fetchVocab();
|
||||
final targetCount = min(constructs.length, types.length);
|
||||
targets.addAll([
|
||||
for (var i = 0; i < targetCount; i++)
|
||||
PracticeTarget(
|
||||
tokens: [constructs[i].asToken],
|
||||
activityType: types[i],
|
||||
AnalyticsActivityTarget(
|
||||
target: PracticeTarget(
|
||||
tokens: [constructs[i].asToken],
|
||||
activityType: types[i],
|
||||
),
|
||||
),
|
||||
]);
|
||||
} else {
|
||||
final morphs = await _fetchMorphs();
|
||||
targets.addAll([
|
||||
for (final entry in morphs.entries)
|
||||
PracticeTarget(
|
||||
tokens: [entry.key],
|
||||
activityType: types[targets.length],
|
||||
morphFeature: entry.value,
|
||||
),
|
||||
]);
|
||||
final errorTargets = await _fetchErrors();
|
||||
targets.addAll(errorTargets);
|
||||
}
|
||||
|
||||
final session = AnalyticsPracticeSessionModel(
|
||||
|
|
@ -144,4 +142,99 @@ class AnalyticsPracticeSessionRepo {
|
|||
|
||||
return targets;
|
||||
}
|
||||
|
||||
static Future<List<AnalyticsActivityTarget>> _fetchErrors() async {
|
||||
final uses = await MatrixState
|
||||
.pangeaController.matrixState.analyticsDataService
|
||||
.getUses(count: 100, type: ConstructUseTypeEnum.ga);
|
||||
|
||||
final client = MatrixState.pangeaController.matrixState.client;
|
||||
final Map<String, PangeaMessageEvent?> idsToEvents = {};
|
||||
|
||||
for (final use in uses) {
|
||||
final eventID = use.metadata.eventId;
|
||||
if (eventID == null || idsToEvents.containsKey(eventID)) continue;
|
||||
|
||||
final roomID = use.metadata.roomId;
|
||||
if (roomID == null) {
|
||||
idsToEvents[eventID] = null;
|
||||
continue;
|
||||
}
|
||||
|
||||
final room = client.getRoomById(roomID);
|
||||
final event = await room?.getEventById(eventID);
|
||||
if (event == null || event.redacted) {
|
||||
idsToEvents[eventID] = null;
|
||||
continue;
|
||||
}
|
||||
|
||||
final timeline = await room!.getTimeline();
|
||||
idsToEvents[eventID] = PangeaMessageEvent(
|
||||
event: event,
|
||||
timeline: timeline,
|
||||
ownMessage: event.senderId == client.userID,
|
||||
);
|
||||
}
|
||||
|
||||
final l2Code =
|
||||
MatrixState.pangeaController.userController.userL2!.langCodeShort;
|
||||
|
||||
final events = idsToEvents.values.whereType<PangeaMessageEvent>().toList();
|
||||
final eventsWithContent = events.where((e) {
|
||||
final originalSent = e.originalSent;
|
||||
final choreo = originalSent?.choreo;
|
||||
final tokens = originalSent?.tokens;
|
||||
return originalSent?.langCode.split("-").first == l2Code &&
|
||||
choreo != null &&
|
||||
tokens != null &&
|
||||
tokens.isNotEmpty &&
|
||||
choreo.choreoSteps.any(
|
||||
(step) =>
|
||||
step.acceptedOrIgnoredMatch?.isGrammarMatch == true &&
|
||||
step.acceptedOrIgnoredMatch?.match.bestChoice != null,
|
||||
);
|
||||
});
|
||||
|
||||
final targets = <AnalyticsActivityTarget>[];
|
||||
for (final event in eventsWithContent) {
|
||||
final originalSent = event.originalSent!;
|
||||
final choreo = originalSent.choreo!;
|
||||
final tokens = originalSent.tokens!;
|
||||
|
||||
for (int i = 0; i < choreo.choreoSteps.length; i++) {
|
||||
final step = choreo.choreoSteps[i];
|
||||
final igcMatch = step.acceptedOrIgnoredMatch;
|
||||
if (igcMatch?.isGrammarMatch != true ||
|
||||
igcMatch?.match.bestChoice == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final choices = igcMatch!.match.choices!.map((c) => c.value).toList();
|
||||
final choiceTokens = tokens.where(
|
||||
(token) =>
|
||||
token.lemma.saveVocab &&
|
||||
choices.any(
|
||||
(choice) => choice.contains(token.text.content),
|
||||
),
|
||||
);
|
||||
|
||||
targets.add(
|
||||
AnalyticsActivityTarget(
|
||||
target: PracticeTarget(
|
||||
tokens: choiceTokens.toList(),
|
||||
activityType: ActivityTypeEnum.grammarError,
|
||||
morphFeature: null,
|
||||
),
|
||||
grammarErrorInfo: GrammarErrorRequestInfo(
|
||||
choreo: choreo,
|
||||
stepIndex: i,
|
||||
eventID: event.eventId,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return targets;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -143,8 +143,8 @@ class _AnalyticsActivityView extends StatelessWidget {
|
|||
if (controller.widget.type ==
|
||||
ConstructTypeEnum.vocab)
|
||||
PhoneticTranscriptionWidget(
|
||||
text:
|
||||
target.tokens.first.vocabConstructID.lemma,
|
||||
text: target
|
||||
.target.tokens.first.vocabConstructID.lemma,
|
||||
textLanguage: MatrixState
|
||||
.pangeaController.userController.userL2!,
|
||||
style: const TextStyle(fontSize: 14.0),
|
||||
|
|
@ -157,13 +157,8 @@ class _AnalyticsActivityView extends StatelessWidget {
|
|||
Expanded(
|
||||
flex: 2,
|
||||
child: Center(
|
||||
child: ValueListenableBuilder(
|
||||
valueListenable: controller.activityTarget,
|
||||
builder: (context, target, __) => target != null
|
||||
? _ExampleMessageWidget(
|
||||
controller.getExampleMessage(target),
|
||||
)
|
||||
: const SizedBox(),
|
||||
child: _AnalyticsPracticeCenterContent(
|
||||
controller: controller,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
@ -179,6 +174,36 @@ class _AnalyticsActivityView extends StatelessWidget {
|
|||
}
|
||||
}
|
||||
|
||||
class _AnalyticsPracticeCenterContent extends StatelessWidget {
|
||||
final AnalyticsPracticeState controller;
|
||||
|
||||
const _AnalyticsPracticeCenterContent({
|
||||
required this.controller,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ValueListenableBuilder(
|
||||
valueListenable: controller.activityTarget,
|
||||
builder: (context, target, __) => switch (target?.target.activityType) {
|
||||
null => const SizedBox(),
|
||||
ActivityTypeEnum.grammarError => ValueListenableBuilder(
|
||||
valueListenable: controller.activityState,
|
||||
builder: (context, state, __) => switch (state) {
|
||||
AsyncLoaded(value: final activity) => _ErrorBlankWidget(
|
||||
activity: activity as GrammarErrorPracticeActivityModel,
|
||||
),
|
||||
_ => const SizedBox(),
|
||||
},
|
||||
),
|
||||
_ => _ExampleMessageWidget(
|
||||
controller.getExampleMessage(target!.target),
|
||||
),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ExampleMessageWidget extends StatelessWidget {
|
||||
final Future<List<InlineSpan>?> future;
|
||||
|
||||
|
|
@ -220,6 +245,62 @@ class _ExampleMessageWidget extends StatelessWidget {
|
|||
}
|
||||
}
|
||||
|
||||
class _ErrorBlankWidget extends StatelessWidget {
|
||||
final GrammarErrorPracticeActivityModel activity;
|
||||
|
||||
const _ErrorBlankWidget({
|
||||
required this.activity,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final text = activity.text;
|
||||
final errorOffset = activity.errorOffset;
|
||||
final errorLength = activity.errorLength;
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 8,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Color.alphaBlend(
|
||||
Colors.white.withAlpha(180),
|
||||
ThemeData.dark().colorScheme.primary,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: RichText(
|
||||
text: TextSpan(
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onPrimaryFixed,
|
||||
fontSize: AppConfig.fontSizeFactor * AppConfig.messageFontSize,
|
||||
),
|
||||
children: [
|
||||
if (errorOffset > 0)
|
||||
TextSpan(text: text.characters.take(errorOffset).toString()),
|
||||
WidgetSpan(
|
||||
child: Container(
|
||||
height: 4.0,
|
||||
width: (errorLength * 8).toDouble(),
|
||||
padding: const EdgeInsets.only(bottom: 2.0),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (errorOffset + errorLength < text.length)
|
||||
TextSpan(
|
||||
text:
|
||||
text.characters.skip(errorOffset + errorLength).toString(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ActivityChoicesWidget extends StatelessWidget {
|
||||
final AnalyticsPracticeState controller;
|
||||
|
||||
|
|
@ -366,6 +447,20 @@ class _ChoiceCard extends StatelessWidget {
|
|||
isCorrect: isCorrect,
|
||||
);
|
||||
|
||||
case ActivityTypeEnum.grammarError:
|
||||
final activity = this.activity as GrammarErrorPracticeActivityModel;
|
||||
return GameChoiceCard(
|
||||
key: ValueKey(
|
||||
'${activity.errorLength}_${activity.errorOffset}_${activity.eventID}_${activityType.name}_grammar_error_$choiceId',
|
||||
),
|
||||
shouldFlip: false,
|
||||
targetId: targetId,
|
||||
onPressed: onPressed,
|
||||
isCorrect: isCorrect,
|
||||
height: cardHeight,
|
||||
child: Text(choiceText),
|
||||
);
|
||||
|
||||
default:
|
||||
return GameChoiceCard(
|
||||
key: ValueKey(
|
||||
|
|
|
|||
|
|
@ -1,41 +1,50 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/practice_activities/message_activity_request.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/multiple_choice_activity_model.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/practice_activity_model.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
class GrammarErrorPracticeGenerator {
|
||||
static Future<MessageActivityResponse> get(
|
||||
MessageActivityRequest req,
|
||||
) async {
|
||||
final igcMatch = target.igcMatch;
|
||||
assert(igcMatch.bestChoice != null, 'IGC match must have a best choice');
|
||||
assert(igcMatch.choices != null, 'IGC match must have choices');
|
||||
assert(
|
||||
req.grammarErrorInfo != null,
|
||||
'Grammar error info must be provided for grammar error practice',
|
||||
);
|
||||
|
||||
final errorSpan = igcMatch.errorSpan;
|
||||
final correctChoice = igcMatch.bestChoice!.value;
|
||||
final choreo = req.grammarErrorInfo!.choreo;
|
||||
final stepIndex = req.grammarErrorInfo!.stepIndex;
|
||||
final eventID = req.grammarErrorInfo!.eventID;
|
||||
|
||||
final igcMatch =
|
||||
choreo.choreoSteps[stepIndex].acceptedOrIgnoredMatch?.match;
|
||||
assert(igcMatch?.choices != null, 'IGC match must have choices');
|
||||
assert(igcMatch?.bestChoice != null, 'IGC match must have a best choice');
|
||||
|
||||
final correctChoice = igcMatch!.bestChoice!.value;
|
||||
final choices = igcMatch.choices!.map((c) => c.value).toList();
|
||||
|
||||
final choiceTokens = target.tokens.where(
|
||||
(token) => choices.any(
|
||||
(choice) => choice.contains(token.text.content),
|
||||
),
|
||||
);
|
||||
|
||||
assert(
|
||||
choiceTokens.isNotEmpty,
|
||||
'At least one token should match the error choices',
|
||||
);
|
||||
final stepText = choreo.stepText(stepIndex: stepIndex - 1);
|
||||
final errorSpan = stepText.characters
|
||||
.skip(igcMatch.offset)
|
||||
.take(igcMatch.length)
|
||||
.toString();
|
||||
|
||||
choices.add(errorSpan);
|
||||
choices.shuffle();
|
||||
return MessageActivityResponse(
|
||||
activity: GrammarErrorPracticeActivityModel(
|
||||
tokens: choiceTokens.toList(),
|
||||
tokens: req.target.tokens,
|
||||
langCode: req.userL2,
|
||||
multipleChoiceContent: MultipleChoiceActivity(
|
||||
choices: choices.toSet(),
|
||||
answers: {correctChoice},
|
||||
),
|
||||
text: stepText,
|
||||
errorOffset: igcMatch.offset,
|
||||
errorLength: igcMatch.length,
|
||||
eventID: eventID,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,11 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
|
||||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/choreo_record_model.dart';
|
||||
import 'package:fluffychat/pangea/morphs/morph_features_enum.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/practice_activity_model.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/practice_target.dart';
|
||||
|
||||
|
|
@ -35,23 +41,67 @@ class ActivityQualityFeedback {
|
|||
}
|
||||
}
|
||||
|
||||
class GrammarErrorRequestInfo {
|
||||
final ChoreoRecordModel choreo;
|
||||
final int stepIndex;
|
||||
final String eventID;
|
||||
|
||||
const GrammarErrorRequestInfo({
|
||||
required this.choreo,
|
||||
required this.stepIndex,
|
||||
required this.eventID,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'choreo': choreo.toJson(),
|
||||
'step_index': stepIndex,
|
||||
'event_id': eventID,
|
||||
};
|
||||
}
|
||||
|
||||
factory GrammarErrorRequestInfo.fromJson(Map<String, dynamic> json) {
|
||||
return GrammarErrorRequestInfo(
|
||||
choreo: ChoreoRecordModel.fromJson(json['choreo']),
|
||||
stepIndex: json['step_index'] as int,
|
||||
eventID: json['event_id'] as String,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MessageActivityRequest {
|
||||
final String userL1;
|
||||
final String userL2;
|
||||
final PracticeTarget target;
|
||||
final ActivityQualityFeedback? activityQualityFeedback;
|
||||
final GrammarErrorRequestInfo? grammarErrorInfo;
|
||||
|
||||
MessageActivityRequest({
|
||||
required this.userL1,
|
||||
required this.userL2,
|
||||
required this.activityQualityFeedback,
|
||||
required this.target,
|
||||
this.grammarErrorInfo,
|
||||
}) {
|
||||
if (target.tokens.isEmpty) {
|
||||
throw Exception('Target tokens must not be empty');
|
||||
}
|
||||
}
|
||||
|
||||
String promptText(BuildContext context) {
|
||||
switch (target.activityType) {
|
||||
case ActivityTypeEnum.grammarCategory:
|
||||
return L10n.of(context).whatIsTheMorphTag(
|
||||
target.morphFeature!.getDisplayCopy(context),
|
||||
target.tokens.first.text.content,
|
||||
);
|
||||
case ActivityTypeEnum.grammarError:
|
||||
return L10n.of(context).fillInBlank;
|
||||
default:
|
||||
return target.tokens.first.vocabConstructID.lemma;
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'user_l1': userL1,
|
||||
|
|
@ -60,6 +110,7 @@ class MessageActivityRequest {
|
|||
'target_tokens': target.tokens.map((e) => e.toJson()).toList(),
|
||||
'target_type': target.activityType.name,
|
||||
'target_morph_feature': target.morphFeature,
|
||||
'grammar_error_info': grammarErrorInfo?.toJson(),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -72,7 +123,8 @@ class MessageActivityRequest {
|
|||
other.userL2 == userL2 &&
|
||||
other.target == target &&
|
||||
other.activityQualityFeedback?.feedbackText ==
|
||||
activityQualityFeedback?.feedbackText;
|
||||
activityQualityFeedback?.feedbackText &&
|
||||
other.grammarErrorInfo == grammarErrorInfo;
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -80,7 +132,8 @@ class MessageActivityRequest {
|
|||
return activityQualityFeedback.hashCode ^
|
||||
target.hashCode ^
|
||||
userL1.hashCode ^
|
||||
userL2.hashCode;
|
||||
userL2.hashCode ^
|
||||
grammarErrorInfo.hashCode;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -354,10 +354,19 @@ class LemmaPracticeActivityModel extends MultipleChoicePracticeActivityModel {
|
|||
|
||||
class GrammarErrorPracticeActivityModel
|
||||
extends MultipleChoicePracticeActivityModel {
|
||||
final String text;
|
||||
final int errorOffset;
|
||||
final int errorLength;
|
||||
final String eventID;
|
||||
|
||||
GrammarErrorPracticeActivityModel({
|
||||
required super.tokens,
|
||||
required super.langCode,
|
||||
required super.multipleChoiceContent,
|
||||
required this.text,
|
||||
required this.errorOffset,
|
||||
required this.errorLength,
|
||||
required this.eventID,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -130,6 +130,10 @@ class PracticeRepo {
|
|||
case ActivityTypeEnum.grammarCategory:
|
||||
return MorphCategoryActivityGenerator.get(req);
|
||||
case ActivityTypeEnum.grammarError:
|
||||
assert(
|
||||
req.grammarErrorInfo != null,
|
||||
'Grammar error info must be provided for grammar error activities',
|
||||
);
|
||||
return GrammarErrorPracticeGenerator.get(req);
|
||||
case ActivityTypeEnum.morphId:
|
||||
return MorphActivityGenerator.get(req);
|
||||
|
|
|
|||
|
|
@ -1,9 +1,7 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
|
||||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
|
||||
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
|
||||
import 'package:fluffychat/pangea/morphs/morph_features_enum.dart';
|
||||
|
|
@ -85,18 +83,6 @@ class PracticeTarget {
|
|||
(morphFeature?.name ?? "");
|
||||
}
|
||||
|
||||
String promptText(BuildContext context) {
|
||||
switch (activityType) {
|
||||
case ActivityTypeEnum.grammarCategory:
|
||||
return L10n.of(context).whatIsTheMorphTag(
|
||||
morphFeature!.getDisplayCopy(context),
|
||||
tokens.first.text.content,
|
||||
);
|
||||
default:
|
||||
return tokens.first.vocabConstructID.lemma;
|
||||
}
|
||||
}
|
||||
|
||||
ConstructIdentifier targetTokenConstructID(PangeaToken token) {
|
||||
final defaultID = token.vocabConstructID;
|
||||
final ConstructIdentifier? cId = morphFeature == null
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue