diff --git a/lib/pangea/analytics_data/analytics_data_service.dart b/lib/pangea/analytics_data/analytics_data_service.dart index 32b5d5dc0..f01371c9b 100644 --- a/lib/pangea/analytics_data/analytics_data_service.dart +++ b/lib/pangea/analytics_data/analytics_data_service.dart @@ -10,7 +10,6 @@ import 'package:fluffychat/pangea/analytics_data/analytics_update_events.dart'; import 'package:fluffychat/pangea/analytics_data/analytics_update_service.dart'; import 'package:fluffychat/pangea/analytics_data/construct_merge_table.dart'; import 'package:fluffychat/pangea/analytics_data/derived_analytics_data_model.dart'; -import 'package:fluffychat/pangea/analytics_data/level_up_analytics_service.dart'; import 'package:fluffychat/pangea/analytics_misc/analytics_constants.dart'; import 'package:fluffychat/pangea/analytics_misc/client_analytics_extension.dart'; import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart'; @@ -49,7 +48,6 @@ class AnalyticsDataService { late final AnalyticsUpdateDispatcher updateDispatcher; late final AnalyticsUpdateService updateService; - late final LevelUpAnalyticsService levelUpService; AnalyticsSyncController? _syncController; final ConstructMergeTable _mergeTable = ConstructMergeTable(); @@ -58,11 +56,6 @@ class AnalyticsDataService { AnalyticsDataService(Client client) { updateDispatcher = AnalyticsUpdateDispatcher(this); updateService = AnalyticsUpdateService(this); - levelUpService = LevelUpAnalyticsService( - client: client, - ensureInitialized: () => _ensureInitialized(), - dataService: this, - ); _initDatabase(client); } diff --git a/lib/pangea/analytics_data/derived_analytics_data_model.dart b/lib/pangea/analytics_data/derived_analytics_data_model.dart index 2b5416778..3fdf039f8 100644 --- a/lib/pangea/analytics_data/derived_analytics_data_model.dart +++ b/lib/pangea/analytics_data/derived_analytics_data_model.dart @@ -41,8 +41,7 @@ class DerivedAnalyticsDataModel { // XP from the inverse formula: final double xpDouble = (D / 8.0) * (2.0 * pow(lc - 1.0, 2.0) - 1.0); - // Floor or clamp to ensure non-negative. - final int xp = xpDouble.floor(); + final int xp = xpDouble.ceil(); return (xp < 0) ? 0 : xp; } diff --git a/lib/pangea/analytics_data/level_up_analytics_service.dart b/lib/pangea/analytics_data/level_up_analytics_service.dart deleted file mode 100644 index e5d5a0eb9..000000000 --- a/lib/pangea/analytics_data/level_up_analytics_service.dart +++ /dev/null @@ -1,122 +0,0 @@ -import 'dart:async'; - -import 'package:matrix/matrix.dart'; - -import 'package:fluffychat/pangea/analytics_data/analytics_data_service.dart'; -import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart'; -import 'package:fluffychat/pangea/analytics_misc/constructs_model.dart'; -import 'package:fluffychat/pangea/common/utils/error_handler.dart'; -import 'package:fluffychat/pangea/constructs/construct_repo.dart'; -import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart'; -import 'package:fluffychat/widgets/matrix.dart'; - -class LevelUpAnalyticsService { - final Client client; - final Future Function() ensureInitialized; - final AnalyticsDataService dataService; - - const LevelUpAnalyticsService({ - required this.client, - required this.ensureInitialized, - required this.dataService, - }); - - Future getLevelUpAnalytics( - int lowerLevel, - int upperLevel, - DateTime? lastLevelUpTimestamp, - ) async { - await ensureInitialized(); - - final userController = MatrixState.pangeaController.userController; - final l2 = userController.userL2; - if (l2 == null) { - throw Exception("No L2 language set for user"); - } - - final uses = await dataService.getUses( - l2.langCodeShort, - since: lastLevelUpTimestamp, - ); - final messages = await _buildMessageContext(uses); - - final request = ConstructSummaryRequest( - constructs: uses, - messages: messages, - userL1: userController.userL1!.langCodeShort, - userL2: userController.userL2!.langCodeShort, - lowerLevel: lowerLevel, - upperLevel: upperLevel, - ); - - final response = await ConstructRepo.generateConstructSummary(request); - final summary = response.summary; - - summary.levelVocabConstructs = dataService.uniqueConstructsByType( - ConstructTypeEnum.vocab, - ); - summary.levelGrammarConstructs = dataService.uniqueConstructsByType( - ConstructTypeEnum.morph, - ); - - return summary; - } - - Future>> _buildMessageContext( - List uses, - ) async { - final Map> useEventIds = {}; - - for (final use in uses) { - final roomId = use.metadata.roomId; - final eventId = use.metadata.eventId; - if (roomId == null || eventId == null) continue; - - useEventIds.putIfAbsent(roomId, () => {}).add(eventId); - } - - final List> messages = []; - - for (final entry in useEventIds.entries) { - final room = client.getRoomById(entry.key); - if (room == null) continue; - - final timeline = await room.getTimeline(); - - for (final eventId in entry.value) { - try { - final event = await room.getEventById(eventId); - if (event == null) continue; - - final pangeaEvent = PangeaMessageEvent( - event: event, - timeline: timeline, - ownMessage: room.client.userID == event.senderId, - ); - - if (pangeaEvent.isAudioMessage) { - final stt = pangeaEvent.getSpeechToTextLocal(); - if (stt == null) continue; - messages.add({ - 'sent': stt.transcript.text, - 'written': stt.transcript.text, - }); - } else { - messages.add({ - 'sent': pangeaEvent.originalSent?.text ?? pangeaEvent.body, - 'written': pangeaEvent.originalWrittenContent, - }); - } - } catch (e, s) { - ErrorHandler.logError( - e: e, - s: s, - data: {'roomId': entry.key, 'eventId': eventId}, - ); - } - } - } - - return messages; - } -} diff --git a/lib/pangea/analytics_misc/level_summary_extension.dart b/lib/pangea/analytics_misc/level_summary_extension.dart deleted file mode 100644 index 2cab1d179..000000000 --- a/lib/pangea/analytics_misc/level_summary_extension.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'package:matrix/matrix.dart'; - -import 'package:fluffychat/pangea/constructs/construct_repo.dart'; -import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart'; - -extension LevelSummaryExtension on Room { - ConstructSummary? get levelUpSummary { - final summaryEvent = getState(PangeaEventTypes.constructSummary); - if (summaryEvent != null) { - return ConstructSummary.fromJson(summaryEvent.content); - } - return null; - } - - DateTime? get lastLevelUpTimestamp { - final lastLevelUp = getState(PangeaEventTypes.constructSummary); - return lastLevelUp is Event ? lastLevelUp.originServerTs : null; - } - - Future setLevelUpSummary(ConstructSummary summary) => - client.setRoomStateWithKey( - id, - PangeaEventTypes.constructSummary, - '', - summary.toJson(), - ); -} diff --git a/lib/pangea/analytics_misc/level_up/level_up_banner.dart b/lib/pangea/analytics_misc/level_up/level_up_banner.dart index 7cc82dbb9..55f970613 100644 --- a/lib/pangea/analytics_misc/level_up/level_up_banner.dart +++ b/lib/pangea/analytics_misc/level_up/level_up_banner.dart @@ -10,13 +10,7 @@ import 'package:fluffychat/config/setting_keys.dart'; import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/pangea/analytics_misc/analytics_constants.dart'; -import 'package:fluffychat/pangea/analytics_misc/client_analytics_extension.dart'; -import 'package:fluffychat/pangea/analytics_misc/level_summary_extension.dart'; -import 'package:fluffychat/pangea/analytics_misc/level_up/level_up_manager.dart'; -import 'package:fluffychat/pangea/analytics_misc/level_up/level_up_popup.dart'; -import 'package:fluffychat/pangea/common/utils/error_handler.dart'; import 'package:fluffychat/pangea/common/utils/overlay.dart'; -import 'package:fluffychat/pangea/constructs/construct_repo.dart'; import 'package:fluffychat/widgets/matrix.dart'; class LevelUpConstants { @@ -77,7 +71,7 @@ class LevelUpUtil { } } -class LevelUpBanner extends StatefulWidget { +class LevelUpBanner extends StatelessWidget { final int level; final int prevLevel; final Widget? backButtonOverride; @@ -89,103 +83,6 @@ class LevelUpBanner extends StatefulWidget { super.key, }); - @override - LevelUpBannerState createState() => LevelUpBannerState(); -} - -class LevelUpBannerState extends State - with TickerProviderStateMixin { - late AnimationController _slideController; - late Animation _slideAnimation; - - bool _showedDetails = false; - - final Completer _constructSummaryCompleter = - Completer(); - - @override - void initState() { - super.initState(); - - _loadConstructSummary(); - - final analyticsService = Matrix.of(context).analyticsDataService; - LevelUpManager.instance.preloadAnalytics( - widget.level, - widget.prevLevel, - analyticsService, - ); - - _slideController = AnimationController( - vsync: this, - duration: FluffyThemes.animationDuration, - ); - - _slideAnimation = Tween( - begin: const Offset(0, -1), - end: Offset.zero, - ).animate(CurvedAnimation(parent: _slideController, curve: Curves.easeOut)); - - _slideController.forward(); - - Future.delayed(const Duration(seconds: 10), () async { - if (mounted && !_showedDetails) { - _close(); - } - }); - } - - Future _close() async { - await _slideController.reverse(); - MatrixState.pAnyState.closeOverlay("level_up_notification"); - } - - @override - void dispose() { - _slideController.dispose(); - super.dispose(); - } - - Future _toggleDetails() async { - await _close(); - LevelUpManager.instance.markPopupSeen(); - _showedDetails = true; - - FocusScope.of(context).unfocus(); - - await showDialog( - context: context, - builder: (context) => - LevelUpPopup(constructSummaryCompleter: _constructSummaryCompleter), - ); - } - - Future _loadConstructSummary() async { - try { - final analyticsRoom = await Matrix.of(context).client.getMyAnalyticsRoom( - MatrixState.pangeaController.userController.userL2!, - ); - - final timestamp = analyticsRoom!.lastLevelUpTimestamp; - final analyticsService = Matrix.of(context).analyticsDataService; - final summary = await analyticsService.levelUpService.getLevelUpAnalytics( - widget.prevLevel, - widget.level, - timestamp, - ); - _constructSummaryCompleter.complete(summary); - analyticsRoom.setLevelUpSummary(summary); - } catch (e, s) { - debugPrint("Error generating level up analytics: $e"); - ErrorHandler.logError( - e: e, - s: s, - data: {"level": widget.level, "prevLevel": widget.prevLevel}, - ); - _constructSummaryCompleter.completeError(e); - } - } - @override Widget build(BuildContext context) { final isColumnMode = FluffyThemes.isColumnMode(context); @@ -205,101 +102,88 @@ class LevelUpBannerState extends State return SafeArea( child: Material( type: MaterialType.transparency, - child: SlideTransition( - position: _slideAnimation, - child: LayoutBuilder( - builder: (context, constraints) { - return GestureDetector( - onPanUpdate: (details) { - if (details.delta.dy < -10) _close(); - }, - onTap: _toggleDetails, - child: Container( - padding: const EdgeInsets.symmetric( - vertical: 16.0, - horizontal: 4.0, - ), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - border: Border( - bottom: BorderSide( - color: AppConfig.gold.withAlpha(200), - width: 2.0, - ), - ), - borderRadius: const BorderRadius.only( - bottomLeft: Radius.circular(AppConfig.borderRadius), - bottomRight: Radius.circular(AppConfig.borderRadius), - ), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - // Spacer for symmetry - SizedBox( - width: constraints.maxWidth >= 600 ? 120.0 : 65.0, - ), - // Centered content - Expanded( - child: Padding( - padding: EdgeInsets.symmetric( - horizontal: isColumnMode ? 16.0 : 8.0, - ), - child: Wrap( - spacing: 16.0, - alignment: WrapAlignment.center, - crossAxisAlignment: WrapCrossAlignment.center, - children: [ - Text( - L10n.of(context).levelUp, - style: style, - overflow: TextOverflow.ellipsis, - ), - CachedNetworkImage( - imageUrl: - "${AppConfig.assetsBaseURL}/${LevelUpConstants.starFileName}", - height: 24, - width: 24, - ), - ], - ), - ), - ), - SizedBox( - width: constraints.maxWidth >= 600 ? 120.0 : 65.0, - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - SizedBox( - width: 32.0, - height: 32.0, - child: Center( - child: IconButton( - icon: const Icon(Icons.close), - style: IconButton.styleFrom( - padding: const EdgeInsets.all(4.0), - ), - onPressed: () { - MatrixState.pAnyState.closeOverlay( - "level_up_notification", - ); - }, - constraints: const BoxConstraints(), - color: Theme.of( - context, - ).colorScheme.onSurface, - ), - ), - ), - ], - ), - ), - ], + child: LayoutBuilder( + builder: (context, constraints) { + return Container( + padding: const EdgeInsets.symmetric( + vertical: 16.0, + horizontal: 4.0, + ), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + border: Border( + bottom: BorderSide( + color: AppConfig.gold.withAlpha(200), + width: 2.0, ), ), - ); - }, - ), + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(AppConfig.borderRadius), + bottomRight: Radius.circular(AppConfig.borderRadius), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + // Spacer for symmetry + SizedBox(width: constraints.maxWidth >= 600 ? 120.0 : 65.0), + // Centered content + Expanded( + child: Padding( + padding: EdgeInsets.symmetric( + horizontal: isColumnMode ? 16.0 : 8.0, + ), + child: Wrap( + spacing: 16.0, + alignment: WrapAlignment.center, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + Text( + L10n.of(context).levelUp, + style: style, + overflow: TextOverflow.ellipsis, + ), + CachedNetworkImage( + imageUrl: + "${AppConfig.assetsBaseURL}/${LevelUpConstants.starFileName}", + height: 24, + width: 24, + ), + ], + ), + ), + ), + SizedBox( + width: constraints.maxWidth >= 600 ? 120.0 : 65.0, + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + SizedBox( + width: 32.0, + height: 32.0, + child: Center( + child: IconButton( + icon: const Icon(Icons.close), + style: IconButton.styleFrom( + padding: const EdgeInsets.all(4.0), + ), + onPressed: () { + MatrixState.pAnyState.closeOverlay( + "level_up_notification", + ); + }, + constraints: const BoxConstraints(), + color: Theme.of(context).colorScheme.onSurface, + ), + ), + ), + ], + ), + ), + ], + ), + ); + }, ), ), ); diff --git a/lib/pangea/analytics_misc/level_up/level_up_manager.dart b/lib/pangea/analytics_misc/level_up/level_up_manager.dart deleted file mode 100644 index 61f0b6b5d..000000000 --- a/lib/pangea/analytics_misc/level_up/level_up_manager.dart +++ /dev/null @@ -1,77 +0,0 @@ -import 'package:matrix/matrix.dart'; - -import 'package:fluffychat/pangea/analytics_data/analytics_data_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/level_summary_extension.dart'; -import 'package:fluffychat/pangea/languages/language_model.dart'; -import 'package:fluffychat/widgets/matrix.dart'; - -class LevelUpManager { - // Singleton instance so analytics can be generated when level up is initiated, and be ready by the time user clicks on banner - static final LevelUpManager instance = LevelUpManager._internal(); - - LevelUpManager._internal(); - - int prevLevel = 0; - int level = 0; - - int prevGrammar = 0; - int nextGrammar = 0; - int prevVocab = 0; - int nextVocab = 0; - - bool hasSeenPopup = false; - bool shouldAutoPopup = false; - - Future preloadAnalytics( - int level, - int prevLevel, - AnalyticsDataService analyticsService, - ) async { - this.level = level; - this.prevLevel = prevLevel; - - //For on route change behavior, if added in the future - shouldAutoPopup = true; - - nextGrammar = analyticsService.numConstructs(ConstructTypeEnum.morph); - nextVocab = analyticsService.numConstructs(ConstructTypeEnum.vocab); - - final LanguageModel? l2 = - MatrixState.pangeaController.userController.userL2; - final Room? analyticsRoom = MatrixState.pangeaController.matrixState.client - .analyticsRoomLocal(l2!); - - if (analyticsRoom != null) { - final lastSummary = analyticsRoom.levelUpSummary; - - //Set grammar and vocab from last level summary, if there is one. Otherwise set to placeholder data - if (lastSummary != null && - lastSummary.levelVocabConstructs != null && - lastSummary.levelGrammarConstructs != null) { - prevVocab = lastSummary.levelVocabConstructs!; - prevGrammar = lastSummary.levelGrammarConstructs!; - } else { - prevGrammar = nextGrammar - (nextGrammar / prevLevel).round(); - prevVocab = nextVocab - (nextVocab / prevLevel).round(); - } - } - } - - void markPopupSeen() { - hasSeenPopup = true; - shouldAutoPopup = false; - } - - void reset() { - hasSeenPopup = false; - shouldAutoPopup = false; - prevLevel = 0; - level = 0; - prevGrammar = 0; - nextGrammar = 0; - prevVocab = 0; - nextVocab = 0; - } -} diff --git a/lib/pangea/analytics_misc/level_up/level_up_popup.dart b/lib/pangea/analytics_misc/level_up/level_up_popup.dart deleted file mode 100644 index d4cdd38b7..000000000 --- a/lib/pangea/analytics_misc/level_up/level_up_popup.dart +++ /dev/null @@ -1,517 +0,0 @@ -import 'dart:async'; -import 'dart:math'; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; - -import 'package:animated_flip_counter/animated_flip_counter.dart'; -import 'package:cached_network_image/cached_network_image.dart'; -import 'package:material_symbols_icons/symbols.dart'; -import 'package:matrix/matrix_api_lite/generated/model.dart'; - -import 'package:fluffychat/config/app_config.dart'; -import 'package:fluffychat/l10n/l10n.dart'; -import 'package:fluffychat/pangea/analytics_misc/learning_skills_enum.dart'; -import 'package:fluffychat/pangea/analytics_misc/level_up/level_popup_progess_bar.dart'; -import 'package:fluffychat/pangea/analytics_misc/level_up/level_up_banner.dart'; -import 'package:fluffychat/pangea/analytics_misc/level_up/level_up_manager.dart'; -import 'package:fluffychat/pangea/analytics_misc/level_up/star_rain_widget.dart'; -import 'package:fluffychat/pangea/common/widgets/error_indicator.dart'; -import 'package:fluffychat/pangea/common/widgets/full_width_dialog.dart'; -import 'package:fluffychat/pangea/constructs/construct_repo.dart'; -import 'package:fluffychat/pangea/languages/language_constants.dart'; -import 'package:fluffychat/utils/localized_exception_extension.dart'; -import 'package:fluffychat/widgets/avatar.dart'; -import 'package:fluffychat/widgets/matrix.dart'; -import 'package:fluffychat/widgets/mxc_image.dart'; - -class LevelUpPopup extends StatefulWidget { - final Completer constructSummaryCompleter; - const LevelUpPopup({required this.constructSummaryCompleter, super.key}); - - @override - State createState() => _LevelUpPopupState(); -} - -class _LevelUpPopupState extends State { - bool shouldShowRain = false; - - void setShowRain(bool show) { - setState(() { - shouldShowRain = show; - }); - } - - @override - Widget build(BuildContext context) { - return Stack( - children: [ - FullWidthDialog( - maxWidth: 400, - maxHeight: 800, - dialogContent: Scaffold( - appBar: AppBar( - centerTitle: true, - title: kIsWeb - ? Text( - L10n.of(context).youHaveLeveledUp, - style: const TextStyle( - color: AppConfig.gold, - fontWeight: FontWeight.w600, - ), - ) - : null, - ), - body: LevelUpPopupContent( - prevLevel: LevelUpManager.instance.prevLevel, - level: LevelUpManager.instance.level, - constructSummaryCompleter: widget.constructSummaryCompleter, - onRainTrigger: () => setShowRain(true), - ), - ), - ), - if (shouldShowRain) const StarRainWidget(showBlast: true), - ], - ); - } -} - -class LevelUpPopupContent extends StatefulWidget { - final int prevLevel; - final int level; - final Completer constructSummaryCompleter; - - final VoidCallback? onRainTrigger; - - const LevelUpPopupContent({ - super.key, - required this.prevLevel, - required this.level, - required this.constructSummaryCompleter, - this.onRainTrigger, - }); - - @override - State createState() => _LevelUpPopupContentState(); -} - -class _LevelUpPopupContentState extends State - with SingleTickerProviderStateMixin { - late final AnimationController _controller; - late final Future profile; - - int displayedLevel = -1; - Uri? avatarUrl; - final bool _hasBlastedConfetti = false; - - String language = - MatrixState.pangeaController.userController.userL2Code?.toUpperCase() ?? - LanguageKeys.unknownLanguage; - - ConstructSummary? _constructSummary; - Object? _error; - bool _loading = true; - - @override - void initState() { - super.initState(); - _loadConstructSummary(); - LevelUpManager.instance.markPopupSeen(); - displayedLevel = widget.prevLevel; - - final client = Matrix.of(context).client; - client.fetchOwnProfile().then((profile) { - setState(() => avatarUrl = profile.avatarUrl); - }); - - _controller = AnimationController( - duration: const Duration(seconds: 5), - vsync: this, - ); - - // halfway through the animation, switch to the new level - _controller.addListener(() { - if (_controller.value >= 0.5 && displayedLevel == widget.prevLevel) { - setState(() { - displayedLevel = widget.level; - }); - } - }); - - // Listener to trigger rain confetti via callback - _controller.addListener(() { - if (_controller.value >= 0.5 && !_hasBlastedConfetti) { - // _hasBlastedConfetti = true; - if (widget.onRainTrigger != null) widget.onRainTrigger!(); - } - }); - - _controller.forward(); - } - - @override - void dispose() { - _controller.dispose(); - LevelUpManager.instance.reset(); - super.dispose(); - } - - int get _startGrammar => LevelUpManager.instance.prevGrammar; - int get _startVocab => LevelUpManager.instance.prevVocab; - - int get _endGrammar => LevelUpManager.instance.nextGrammar; - int get _endVocab => LevelUpManager.instance.nextVocab; - - Future _loadConstructSummary() async { - try { - _constructSummary = await widget.constructSummaryCompleter.future; - } catch (e) { - _error = e; - } finally { - setState(() => _loading = false); - } - } - - int _getSkillXP(LearningSkillsEnum skill) { - if (_constructSummary == null) return 0; - return switch (skill) { - LearningSkillsEnum.writing => - _constructSummary?.writingConstructScore ?? 0, - LearningSkillsEnum.reading => - _constructSummary?.readingConstructScore ?? 0, - LearningSkillsEnum.speaking => - _constructSummary?.speakingConstructScore ?? 0, - LearningSkillsEnum.hearing => - _constructSummary?.hearingConstructScore ?? 0, - _ => 0, - }; - } - - @override - @override - Widget build(BuildContext context) { - final Animation vocabAnimation = - IntTween(begin: _startVocab, end: _endVocab).animate( - CurvedAnimation( - parent: _controller, - curve: const Interval(0.0, 0.5, curve: Curves.easeInOutQuad), - ), - ); - - final Animation grammarAnimation = - IntTween(begin: _startGrammar, end: _endGrammar).animate( - CurvedAnimation( - parent: _controller, - curve: const Interval(0.0, 0.5, curve: Curves.easeInOutQuad), - ), - ); - - final Animation skillsOpacity = Tween(begin: 0.0, end: 1.0) - .animate( - CurvedAnimation( - parent: _controller, - curve: const Interval(0.7, 1.0, curve: Curves.easeIn), - ), - ); - - final Animation shrinkMultiplier = - Tween(begin: 1.0, end: 0.3).animate( - CurvedAnimation( - parent: _controller, - curve: const Interval(0.7, 1.0, curve: Curves.easeInOut), - ), - ); - - final colorScheme = Theme.of(context).colorScheme; - final grammarVocabStyle = Theme.of(context).textTheme.titleLarge?.copyWith( - fontWeight: FontWeight.bold, - color: colorScheme.primary, - ); - final username = - Matrix.of(context).client.userID?.split(':').first.substring(1) ?? ''; - - return Stack( - children: [ - SingleChildScrollView( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - AnimatedBuilder( - animation: _controller, - builder: (_, _) => Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Padding( - padding: const EdgeInsets.all(24.0), - child: avatarUrl == null - ? Avatar( - name: username, - showPresence: false, - size: 150 * shrinkMultiplier.value, - ) - : ClipOval( - child: MxcImage( - uri: avatarUrl, - width: 150 * shrinkMultiplier.value, - height: 150 * shrinkMultiplier.value, - ), - ), - ), - Text( - language, - style: TextStyle( - fontSize: 24 * skillsOpacity.value, - color: AppConfig.goldLight, - fontWeight: FontWeight.bold, - ), - ), - ], - ), - ), - // Progress bar + Level - AnimatedBuilder( - animation: _controller, - builder: (_, _) => Row( - children: [ - const Expanded( - child: LevelPopupProgressBar( - height: 20, - duration: Duration(milliseconds: 1000), - ), - ), - const SizedBox(width: 8), - Text("⭐", style: Theme.of(context).textTheme.titleLarge), - Padding( - padding: const EdgeInsets.all(8.0), - child: AnimatedFlipCounter( - value: displayedLevel, - textStyle: Theme.of(context).textTheme.headlineMedium - ?.copyWith( - fontWeight: FontWeight.bold, - color: AppConfig.goldLight, - ), - duration: const Duration(milliseconds: 1000), - curve: Curves.easeInOut, - ), - ), - ], - ), - ), - const SizedBox(height: 25), - - // Vocab and grammar row - AnimatedBuilder( - animation: _controller, - builder: (_, _) => Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Text( - "+ ${_endVocab - _startVocab}", - style: const TextStyle( - color: Colors.lightGreen, - fontWeight: FontWeight.bold, - ), - ), - Row( - children: [ - Icon( - Symbols.dictionary, - color: colorScheme.primary, - size: 35, - ), - const SizedBox(width: 8), - Text( - '${vocabAnimation.value}', - style: grammarVocabStyle, - ), - ], - ), - ], - ), - const SizedBox(width: 40), - Column( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Text( - "+ ${_endGrammar - _startGrammar}", - style: const TextStyle( - color: Colors.lightGreen, - fontWeight: FontWeight.bold, - ), - ), - Row( - children: [ - Icon( - Symbols.toys_and_games, - color: colorScheme.primary, - size: 35, - ), - const SizedBox(width: 8), - Text( - '${grammarAnimation.value}', - style: grammarVocabStyle, - ), - ], - ), - ], - ), - ], - ), - ), - const SizedBox(height: 16), - if (_loading) - const Center( - child: SizedBox( - height: 50, - width: 50, - child: CircularProgressIndicator( - strokeWidth: 2.0, - color: AppConfig.goldLight, - ), - ), - ) - else if (_error != null) - Padding( - padding: const EdgeInsets.all(16.0), - child: ErrorIndicator( - message: _error!.toLocalizedString(context), - ), - ) - else if (_constructSummary != null) - // Skills section - AnimatedBuilder( - animation: skillsOpacity, - builder: (_, _) => Opacity( - opacity: skillsOpacity.value, - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - _buildSkillsTable(context), - const SizedBox(height: 8), - Padding( - padding: const EdgeInsets.only(top: 16), - child: Text( - _constructSummary!.textSummary, - textAlign: TextAlign.left, - style: Theme.of(context).textTheme.bodyMedium, - ), - ), - Padding( - padding: const EdgeInsets.all(24.0), - child: CachedNetworkImage( - imageUrl: - "${AppConfig.assetsBaseURL}/${LevelUpConstants.dinoLevelUPFileName}", - width: 400, - fit: BoxFit.cover, - ), - ), - ], - ), - ), - ), - // Share button, currently no functionality - // ElevatedButton( - // onPressed: () { - // // Add share functionality - // }, - // style: ElevatedButton.styleFrom( - // backgroundColor: Colors.white, - // foregroundColor: Colors.black, - // padding: const EdgeInsets.symmetric( - // vertical: 12, - // horizontal: 24, - // ), - // shape: RoundedRectangleBorder( - // borderRadius: BorderRadius.circular(8), - // ), - // ), - // child: const Row( - // mainAxisSize: MainAxisSize.min, - // children: [ - // Text( - // "Share with Friends", - // style: TextStyle( - // fontSize: 16, - // fontWeight: FontWeight.bold, - // ), - // ), - // SizedBox( - // width: 8, - // ), - // Icon( - // Icons.ios_share, - // size: 20, - // ), - // ], - // ), - // ), - ], - ), - ), - ], - ); - } - - Widget _buildSkillsTable(BuildContext context) { - final visibleSkills = LearningSkillsEnum.values - .where((skill) => (_getSkillXP(skill) > -1) && skill.isVisible) - .toList(); - - const itemsPerRow = 4; - // chunk into rows of up to 4 - final rows = >[ - for (var i = 0; i < visibleSkills.length; i += itemsPerRow) - visibleSkills.sublist(i, min(i + itemsPerRow, visibleSkills.length)), - ]; - - return Column( - children: rows.map((row) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - crossAxisAlignment: CrossAxisAlignment.start, - children: row.map((skill) { - return Flexible( - fit: FlexFit.loose, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - skill.tooltip(context), - style: const TextStyle( - fontSize: 12, - fontWeight: FontWeight.w600, - ), - textAlign: TextAlign.center, - ), - const SizedBox(height: 4), - Icon( - skill.icon, - size: 25, - color: Theme.of(context).colorScheme.onSurface, - ), - const SizedBox(height: 4), - Text( - '+ ${_getSkillXP(skill)} XP', - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - color: AppConfig.gold, - ), - textAlign: TextAlign.center, - ), - ], - ), - ); - }).toList(), - ), - ); - }).toList(), - ); - } -} diff --git a/lib/pangea/common/network/urls.dart b/lib/pangea/common/network/urls.dart index 06463553a..e3f1f2ae3 100644 --- a/lib/pangea/common/network/urls.dart +++ b/lib/pangea/common/network/urls.dart @@ -74,8 +74,6 @@ class PApiUrls { "${PApiUrls._choreoEndpoint}/token/feedback_v2"; static String morphFeaturesAndTags = "${PApiUrls._choreoEndpoint}/morphs"; - static String constructSummary = - "${PApiUrls._choreoEndpoint}/construct_summary"; ///--------------------------- course translations --------------------------- static String getLocalizedCourse = diff --git a/lib/pangea/constructs/construct_repo.dart b/lib/pangea/constructs/construct_repo.dart deleted file mode 100644 index bedafc4a9..000000000 --- a/lib/pangea/constructs/construct_repo.dart +++ /dev/null @@ -1,129 +0,0 @@ -import 'dart:convert'; - -import 'package:http/http.dart'; - -import 'package:fluffychat/pangea/analytics_misc/constructs_model.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/widgets/matrix.dart'; - -class ConstructSummary { - final int upperLevel; - final int lowerLevel; - int? levelVocabConstructs; - int? levelGrammarConstructs; - final String language; - final String textSummary; - final int writingConstructScore; - final int readingConstructScore; - final int hearingConstructScore; - final int speakingConstructScore; - - ConstructSummary({ - required this.upperLevel, - required this.lowerLevel, - this.levelVocabConstructs, - this.levelGrammarConstructs, - required this.language, - required this.textSummary, - required this.writingConstructScore, - required this.readingConstructScore, - required this.hearingConstructScore, - required this.speakingConstructScore, - }); - - Map toJson() { - return { - 'upper_level': upperLevel, - 'lower_level': lowerLevel, - 'level_grammar_constructs': levelGrammarConstructs, - 'level_vocab_constructs': levelVocabConstructs, - 'language': language, - 'text_summary': textSummary, - 'writing_construct_score': writingConstructScore, - 'reading_construct_score': readingConstructScore, - 'hearing_construct_score': hearingConstructScore, - 'speaking_construct_score': speakingConstructScore, - }; - } - - factory ConstructSummary.fromJson(Map json) { - return ConstructSummary( - upperLevel: json['upper_level'], - lowerLevel: json['lower_level'], - levelGrammarConstructs: json['level_grammar_constructs'], - levelVocabConstructs: json['level_vocab_constructs'], - language: json['language'], - textSummary: json['text_summary'], - writingConstructScore: json['writing_construct_score'], - readingConstructScore: json['reading_construct_score'], - hearingConstructScore: json['hearing_construct_score'], - speakingConstructScore: json['speaking_construct_score'], - ); - } -} - -class ConstructSummaryRequest { - final List constructs; - final List> messages; - final String userL1; - final String userL2; - final int upperLevel; - final int lowerLevel; - - ConstructSummaryRequest({ - required this.constructs, - required this.messages, - required this.userL1, - required this.userL2, - required this.upperLevel, - required this.lowerLevel, - }); - - Map toJson() { - return { - 'constructs': constructs.map((construct) => construct.toJson()).toList(), - 'msgs': messages, - 'user_l1': userL1, - 'user_l2': userL2, - 'language': userL1, - 'upper_level': upperLevel, - 'lower_level': lowerLevel, - }; - } -} - -class ConstructSummaryResponse { - final ConstructSummary summary; - - ConstructSummaryResponse({required this.summary}); - - Map toJson() { - return {'summary': summary.toJson()}; - } - - factory ConstructSummaryResponse.fromJson(Map json) { - return ConstructSummaryResponse( - summary: ConstructSummary.fromJson(json['summary']), - ); - } -} - -class ConstructRepo { - static Future generateConstructSummary( - ConstructSummaryRequest request, - ) async { - final Requests req = Requests( - choreoApiKey: Environment.choreoApiKey, - accessToken: MatrixState.pangeaController.userController.accessToken, - ); - final Response res = await req.post( - url: PApiUrls.constructSummary, - body: request.toJson(), - ); - final decodedBody = jsonDecode(utf8.decode(res.bodyBytes)); - final response = ConstructSummaryResponse.fromJson(decodedBody); - return response; - } -} diff --git a/lib/pangea/events/constants/pangea_event_types.dart b/lib/pangea/events/constants/pangea_event_types.dart index 0f269270b..580a3a291 100644 --- a/lib/pangea/events/constants/pangea_event_types.dart +++ b/lib/pangea/events/constants/pangea_event_types.dart @@ -1,7 +1,6 @@ class PangeaEventTypes { static const construct = "pangea.construct"; static const userSetLemmaInfo = "p.user_lemma_info"; - static const constructSummary = "pangea.construct_summary"; static const userChosenEmoji = "p.emoji"; static const tokens = "pangea.tokens"; diff --git a/lib/utils/client_manager.dart b/lib/utils/client_manager.dart index 78a5a6406..56844ce80 100644 --- a/lib/utils/client_manager.dart +++ b/lib/utils/client_manager.dart @@ -140,7 +140,6 @@ abstract class ClientManager { PangeaEventTypes.activityPlan, PangeaEventTypes.activityRole, PangeaEventTypes.activitySummary, - PangeaEventTypes.constructSummary, PangeaEventTypes.activityRoomIds, PangeaEventTypes.coursePlan, PangeaEventTypes.teacherMode,