import 'dart:async'; import 'dart:developer'; 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/constants/choreo_constants.dart'; import 'package:fluffychat/pangea/enum/instructions_enum.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:fluffychat/pangea/utils/inline_tooltip.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import '../../../config/app_config.dart'; import '../../controllers/it_feedback_controller.dart'; import '../../models/it_response_model.dart'; import '../../utils/overlay.dart'; import '../../widgets/igc/word_data_card.dart'; import 'choice_array.dart'; class ITBar extends StatefulWidget { final Choreographer choreographer; const ITBar({super.key, required this.choreographer}); @override ITBarState createState() => ITBarState(); } class ITBarState extends State { ITController get itController => widget.choreographer.itController; StreamSubscription? _choreoSub; @override void initState() { // Rebuild the widget each time there's an update from choreo. _choreoSub = widget.choreographer.stateListener.stream.listen((_) { setState(() {}); }); super.initState(); } @override void dispose() { _choreoSub?.cancel(); super.dispose(); } @override Widget build(BuildContext context) { return AnimatedSize( duration: itController.willOpen ? const Duration(milliseconds: 2000) : const Duration(milliseconds: 500), curve: Curves.fastOutSlowIn, clipBehavior: Clip.none, child: !itController.willOpen ? const SizedBox() : CompositedTransformTarget( link: widget.choreographer.itBarLinkAndKey.link, child: AnimatedOpacity( duration: itController.willOpen ? const Duration(milliseconds: 2000) : const Duration(milliseconds: 500), opacity: itController.willOpen ? 1.0 : 0.0, child: Container( key: widget.choreographer.itBarLinkAndKey.key, decoration: BoxDecoration( color: Theme.of(context).brightness == Brightness.light ? Colors.white : Colors.black, borderRadius: const BorderRadius.only( topLeft: Radius.circular(AppConfig.borderRadius), topRight: Radius.circular(AppConfig.borderRadius), ), ), width: double.infinity, padding: const EdgeInsets.fromLTRB(0, 3, 3, 3), child: Stack( children: [ SingleChildScrollView( child: Column( children: [ // Row( // mainAxisAlignment: MainAxisAlignment.spaceBetween, // crossAxisAlignment: CrossAxisAlignment.start, // children: [ // // Row( // // mainAxisAlignment: MainAxisAlignment.start, // // crossAxisAlignment: CrossAxisAlignment.start, // // children: [ // // CounterDisplay( // // correct: controller.correctChoices, // // custom: controller.customChoices, // // incorrect: controller.incorrectChoices, // // yellow: controller.wildcardChoices, // // ), // // CompositedTransformTarget( // // link: choreographer.itBotLayerLinkAndKey.link, // // child: ITBotButton( // // key: choreographer.itBotLayerLinkAndKey.key, // // choreographer: choreographer, // // ), // // ), // // ], // // ), // ITCloseButton(choreographer: choreographer), // ], // ), // const SizedBox(height: 40.0), OriginalText(controller: itController), const SizedBox(height: 7.0), IntrinsicHeight( child: Container( constraints: const BoxConstraints(minHeight: 80), width: double.infinity, padding: const EdgeInsets.symmetric(horizontal: 4.0), child: Center( child: itController .choreographer.errorService.isError ? ITError( error: itController.choreographer .errorService.error!, controller: itController, ) : itController.showChoiceFeedback ? ChoiceFeedbackText( controller: itController, ) : itController.isTranslationDone ? TranslationFeedback( controller: itController, ) : ITChoices( controller: itController, ), ), ), ), if (!widget .choreographer.pangeaController.instructions .wereInstructionsTurnedOff( InlineInstructions.translationChoices.toString(), )) Container( constraints: const BoxConstraints( maxWidth: 180, ), child: InlineTooltip( body: InlineInstructions.translationChoices .body(context), onClose: itController.closeHint, ), ), const SizedBox( height: 10, ), ], ), ), Positioned( top: 0.0, right: 0.0, child: ITCloseButton(choreographer: widget.choreographer), ), ], ), ), ), ), ); } } class ChoiceFeedbackText extends StatelessWidget { const ChoiceFeedbackText({ super.key, required this.controller, }); final ITController controller; @override Widget build(BuildContext context) { //reimplement if we decide we want it return const SizedBox(); // return AnimatedTextKit( // isRepeatingAnimation: false, // animatedTexts: [ // ScaleAnimatedText( // controller.latestChoiceFeedback(context), // duration: Duration( // milliseconds: // (ChoreoConstants.millisecondsToDisplayFeedback / 2).round(), // ), // scalingFactor: 1.4, // textStyle: BotStyle.text(context), // ), // ], // ); } } class OriginalText extends StatelessWidget { const OriginalText({ super.key, required this.controller, }); final ITController controller; @override Widget build(BuildContext context) { return Container( constraints: const BoxConstraints(minHeight: 50), padding: const EdgeInsets.only(left: 60.0, right: 40.0), decoration: const BoxDecoration( borderRadius: BorderRadius.only( topLeft: Radius.circular(AppConfig.borderRadius), topRight: Radius.circular(AppConfig.borderRadius), ), ), child: Row( //PTODO - does this already update after reset or we need to setState? mainAxisAlignment: MainAxisAlignment.center, children: [ if (!controller.isEditingSourceText) controller.sourceText != null ? Flexible(child: Text(controller.sourceText!)) : const LinearProgressIndicator(), if (controller.isEditingSourceText) Expanded( child: TextField( controller: TextEditingController(text: controller.sourceText), autofocus: true, enableSuggestions: false, maxLines: null, textInputAction: TextInputAction.send, onSubmitted: controller.onEditSourceTextSubmit, obscureText: false, decoration: const InputDecoration( border: OutlineInputBorder(), ), ), ), if (!controller.isEditingSourceText && controller.sourceText != null) AnimatedOpacity( duration: const Duration(milliseconds: 500), opacity: controller.nextITStep != null ? 1.0 : 0.0, child: IconButton( onPressed: () => { if (controller.nextITStep != null) { controller.setIsEditingSourceText(true), }, }, icon: const Icon(Icons.edit_outlined), ), ), ], ), ); } } class ITChoices extends StatelessWidget { const ITChoices({ super.key, required this.controller, }); // final choices = [ // "we need a really long translation to see what's going to happen with that. it should probably have multiple sentences so that we can see what happens there.we need a really long translation to see what's going to happen with that. it should probably have multiple sentences so that we can see what happens there.", // "we need a really long translation to see what's going to happen with that. it should probably have multiple sentences so that we can see what happens there.", // "we need a really long translation to see what's going to happen with that. it should probably have multiple sentences so that we can see what happens there.", // "we need a really long translation to see what's going to happen with that. it should probably have multiple sentences so that we can see what happens there.", // "we need a really long translation to see what's going to happen with that. it should probably have multiple sentences so that we can see what happens there.", // "we need a really long translation to see what's going to happen with that. it should probably have multiple sentences so that we can see what happens there.", // ]; final ITController controller; String? get sourceText { if ((controller.sourceText == null || controller.sourceText!.isEmpty)) { ErrorHandler.logError(m: "null source text in ItChoices"); } return controller.sourceText; } void showCard( BuildContext context, int index, [ Color? borderColor, String? choiceFeedback, ]) { if (controller.currentITStep == null) { ErrorHandler.logError( m: "currentITStep is null in showCard", s: StackTrace.current, ); return; } OverlayUtil.showPositionedCard( context: context, cardToShow: choiceFeedback == null ? WordDataCard( word: controller.currentITStep!.continuances[index].text, wordLang: controller.targetLangCode, fullText: sourceText ?? controller.choreographer.currentText, fullTextLang: sourceText != null ? controller.sourceLangCode : controller.targetLangCode, hasInfo: controller.currentITStep!.continuances[index].hasInfo, choiceFeedback: choiceFeedback, room: controller.choreographer.chatController.room, ) : ITFeedbackCard( req: ITFeedbackRequestModel( sourceText: sourceText!, currentText: controller.choreographer.currentText, chosenContinuance: controller.currentITStep!.continuances[index].text, bestContinuance: controller.currentITStep!.best.text, // TODO: we want this to eventually switch between target and source lang, // based on the learner's proficiency - maybe with the words involved in the translation // maybe overall. For now, we'll just use the source lang. feedbackLang: controller.choreographer.l1Lang?.langCode ?? controller.sourceLangCode, sourceTextLang: controller.sourceLangCode, targetLang: controller.targetLangCode, ), choiceFeedback: choiceFeedback, ), cardSize: const Size(300, 300), borderColor: borderColor, transformTargetId: controller.choreographer.itBarTransformTargetKey, backDropToDismiss: false, ); } @override Widget build(BuildContext context) { try { if (controller.isEditingSourceText || controller.currentITStep == null) { return const SizedBox(); } return ChoicesArray( isLoading: controller.isLoading || controller.choreographer.isFetching || controller.currentITStep == null, //TODO - pass current span being translated originalSpan: "dummy", choices: controller.currentITStep!.continuances.map((e) { try { return Choice( text: e.text.trim(), color: e.color, isGold: e.description == "best", ); } catch (e) { debugger(when: kDebugMode); return Choice(text: "error", color: Colors.red); } }).toList(), onPressed: (int index) { final Continuance continuance = controller.currentITStep!.continuances[index]; debugPrint("is gold? ${continuance.gold}"); if (continuance.level == 1 || continuance.wasClicked) { Future.delayed( const Duration(milliseconds: 500), () => controller.selectTranslation(index), ); } else { showCard( context, index, continuance.level == 2 ? ChoreoConstants.yellow : ChoreoConstants.red, continuance.feedbackText(context), ); } controller.currentITStep!.continuances[index].wasClicked = true; controller.choreographer.setState(); }, onLongPress: (int index) { showCard(context, index); }, uniqueKeyForLayerLink: (int index) => "itChoices$index", selectedChoiceIndex: null, ); } catch (e) { debugger(when: kDebugMode); return const SizedBox(); } } } class ITError extends StatelessWidget { final ITController controller; final Object error; const ITError({super.key, required this.error, required this.controller}); @override Widget build(BuildContext context) { final ErrorCopy errorCopy = ErrorCopy(context, error); return Padding( padding: const EdgeInsets.symmetric(horizontal: 8), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ ConstrainedBox( constraints: const BoxConstraints(maxWidth: 300), child: Text( // Text( "${errorCopy.title}\n${errorCopy.body}", // Haga clic en su mensaje para ver los significados de las palabras. style: TextStyle( fontStyle: FontStyle.italic, color: Theme.of(context).colorScheme.error, ), ), ), ITRestartButton(controller: controller), ], ), ); } }