import 'dart:developer'; import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pangea/enum/construct_use_type_enum.dart'; import 'package:fluffychat/pangea/enum/span_data_type.dart'; import 'package:fluffychat/pangea/models/pangea_token_model.dart'; import 'package:fluffychat/pangea/models/span_data.dart'; import 'package:fluffychat/pangea/utils/bot_style.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:fluffychat/pangea/utils/match_copy.dart'; import 'package:fluffychat/pangea/widgets/animations/gain_points.dart'; import 'package:fluffychat/pangea/widgets/igc/card_error_widget.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import '../../../widgets/matrix.dart'; import '../../choreographer/widgets/choice_array.dart'; import '../../controllers/pangea_controller.dart'; import '../../enum/span_choice_type.dart'; import '../../models/span_card_model.dart'; import '../common/bot_face_svg.dart'; import 'card_header.dart'; import 'why_button.dart'; //switch for definition vs correction vs practice //always show a title //if description then show description //choices then show choices // class SpanCard extends StatefulWidget { final PangeaController pangeaController = MatrixState.pangeaController; final SpanCardModel scm; final String roomId; SpanCard({ super.key, required this.scm, required this.roomId, }); @override State createState() => SpanCardState(); } class SpanCardState extends State { Object? error; bool fetchingData = false; int? selectedChoiceIndex; BotExpression currentExpression = BotExpression.nonGold; //on initState, get SpanDetails @override void initState() { // debugger(when: kDebugMode); super.initState(); getSpanDetails(); fetchSelected(); } //get selected choice SpanChoice? get selectedChoice { if (selectedChoiceIndex == null) return null; return choiceByIndex(selectedChoiceIndex!); } SpanChoice? choiceByIndex(int index) { if (widget.scm.pangeaMatch?.match.choices == null || widget.scm.pangeaMatch!.match.choices!.length <= index) { return null; } return widget.scm.pangeaMatch?.match.choices?[index]; } void fetchSelected() { if (widget.scm.pangeaMatch?.match.choices == null) { return; } if (selectedChoiceIndex == null) { DateTime? mostRecent; final numChoices = widget.scm.pangeaMatch!.match.choices!.length; for (int i = 0; i < numChoices; i++) { final choice = choiceByIndex(i); if (choice!.timestamp != null && (mostRecent == null || choice.timestamp!.isAfter(mostRecent))) { mostRecent = choice.timestamp; selectedChoiceIndex = i; } } } } Future getSpanDetails() async { try { if (widget.scm.pangeaMatch?.isITStart ?? false) return; if (!mounted) return; setState(() { fetchingData = true; }); await widget.scm.choreographer.igc.spanDataController .getSpanDetails(widget.scm.matchIndex); if (mounted) { setState(() => fetchingData = false); } } catch (e, s) { // debugger(when: kDebugMode); ErrorHandler.logError(e: e, s: s); if (mounted) { setState(() { error = e; fetchingData = false; }); } } } Future onChoiceSelect(String value, int index) async { selectedChoiceIndex = index; if (selectedChoice != null) { if (!selectedChoice!.selected) { MatrixState.pangeaController.myAnalytics.addDraftUses( selectedChoice!.tokens, widget.roomId, selectedChoice!.isBestCorrection ? ConstructUseTypeEnum.corIGC : ConstructUseTypeEnum.incIGC, ); } selectedChoice!.timestamp = DateTime.now(); selectedChoice!.selected = true; setState( () => (selectedChoice!.isBestCorrection ? BotExpression.gold : BotExpression.surprised), ); } } /// @ggurdin - this seems like it would be including the correct answer as well /// we only want to give this kind of points for ignored distractors /// Returns the list of choices that are not selected List? get ignoredMatches => widget.scm.pangeaMatch?.match.choices ?.where((choice) => !choice.selected) .toList(); /// Returns the list of tokens from choices that are not selected List? get ignoredTokens => ignoredMatches ?.expand((choice) => choice.tokens) .toList() .cast(); /// Adds the ignored tokens to locally cached analytics void addIgnoredTokenUses() { MatrixState.pangeaController.myAnalytics.addDraftUses( ignoredTokens ?? [], widget.roomId, ConstructUseTypeEnum.ignIGC, ); } void onReplaceSelected() { addIgnoredTokenUses(); widget.scm .onReplacementSelect( matchIndex: widget.scm.matchIndex, choiceIndex: selectedChoiceIndex!, ) .then((value) => setState(() {})); } void onIgnoreMatch() { MatrixState.pAnyState.closeOverlay(); addIgnoredTokenUses(); Future.delayed( Duration.zero, () { widget.scm.onIgnore(); }, ); } @override Widget build(BuildContext context) { return WordMatchContent(controller: this); } } class WordMatchContent extends StatelessWidget { final PangeaController pangeaController = MatrixState.pangeaController; final SpanCardState controller; WordMatchContent({ required this.controller, super.key, }); @override Widget build(BuildContext context) { if (controller.widget.scm.pangeaMatch == null) { return const SizedBox(); } if (controller.error != null) { return CardErrorWidget( error: controller.error!, choreographer: controller.widget.scm.choreographer, offset: controller.widget.scm.pangeaMatch?.match.offset, ); } final MatchCopy matchCopy = MatchCopy( context, controller.widget.scm.pangeaMatch!, ); final ScrollController scrollController = ScrollController(); try { return Stack( alignment: Alignment.topCenter, children: [ const Positioned( top: 40, child: PointsGainedAnimation(), ), Column( children: [ // if (!controller.widget.scm.pangeaMatch!.isITStart) CardHeader( text: controller.error?.toString() ?? matchCopy.title, botExpression: controller.error == null ? controller.currentExpression : BotExpression.addled, ), Expanded( child: Scrollbar( controller: scrollController, thumbVisibility: true, child: SingleChildScrollView( controller: scrollController, child: Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ // const SizedBox(height: 10.0), // if (matchCopy.description != null) // Padding( // padding: const EdgeInsets.only(), // child: Text( // matchCopy.description!, // style: BotStyle.text(context), // ), // ), const SizedBox(height: 8), if (!controller.widget.scm.pangeaMatch!.isITStart) ChoicesArray( originalSpan: controller.widget.scm.pangeaMatch!.matchContent, isLoading: controller.fetchingData, choices: controller.widget.scm.pangeaMatch!.match.choices ?.map( (e) => Choice( text: e.value, color: e.selected ? e.type.color : null, isGold: e.type.name == 'bestCorrection', ), ) .toList(), onPressed: controller.onChoiceSelect, uniqueKeyForLayerLink: (int index) => "wordMatch$index", selectedChoiceIndex: controller.selectedChoiceIndex, ), const SizedBox(height: 12), PromptAndFeedback(controller: controller), ], ), ), ), ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const SizedBox(width: 10), Expanded( child: Opacity( opacity: 0.8, child: TextButton( style: ButtonStyle( backgroundColor: WidgetStateProperty.all( AppConfig.primaryColor.withOpacity(0.1), ), ), onPressed: controller.onIgnoreMatch, child: Center( child: Text(L10n.of(context)!.ignoreInThisText), ), ), ), ), const SizedBox(width: 10), if (!controller.widget.scm.pangeaMatch!.isITStart) Expanded( child: Opacity( opacity: controller.selectedChoiceIndex != null ? 1.0 : 0.5, child: TextButton( onPressed: controller.selectedChoiceIndex != null ? controller.onReplaceSelected : null, style: ButtonStyle( backgroundColor: WidgetStateProperty.all( (controller.selectedChoice != null ? controller.selectedChoice!.color : AppConfig.primaryColor) .withOpacity(0.2), ), // Outline if Replace button enabled side: controller.selectedChoice != null ? WidgetStateProperty.all( BorderSide( color: controller.selectedChoice!.color, style: BorderStyle.solid, width: 2.0, ), ) : null, ), child: Text(L10n.of(context)!.replace), ), ), ), const SizedBox(width: 10), if (controller.widget.scm.pangeaMatch!.isITStart) Expanded( child: TextButton( onPressed: () { MatrixState.pAnyState.closeOverlay(); Future.delayed( Duration.zero, () => controller.widget.scm.onITStart(), ); }, style: ButtonStyle( backgroundColor: WidgetStateProperty.all( (AppConfig.primaryColor).withOpacity(0.1), ), ), child: Text(L10n.of(context)!.helpMeTranslate), ), ), ], ), if (controller.widget.scm.pangeaMatch!.isITStart) DontShowSwitchListTile( controller: pangeaController, onSwitch: (bool value) { pangeaController.userController.updateProfile((profile) { profile.userSettings.itAutoPlay = value; return profile; }); }, ), ], ), ], ); } on Exception catch (e) { debugger(when: kDebugMode); ErrorHandler.logError(e: e, s: StackTrace.current); rethrow; } } } class PromptAndFeedback extends StatelessWidget { const PromptAndFeedback({ super.key, required this.controller, }); final SpanCardState controller; @override Widget build(BuildContext context) { if (controller.widget.scm.pangeaMatch == null) { return const SizedBox(); } return Container( constraints: controller.widget.scm.pangeaMatch!.isITStart ? null : const BoxConstraints(minHeight: 100), child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ if (controller.selectedChoice == null && controller.fetchingData) const Center( child: SizedBox( width: 24.0, height: 24.0, child: CircularProgressIndicator(), ), ), if (controller.selectedChoice != null) ...[ Text( controller.selectedChoice!.feedbackToDisplay(context), style: BotStyle.text(context), ), const SizedBox(height: 8), if (controller.selectedChoice?.feedback == null) WhyButton( onPress: () { if (!controller.fetchingData) { controller.getSpanDetails(); } }, loading: controller.fetchingData, ), ], if (!controller.fetchingData && controller.selectedChoiceIndex == null) Text( controller.widget.scm.pangeaMatch!.match.type.typeName .defaultPrompt(context), style: BotStyle.text(context), ), ], ), ); } } class LoadingText extends StatefulWidget { const LoadingText({ super.key, }); @override _LoadingTextState createState() => _LoadingTextState(); } class _LoadingTextState extends State with SingleTickerProviderStateMixin { late final AnimationController _controller = AnimationController( duration: const Duration(seconds: 1), vsync: this, )..repeat(); @override Widget build(BuildContext context) { return Row( mainAxisSize: MainAxisSize.min, children: [ Text( L10n.of(context)!.makingActivity, style: BotStyle.text(context), ), AnimatedBuilder( animation: _controller, builder: (BuildContext context, Widget? child) { return Text( _controller.isAnimating ? '.' * _controller.value.toInt() : '', style: BotStyle.text(context), ); }, ), ], ); } @override void dispose() { _controller.dispose(); super.dispose(); } } class StartITButton extends StatelessWidget { const StartITButton({ super.key, required this.onITStart, }); final void Function() onITStart; @override Widget build(BuildContext context) { return Material( type: MaterialType.transparency, child: ListTile( leading: const Icon(Icons.translate_outlined), title: Text(L10n.of(context)!.helpMeTranslate), onTap: () { MatrixState.pAnyState.closeOverlay(); Future.delayed(Duration.zero, () => onITStart()); }, ), ); } } class DontShowSwitchListTile extends StatefulWidget { final PangeaController controller; final Function(bool) onSwitch; const DontShowSwitchListTile({ super.key, required this.controller, required this.onSwitch, }); @override DontShowSwitchListTileState createState() => DontShowSwitchListTileState(); } class DontShowSwitchListTileState extends State { bool switchValue = false; @override void initState() { super.initState(); } @override Widget build(BuildContext context) { return SwitchListTile.adaptive( activeColor: AppConfig.activeToggleColor, title: Text(L10n.of(context)!.interactiveTranslatorAutoPlaySliderHeader), value: switchValue, onChanged: (value) { widget.onSwitch(value); setState(() => switchValue = value); }, ); } }