word focus turned off and tts on Choice click (#1118)
* word focus turned off and tts on Choice click * play audio on word selection
This commit is contained in:
parent
25ab5e54bc
commit
78cb3afe0b
15 changed files with 198 additions and 94 deletions
|
|
@ -140,6 +140,7 @@ class ChatController extends State<ChatPageWithRoom>
|
||||||
Timer? typingTimeout;
|
Timer? typingTimeout;
|
||||||
bool currentlyTyping = false;
|
bool currentlyTyping = false;
|
||||||
// #Pangea
|
// #Pangea
|
||||||
|
|
||||||
// bool dragging = false;
|
// bool dragging = false;
|
||||||
|
|
||||||
// void onDragEntered(_) => setState(() => dragging = true);
|
// void onDragEntered(_) => setState(() => dragging = true);
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ import 'package:fluffychat/pangea/models/tokens_event_content_model.dart';
|
||||||
import 'package:fluffychat/pangea/utils/any_state_holder.dart';
|
import 'package:fluffychat/pangea/utils/any_state_holder.dart';
|
||||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||||
import 'package:fluffychat/pangea/utils/overlay.dart';
|
import 'package:fluffychat/pangea/utils/overlay.dart';
|
||||||
|
import 'package:fluffychat/pangea/widgets/chat/tts_controller.dart';
|
||||||
import 'package:fluffychat/pangea/widgets/igc/paywall_card.dart';
|
import 'package:fluffychat/pangea/widgets/igc/paywall_card.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
@ -40,6 +41,7 @@ class Choreographer {
|
||||||
late IgcController igc;
|
late IgcController igc;
|
||||||
late AlternativeTranslator altTranslator;
|
late AlternativeTranslator altTranslator;
|
||||||
late ErrorService errorService;
|
late ErrorService errorService;
|
||||||
|
final tts = TtsController();
|
||||||
|
|
||||||
bool isFetching = false;
|
bool isFetching = false;
|
||||||
int _timesClicked = 0;
|
int _timesClicked = 0;
|
||||||
|
|
@ -66,6 +68,8 @@ class Choreographer {
|
||||||
.subscriptionController.trialActivationStream.stream
|
.subscriptionController.trialActivationStream.stream
|
||||||
.listen((_) => _onChangeListener);
|
.listen((_) => _onChangeListener);
|
||||||
|
|
||||||
|
tts.setupTTS();
|
||||||
|
|
||||||
clear();
|
clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -454,6 +458,7 @@ class Choreographer {
|
||||||
choreoRecord = ChoreoRecord.newRecord;
|
choreoRecord = ChoreoRecord.newRecord;
|
||||||
itController.clear();
|
itController.clear();
|
||||||
igc.clear();
|
igc.clear();
|
||||||
|
//@ggurdin - why is this commented out?
|
||||||
// errorService.clear();
|
// errorService.clear();
|
||||||
_resetDebounceTimer();
|
_resetDebounceTimer();
|
||||||
}
|
}
|
||||||
|
|
@ -477,6 +482,7 @@ class Choreographer {
|
||||||
dispose() {
|
dispose() {
|
||||||
_textController.dispose();
|
_textController.dispose();
|
||||||
trialStream?.cancel();
|
trialStream?.cancel();
|
||||||
|
tts.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
LanguageModel? get l2Lang {
|
LanguageModel? get l2Lang {
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import 'dart:developer';
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:fluffychat/pangea/widgets/chat/tts_controller.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||||
|
|
@ -20,6 +21,10 @@ class ChoicesArray extends StatefulWidget {
|
||||||
final String originalSpan;
|
final String originalSpan;
|
||||||
final String Function(int) uniqueKeyForLayerLink;
|
final String Function(int) uniqueKeyForLayerLink;
|
||||||
|
|
||||||
|
/// If null then should not be used
|
||||||
|
/// We don't want tts in the case of L1 options
|
||||||
|
final TtsController? tts;
|
||||||
|
|
||||||
/// Used to unqiuely identify the keys for choices, in cases where multiple
|
/// Used to unqiuely identify the keys for choices, in cases where multiple
|
||||||
/// choices could have identical text, like in back-to-back practice activities
|
/// choices could have identical text, like in back-to-back practice activities
|
||||||
final String? id;
|
final String? id;
|
||||||
|
|
@ -35,6 +40,7 @@ class ChoicesArray extends StatefulWidget {
|
||||||
required this.originalSpan,
|
required this.originalSpan,
|
||||||
required this.uniqueKeyForLayerLink,
|
required this.uniqueKeyForLayerLink,
|
||||||
required this.selectedChoiceIndex,
|
required this.selectedChoiceIndex,
|
||||||
|
required this.tts,
|
||||||
this.isActive = true,
|
this.isActive = true,
|
||||||
this.onLongPress,
|
this.onLongPress,
|
||||||
this.id,
|
this.id,
|
||||||
|
|
@ -73,7 +79,11 @@ class ChoicesArrayState extends State<ChoicesArray> {
|
||||||
theme: theme,
|
theme: theme,
|
||||||
onLongPress: widget.isActive ? widget.onLongPress : null,
|
onLongPress: widget.isActive ? widget.onLongPress : null,
|
||||||
onPressed: widget.isActive
|
onPressed: widget.isActive
|
||||||
? widget.onPressed
|
? (String value, int index) {
|
||||||
|
widget.onPressed(value, index);
|
||||||
|
// TODO - what to pass here as eventID?
|
||||||
|
widget.tts?.tryToSpeak(value, context, null);
|
||||||
|
}
|
||||||
: (String value, int index) {
|
: (String value, int index) {
|
||||||
debugger(when: kDebugMode);
|
debugger(when: kDebugMode);
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -418,6 +418,7 @@ class ITChoices extends StatelessWidget {
|
||||||
onLongPress: (value, index) => showCard(context, index),
|
onLongPress: (value, index) => showCard(context, index),
|
||||||
uniqueKeyForLayerLink: (int index) => "itChoices$index",
|
uniqueKeyForLayerLink: (int index) => "itChoices$index",
|
||||||
selectedChoiceIndex: null,
|
selectedChoiceIndex: null,
|
||||||
|
tts: controller.choreographer.tts,
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugger(when: kDebugMode);
|
debugger(when: kDebugMode);
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,7 @@ class AlternativeTranslations extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ChoicesArray(
|
return ChoicesArray(
|
||||||
originalSpan: controller.choreographer.itController.sourceText ?? "dummy",
|
originalSpan: controller.sourceText ?? "dummy",
|
||||||
isLoading:
|
isLoading:
|
||||||
controller.choreographer.altTranslator.loadingAlternativeTranslations,
|
controller.choreographer.altTranslator.loadingAlternativeTranslations,
|
||||||
// choices: controller.choreographer.altTranslator.similarityResponse.scores
|
// choices: controller.choreographer.altTranslator.similarityResponse.scores
|
||||||
|
|
@ -82,6 +82,7 @@ class AlternativeTranslations extends StatelessWidget {
|
||||||
},
|
},
|
||||||
uniqueKeyForLayerLink: (int index) => "altTranslation$index",
|
uniqueKeyForLayerLink: (int index) => "altTranslation$index",
|
||||||
selectedChoiceIndex: null,
|
selectedChoiceIndex: null,
|
||||||
|
tts: controller.choreographer.tts,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,16 @@ extension ActivityTypeExtension on ActivityTypeEnum {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool get includeTTSOnClick {
|
||||||
|
switch (this) {
|
||||||
|
case ActivityTypeEnum.wordMeaning:
|
||||||
|
return false;
|
||||||
|
case ActivityTypeEnum.wordFocusListening:
|
||||||
|
case ActivityTypeEnum.hiddenWordListening:
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ActivityTypeEnum fromString(String value) {
|
ActivityTypeEnum fromString(String value) {
|
||||||
final split = value.split('.').last;
|
final split = value.split('.').last;
|
||||||
switch (split) {
|
switch (split) {
|
||||||
|
|
|
||||||
|
|
@ -197,6 +197,7 @@ class PangeaToken {
|
||||||
case ActivityTypeEnum.wordMeaning:
|
case ActivityTypeEnum.wordMeaning:
|
||||||
return canBeDefined;
|
return canBeDefined;
|
||||||
case ActivityTypeEnum.wordFocusListening:
|
case ActivityTypeEnum.wordFocusListening:
|
||||||
|
return false;
|
||||||
case ActivityTypeEnum.hiddenWordListening:
|
case ActivityTypeEnum.hiddenWordListening:
|
||||||
return canBeHeard;
|
return canBeHeard;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import 'package:collection/collection.dart';
|
||||||
import 'package:fluffychat/pangea/enum/activity_display_instructions_enum.dart';
|
import 'package:fluffychat/pangea/enum/activity_display_instructions_enum.dart';
|
||||||
import 'package:fluffychat/pangea/enum/activity_type_enum.dart';
|
import 'package:fluffychat/pangea/enum/activity_type_enum.dart';
|
||||||
import 'package:fluffychat/pangea/enum/construct_type_enum.dart';
|
import 'package:fluffychat/pangea/enum/construct_type_enum.dart';
|
||||||
|
import 'package:fluffychat/pangea/models/pangea_token_model.dart';
|
||||||
import 'package:fluffychat/pangea/models/practice_activities.dart/multiple_choice_activity_model.dart';
|
import 'package:fluffychat/pangea/models/practice_activities.dart/multiple_choice_activity_model.dart';
|
||||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
@ -200,13 +201,20 @@ class PracticeActivityRequest {
|
||||||
}
|
}
|
||||||
|
|
||||||
class PracticeActivityModel {
|
class PracticeActivityModel {
|
||||||
|
// deprecated in favor of targetTokens
|
||||||
final List<ConstructIdentifier> tgtConstructs;
|
final List<ConstructIdentifier> tgtConstructs;
|
||||||
|
|
||||||
|
// being added after creation from request info
|
||||||
|
// TODO - replace tgtConstructs with targetTokens in server return
|
||||||
|
List<PangeaToken>? targetTokens;
|
||||||
|
|
||||||
final String langCode;
|
final String langCode;
|
||||||
final ActivityTypeEnum activityType;
|
final ActivityTypeEnum activityType;
|
||||||
final ActivityContent content;
|
final ActivityContent content;
|
||||||
|
|
||||||
PracticeActivityModel({
|
PracticeActivityModel({
|
||||||
required this.tgtConstructs,
|
required this.tgtConstructs,
|
||||||
|
required this.targetTokens,
|
||||||
required this.langCode,
|
required this.langCode,
|
||||||
required this.activityType,
|
required this.activityType,
|
||||||
required this.content,
|
required this.content,
|
||||||
|
|
@ -244,6 +252,11 @@ class PracticeActivityModel {
|
||||||
activityType:
|
activityType:
|
||||||
ActivityTypeEnum.wordMeaning.fromString(json['activity_type']),
|
ActivityTypeEnum.wordMeaning.fromString(json['activity_type']),
|
||||||
content: ActivityContent.fromJson(contentMap),
|
content: ActivityContent.fromJson(contentMap),
|
||||||
|
targetTokens: json['target_tokens'] is List
|
||||||
|
? (json['target_tokens'] as List)
|
||||||
|
.map((e) => PangeaToken.fromJson(e as Map<String, dynamic>))
|
||||||
|
.toList()
|
||||||
|
: null,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -256,6 +269,7 @@ class PracticeActivityModel {
|
||||||
'lang_code': langCode,
|
'lang_code': langCode,
|
||||||
'activity_type': activityType.string,
|
'activity_type': activityType.string,
|
||||||
'content': content.toJson(),
|
'content': content.toJson(),
|
||||||
|
'target_tokens': targetTokens?.map((e) => e.toJson()).toList(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -59,11 +59,11 @@ class MessageAudioCardState extends State<MessageAudioCard> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void didUpdateWidget(covariant oldWidget) {
|
void didUpdateWidget(covariant oldWidget) {
|
||||||
if (oldWidget.selection != widget.selection && widget.selection != null) {
|
// if (oldWidget.selection != widget.selection && widget.selection != null) {
|
||||||
debugPrint('selection changed');
|
// debugPrint('selection changed');
|
||||||
setSectionStartAndEndFromSelection();
|
// setSectionStartAndEndFromSelection();
|
||||||
playSelectionAudio();
|
// playSelectionAudio();
|
||||||
}
|
// }
|
||||||
super.didUpdateWidget(oldWidget);
|
super.didUpdateWidget(oldWidget);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,6 @@ import 'package:fluffychat/pangea/widgets/chat/message_toolbar_buttons.dart';
|
||||||
import 'package:fluffychat/pangea/widgets/chat/overlay_footer.dart';
|
import 'package:fluffychat/pangea/widgets/chat/overlay_footer.dart';
|
||||||
import 'package:fluffychat/pangea/widgets/chat/overlay_header.dart';
|
import 'package:fluffychat/pangea/widgets/chat/overlay_header.dart';
|
||||||
import 'package:fluffychat/pangea/widgets/chat/overlay_message.dart';
|
import 'package:fluffychat/pangea/widgets/chat/overlay_message.dart';
|
||||||
import 'package:fluffychat/pangea/widgets/chat/tts_controller.dart';
|
|
||||||
import 'package:fluffychat/widgets/avatar.dart';
|
import 'package:fluffychat/widgets/avatar.dart';
|
||||||
import 'package:fluffychat/widgets/matrix.dart';
|
import 'package:fluffychat/widgets/matrix.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
@ -67,7 +66,6 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
|
||||||
|
|
||||||
PangeaMessageEvent? get pangeaMessageEvent => widget._pangeaMessageEvent;
|
PangeaMessageEvent? get pangeaMessageEvent => widget._pangeaMessageEvent;
|
||||||
|
|
||||||
final TtsController tts = TtsController();
|
|
||||||
bool _isPlayingAudio = false;
|
bool _isPlayingAudio = false;
|
||||||
|
|
||||||
bool get showToolbarButtons =>
|
bool get showToolbarButtons =>
|
||||||
|
|
@ -139,8 +137,6 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
).listen((_) => setState(() {}));
|
).listen((_) => setState(() {}));
|
||||||
|
|
||||||
tts.setupTTS();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
MessageAnalyticsEntry? get messageAnalyticsEntry =>
|
MessageAnalyticsEntry? get messageAnalyticsEntry =>
|
||||||
|
|
@ -297,6 +293,14 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_selectedSpan != null) {
|
||||||
|
widget.chatController.choreographer.tts.tryToSpeak(
|
||||||
|
token.text.content,
|
||||||
|
context,
|
||||||
|
pangeaMessageEvent!.eventId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
setState(() {});
|
setState(() {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -450,7 +454,7 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_animationController.dispose();
|
_animationController.dispose();
|
||||||
_reactionSubscription?.cancel();
|
_reactionSubscription?.cancel();
|
||||||
tts.dispose();
|
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -562,7 +566,6 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
|
||||||
MessageToolbar(
|
MessageToolbar(
|
||||||
pangeaMessageEvent: pangeaMessageEvent!,
|
pangeaMessageEvent: pangeaMessageEvent!,
|
||||||
overLayController: this,
|
overLayController: this,
|
||||||
ttsController: tts,
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
|
|
|
||||||
|
|
@ -27,15 +27,16 @@ const double minCardHeight = 70;
|
||||||
class MessageToolbar extends StatelessWidget {
|
class MessageToolbar extends StatelessWidget {
|
||||||
final PangeaMessageEvent pangeaMessageEvent;
|
final PangeaMessageEvent pangeaMessageEvent;
|
||||||
final MessageOverlayController overLayController;
|
final MessageOverlayController overLayController;
|
||||||
final TtsController ttsController;
|
|
||||||
|
|
||||||
const MessageToolbar({
|
const MessageToolbar({
|
||||||
super.key,
|
super.key,
|
||||||
required this.pangeaMessageEvent,
|
required this.pangeaMessageEvent,
|
||||||
required this.overLayController,
|
required this.overLayController,
|
||||||
required this.ttsController,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
TtsController get ttsController =>
|
||||||
|
overLayController.widget.chatController.choreographer.tts;
|
||||||
|
|
||||||
Widget toolbarContent(BuildContext context) {
|
Widget toolbarContent(BuildContext context) {
|
||||||
final bool subscribed =
|
final bool subscribed =
|
||||||
MatrixState.pangeaController.subscriptionController.isSubscribed;
|
MatrixState.pangeaController.subscriptionController.isSubscribed;
|
||||||
|
|
@ -135,7 +136,6 @@ class MessageToolbar extends StatelessWidget {
|
||||||
return PracticeActivityCard(
|
return PracticeActivityCard(
|
||||||
pangeaMessageEvent: pangeaMessageEvent,
|
pangeaMessageEvent: pangeaMessageEvent,
|
||||||
overlayController: overLayController,
|
overlayController: overLayController,
|
||||||
ttsController: ttsController,
|
|
||||||
);
|
);
|
||||||
default:
|
default:
|
||||||
debugger(when: kDebugMode);
|
debugger(when: kDebugMode);
|
||||||
|
|
|
||||||
|
|
@ -145,7 +145,8 @@ class TtsController {
|
||||||
Future<void> tryToSpeak(
|
Future<void> tryToSpeak(
|
||||||
String text,
|
String text,
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
String eventID,
|
// TODO - make non-nullable again
|
||||||
|
String? eventID,
|
||||||
) async {
|
) async {
|
||||||
if (_isLanguageFullySupported) {
|
if (_isLanguageFullySupported) {
|
||||||
await _speak(text);
|
await _speak(text);
|
||||||
|
|
@ -157,7 +158,9 @@ class TtsController {
|
||||||
'availableLangCodes': _availableLangCodes,
|
'availableLangCodes': _availableLangCodes,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
await _showMissingVoicePopup(context, eventID);
|
if (eventID != null) {
|
||||||
|
await _showMissingVoicePopup(context, eventID);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -280,6 +280,7 @@ class WordMatchContent extends StatelessWidget {
|
||||||
uniqueKeyForLayerLink: (int index) =>
|
uniqueKeyForLayerLink: (int index) =>
|
||||||
"wordMatch$index",
|
"wordMatch$index",
|
||||||
selectedChoiceIndex: controller.selectedChoiceIndex,
|
selectedChoiceIndex: controller.selectedChoiceIndex,
|
||||||
|
tts: controller.widget.scm.choreographer.tts,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
PromptAndFeedback(controller: controller),
|
PromptAndFeedback(controller: controller),
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import 'package:fluffychat/config/app_config.dart';
|
||||||
import 'package:fluffychat/pangea/choreographer/widgets/choice_array.dart';
|
import 'package:fluffychat/pangea/choreographer/widgets/choice_array.dart';
|
||||||
import 'package:fluffychat/pangea/controllers/put_analytics_controller.dart';
|
import 'package:fluffychat/pangea/controllers/put_analytics_controller.dart';
|
||||||
import 'package:fluffychat/pangea/enum/activity_type_enum.dart';
|
import 'package:fluffychat/pangea/enum/activity_type_enum.dart';
|
||||||
|
import 'package:fluffychat/pangea/models/pangea_token_model.dart';
|
||||||
import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart';
|
import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart';
|
||||||
import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_record_model.dart';
|
import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_record_model.dart';
|
||||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||||
|
|
@ -21,7 +22,6 @@ import 'package:matrix/matrix.dart';
|
||||||
class MultipleChoiceActivity extends StatefulWidget {
|
class MultipleChoiceActivity extends StatefulWidget {
|
||||||
final PracticeActivityCardState practiceCardController;
|
final PracticeActivityCardState practiceCardController;
|
||||||
final PracticeActivityModel currentActivity;
|
final PracticeActivityModel currentActivity;
|
||||||
final TtsController tts;
|
|
||||||
final Event event;
|
final Event event;
|
||||||
final VoidCallback? onError;
|
final VoidCallback? onError;
|
||||||
|
|
||||||
|
|
@ -29,7 +29,6 @@ class MultipleChoiceActivity extends StatefulWidget {
|
||||||
super.key,
|
super.key,
|
||||||
required this.practiceCardController,
|
required this.practiceCardController,
|
||||||
required this.currentActivity,
|
required this.currentActivity,
|
||||||
required this.tts,
|
|
||||||
required this.event,
|
required this.event,
|
||||||
this.onError,
|
this.onError,
|
||||||
});
|
});
|
||||||
|
|
@ -46,6 +45,8 @@ class MultipleChoiceActivityState extends State<MultipleChoiceActivity> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
speakTargetTokens();
|
||||||
|
|
||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -55,18 +56,59 @@ class MultipleChoiceActivityState extends State<MultipleChoiceActivity> {
|
||||||
if (widget.practiceCardController.currentCompletionRecord?.responses
|
if (widget.practiceCardController.currentCompletionRecord?.responses
|
||||||
.isEmpty ??
|
.isEmpty ??
|
||||||
false) {
|
false) {
|
||||||
|
speakTargetTokens();
|
||||||
|
|
||||||
setState(() => selectedChoiceIndex = null);
|
setState(() => selectedChoiceIndex = null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void speakTargetTokens() {
|
||||||
|
if (widget.practiceCardController.currentActivity?.targetTokens != null) {
|
||||||
|
widget.practiceCardController.tts.tryToSpeak(
|
||||||
|
PangeaToken.reconstructText(
|
||||||
|
widget.practiceCardController.currentActivity!.targetTokens!,
|
||||||
|
),
|
||||||
|
context,
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TtsController get tts => widget.practiceCardController.tts;
|
||||||
|
|
||||||
void updateChoice(String value, int index) {
|
void updateChoice(String value, int index) {
|
||||||
|
final bool isCorrect =
|
||||||
|
widget.currentActivity.content.isCorrect(value, index);
|
||||||
|
|
||||||
|
// If the activity is not set to include TTS on click, and the choice is correct, speak the target tokens
|
||||||
|
// We have to check if tokens
|
||||||
|
if (!widget.currentActivity.activityType.includeTTSOnClick &&
|
||||||
|
isCorrect &&
|
||||||
|
mounted) {
|
||||||
|
// should be set by now but just in case we make a mistake
|
||||||
|
if (widget.practiceCardController.currentActivity?.targetTokens == null) {
|
||||||
|
debugger(when: kDebugMode);
|
||||||
|
ErrorHandler.logError(
|
||||||
|
e: "Missing target tokens in multiple choice activity",
|
||||||
|
data: {
|
||||||
|
"currentActivity": widget.practiceCardController.currentActivity,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
tts.tryToSpeak(
|
||||||
|
PangeaToken.reconstructText(
|
||||||
|
widget.practiceCardController.currentActivity!.targetTokens!,
|
||||||
|
),
|
||||||
|
context,
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (currentRecordModel?.hasTextResponse(value) ?? false) {
|
if (currentRecordModel?.hasTextResponse(value) ?? false) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final bool isCorrect =
|
|
||||||
widget.currentActivity.content.isCorrect(value, index);
|
|
||||||
|
|
||||||
currentRecordModel?.addResponse(
|
currentRecordModel?.addResponse(
|
||||||
text: value,
|
text: value,
|
||||||
score: isCorrect ? 1 : 0,
|
score: isCorrect ? 1 : 0,
|
||||||
|
|
@ -136,7 +178,7 @@ class MultipleChoiceActivityState extends State<MultipleChoiceActivity> {
|
||||||
ActivityTypeEnum.wordFocusListening)
|
ActivityTypeEnum.wordFocusListening)
|
||||||
WordAudioButton(
|
WordAudioButton(
|
||||||
text: practiceActivity.content.answer,
|
text: practiceActivity.content.answer,
|
||||||
ttsController: widget.tts,
|
ttsController: tts,
|
||||||
eventID: widget.event.eventId,
|
eventID: widget.event.eventId,
|
||||||
),
|
),
|
||||||
if (practiceActivity.activityType ==
|
if (practiceActivity.activityType ==
|
||||||
|
|
@ -146,7 +188,7 @@ class MultipleChoiceActivityState extends State<MultipleChoiceActivity> {
|
||||||
widget.practiceCardController.widget.pangeaMessageEvent,
|
widget.practiceCardController.widget.pangeaMessageEvent,
|
||||||
overlayController:
|
overlayController:
|
||||||
widget.practiceCardController.widget.overlayController,
|
widget.practiceCardController.widget.overlayController,
|
||||||
tts: widget.practiceCardController.widget.overlayController.tts,
|
tts: tts,
|
||||||
setIsPlayingAudio: widget.practiceCardController.widget
|
setIsPlayingAudio: widget.practiceCardController.widget
|
||||||
.overlayController.setIsPlayingAudio,
|
.overlayController.setIsPlayingAudio,
|
||||||
onError: widget.onError,
|
onError: widget.onError,
|
||||||
|
|
@ -170,6 +212,7 @@ class MultipleChoiceActivityState extends State<MultipleChoiceActivity> {
|
||||||
.toList(),
|
.toList(),
|
||||||
isActive: true,
|
isActive: true,
|
||||||
id: currentRecordModel?.hashCode.toString(),
|
id: currentRecordModel?.hashCode.toString(),
|
||||||
|
tts: practiceActivity.activityType.includeTTSOnClick ? tts : null,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -30,13 +30,11 @@ import 'package:flutter/material.dart';
|
||||||
class PracticeActivityCard extends StatefulWidget {
|
class PracticeActivityCard extends StatefulWidget {
|
||||||
final PangeaMessageEvent pangeaMessageEvent;
|
final PangeaMessageEvent pangeaMessageEvent;
|
||||||
final MessageOverlayController overlayController;
|
final MessageOverlayController overlayController;
|
||||||
final TtsController ttsController;
|
|
||||||
|
|
||||||
const PracticeActivityCard({
|
const PracticeActivityCard({
|
||||||
super.key,
|
super.key,
|
||||||
required this.pangeaMessageEvent,
|
required this.pangeaMessageEvent,
|
||||||
required this.overlayController,
|
required this.overlayController,
|
||||||
required this.ttsController,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -59,6 +57,9 @@ class PracticeActivityCardState extends State<PracticeActivityCard> {
|
||||||
Duration appropriateTimeForJoy = const Duration(milliseconds: 1500);
|
Duration appropriateTimeForJoy = const Duration(milliseconds: 1500);
|
||||||
bool savoringTheJoy = false;
|
bool savoringTheJoy = false;
|
||||||
|
|
||||||
|
TtsController get tts =>
|
||||||
|
widget.overlayController.widget.chatController.choreographer.tts;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
@ -100,76 +101,86 @@ class PracticeActivityCardState extends State<PracticeActivityCard> {
|
||||||
Future<PracticeActivityModel?> _fetchActivity({
|
Future<PracticeActivityModel?> _fetchActivity({
|
||||||
ActivityQualityFeedback? activityFeedback,
|
ActivityQualityFeedback? activityFeedback,
|
||||||
}) async {
|
}) async {
|
||||||
// try {
|
try {
|
||||||
debugPrint('Fetching activity');
|
debugPrint('Fetching activity');
|
||||||
_updateFetchingActivity(true);
|
_updateFetchingActivity(true);
|
||||||
|
|
||||||
// target tokens can be empty if activities have been completed for each
|
// target tokens can be empty if activities have been completed for each
|
||||||
// it's set on initialization and then removed when each activity is completed
|
// it's set on initialization and then removed when each activity is completed
|
||||||
if (!mounted ||
|
if (!mounted ||
|
||||||
!pangeaController.languageController.languagesSet ||
|
!pangeaController.languageController.languagesSet ||
|
||||||
widget.overlayController.messageAnalyticsEntry == null) {
|
widget.overlayController.messageAnalyticsEntry == null) {
|
||||||
|
debugger(when: kDebugMode);
|
||||||
|
_updateFetchingActivity(false);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final nextActivitySpecs =
|
||||||
|
widget.overlayController.messageAnalyticsEntry?.nextActivity;
|
||||||
|
// the client is going to be choosing the next activity now
|
||||||
|
// if nothing is set then it must be done with practice
|
||||||
|
if (nextActivitySpecs == null) {
|
||||||
|
debugPrint("No next activity set, exiting practice flow");
|
||||||
|
_updateFetchingActivity(false);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if we already have an activity matching the specs
|
||||||
|
final existingActivity = practiceActivities.firstWhereOrNull(
|
||||||
|
(activity) =>
|
||||||
|
nextActivitySpecs.matchesActivity(activity.practiceActivity),
|
||||||
|
);
|
||||||
|
if (existingActivity != null) {
|
||||||
|
debugPrint('found existing activity');
|
||||||
|
_updateFetchingActivity(false);
|
||||||
|
existingActivity.practiceActivity.targetTokens =
|
||||||
|
nextActivitySpecs.tokens;
|
||||||
|
return existingActivity.practiceActivity;
|
||||||
|
}
|
||||||
|
|
||||||
|
debugPrint(
|
||||||
|
"client requesting ${nextActivitySpecs.activityType.string} for: ${nextActivitySpecs.tokens.map((t) => "word: ${t.text.content} xp: ${t.xp}").join(' ')}",
|
||||||
|
);
|
||||||
|
|
||||||
|
final PracticeActivityModelResponse? activityResponse =
|
||||||
|
await pangeaController.practiceGenerationController
|
||||||
|
.getPracticeActivity(
|
||||||
|
MessageActivityRequest(
|
||||||
|
userL1: pangeaController.languageController.userL1!.langCode,
|
||||||
|
userL2: pangeaController.languageController.userL2!.langCode,
|
||||||
|
messageText: widget.pangeaMessageEvent.messageDisplayText,
|
||||||
|
messageTokens: widget.overlayController.tokens!,
|
||||||
|
activityQualityFeedback: activityFeedback,
|
||||||
|
targetTokens: nextActivitySpecs.tokens,
|
||||||
|
targetType: nextActivitySpecs.activityType,
|
||||||
|
),
|
||||||
|
widget.pangeaMessageEvent,
|
||||||
|
);
|
||||||
|
|
||||||
|
currentActivityCompleter = activityResponse?.eventCompleter;
|
||||||
|
_updateFetchingActivity(false);
|
||||||
|
|
||||||
|
if (activityResponse == null || activityResponse.activity == null) {
|
||||||
|
debugPrint('No activity found');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
activityResponse.activity!.targetTokens = nextActivitySpecs.tokens;
|
||||||
|
|
||||||
|
return activityResponse.activity;
|
||||||
|
} catch (e, s) {
|
||||||
debugger(when: kDebugMode);
|
debugger(when: kDebugMode);
|
||||||
_updateFetchingActivity(false);
|
ErrorHandler.logError(
|
||||||
|
e: e,
|
||||||
|
s: s,
|
||||||
|
m: 'Failed to get new activity',
|
||||||
|
data: {
|
||||||
|
'activity': currentActivity,
|
||||||
|
'record': currentCompletionRecord,
|
||||||
|
},
|
||||||
|
);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
final nextActivitySpecs =
|
|
||||||
widget.overlayController.messageAnalyticsEntry?.nextActivity;
|
|
||||||
// the client is going to be choosing the next activity now
|
|
||||||
// if nothing is set then it must be done with practice
|
|
||||||
if (nextActivitySpecs == null) {
|
|
||||||
debugPrint("No next activity set, exiting practice flow");
|
|
||||||
_updateFetchingActivity(false);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if we already have an activity matching the specs
|
|
||||||
final existingActivity = practiceActivities.firstWhereOrNull(
|
|
||||||
(activity) =>
|
|
||||||
nextActivitySpecs.matchesActivity(activity.practiceActivity),
|
|
||||||
);
|
|
||||||
if (existingActivity != null) {
|
|
||||||
debugPrint('found existing activity');
|
|
||||||
_updateFetchingActivity(false);
|
|
||||||
return existingActivity.practiceActivity;
|
|
||||||
}
|
|
||||||
|
|
||||||
debugPrint(
|
|
||||||
"client requesting ${nextActivitySpecs.activityType.string} for: ${nextActivitySpecs.tokens.map((t) => "word: ${t.text.content} xp: ${t.xp}").join(' ')}",
|
|
||||||
);
|
|
||||||
|
|
||||||
final PracticeActivityModelResponse? activityResponse =
|
|
||||||
await pangeaController.practiceGenerationController.getPracticeActivity(
|
|
||||||
MessageActivityRequest(
|
|
||||||
userL1: pangeaController.languageController.userL1!.langCode,
|
|
||||||
userL2: pangeaController.languageController.userL2!.langCode,
|
|
||||||
messageText: widget.pangeaMessageEvent.messageDisplayText,
|
|
||||||
messageTokens: widget.overlayController.tokens!,
|
|
||||||
activityQualityFeedback: activityFeedback,
|
|
||||||
targetTokens: nextActivitySpecs.tokens,
|
|
||||||
targetType: nextActivitySpecs.activityType,
|
|
||||||
),
|
|
||||||
widget.pangeaMessageEvent,
|
|
||||||
);
|
|
||||||
|
|
||||||
currentActivityCompleter = activityResponse?.eventCompleter;
|
|
||||||
_updateFetchingActivity(false);
|
|
||||||
|
|
||||||
return activityResponse?.activity;
|
|
||||||
// } catch (e, s) {
|
|
||||||
// debugger(when: kDebugMode);
|
|
||||||
// ErrorHandler.logError(
|
|
||||||
// e: e,
|
|
||||||
// s: s,
|
|
||||||
// m: 'Failed to get new activity',
|
|
||||||
// data: {
|
|
||||||
// 'activity': currentActivity,
|
|
||||||
// 'record': currentCompletionRecord,
|
|
||||||
// },
|
|
||||||
// );
|
|
||||||
// return null;
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ConstructUseMetaData get metadata => ConstructUseMetaData(
|
ConstructUseMetaData get metadata => ConstructUseMetaData(
|
||||||
|
|
@ -313,7 +324,6 @@ class PracticeActivityCardState extends State<PracticeActivityCard> {
|
||||||
return MultipleChoiceActivity(
|
return MultipleChoiceActivity(
|
||||||
practiceCardController: this,
|
practiceCardController: this,
|
||||||
currentActivity: currentActivity!,
|
currentActivity: currentActivity!,
|
||||||
tts: widget.ttsController,
|
|
||||||
event: widget.pangeaMessageEvent.event,
|
event: widget.pangeaMessageEvent.event,
|
||||||
onError: _onError,
|
onError: _onError,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue