Merge pull request #3317 from pangeachat/load-level-xp

When level summary is loading, show loading instead of 0 xp
pull/2245/head
ggurdin 4 months ago committed by GitHub
commit 6550cdfeaa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -474,100 +474,87 @@ class GetAnalyticsController extends BaseController {
}
}
Future<ConstructSummary?> generateLevelUpAnalytics(
Future<ConstructSummary> generateLevelUpAnalytics(
final int lowerLevel,
final int upperLevel,
) async {
// generate level up analytics as a construct summary
ConstructSummary summary;
try {
final int maxXP = constructListModel.calculateXpWithLevel(upperLevel);
final int minXP = constructListModel.calculateXpWithLevel(lowerLevel);
int diffXP = maxXP - minXP;
if (diffXP < 0) diffXP = 0;
// compute construct use of current level
final List<OneConstructUse> constructUseOfCurrentLevel = [];
int score = 0;
for (final use in constructListModel.uses) {
constructUseOfCurrentLevel.add(use);
score += use.xp;
if (score >= diffXP) break;
}
final int maxXP = constructListModel.calculateXpWithLevel(upperLevel);
final int minXP = constructListModel.calculateXpWithLevel(lowerLevel);
int diffXP = maxXP - minXP;
if (diffXP < 0) diffXP = 0;
// compute construct use of current level
final List<OneConstructUse> constructUseOfCurrentLevel = [];
int score = 0;
for (final use in constructListModel.uses) {
constructUseOfCurrentLevel.add(use);
score += use.xp;
if (score >= diffXP) break;
}
// extract construct use message bodies for analytics
final Map<String, Set<String>> useEventIds = {};
for (final use in constructUseOfCurrentLevel) {
if (use.metadata.roomId == null) continue;
if (use.metadata.eventId == null) continue;
useEventIds[use.metadata.roomId!] ??= {};
useEventIds[use.metadata.roomId!]!.add(use.metadata.eventId!);
}
// extract construct use message bodies for analytics
final Map<String, Set<String>> useEventIds = {};
for (final use in constructUseOfCurrentLevel) {
if (use.metadata.roomId == null) continue;
if (use.metadata.eventId == null) continue;
useEventIds[use.metadata.roomId!] ??= {};
useEventIds[use.metadata.roomId!]!.add(use.metadata.eventId!);
}
final List<String?> constructUseMessageContentBodies = [];
for (final entry in useEventIds.entries) {
final String roomId = entry.key;
final room = _client.getRoomById(roomId);
if (room == null) continue;
final List<String?> messageBodies = [];
for (final eventId in entry.value) {
try {
final Event? event = await room.getEventById(eventId);
if (event?.content["body"] is! String) continue;
final String body = event?.content["body"] as String;
if (body.isEmpty) continue;
messageBodies.add(body);
} catch (e, s) {
debugPrint("Error getting event by ID: $e");
ErrorHandler.logError(
e: e,
s: s,
data: {
'roomId': roomId,
'eventId': eventId,
},
);
continue;
}
final List<String?> constructUseMessageContentBodies = [];
for (final entry in useEventIds.entries) {
final String roomId = entry.key;
final room = _client.getRoomById(roomId);
if (room == null) continue;
final List<String?> messageBodies = [];
for (final eventId in entry.value) {
try {
final Event? event = await room.getEventById(eventId);
if (event?.content["body"] is! String) continue;
final String body = event?.content["body"] as String;
if (body.isEmpty) continue;
messageBodies.add(body);
} catch (e, s) {
debugPrint("Error getting event by ID: $e");
ErrorHandler.logError(
e: e,
s: s,
data: {
'roomId': roomId,
'eventId': eventId,
},
);
continue;
}
constructUseMessageContentBodies.addAll(messageBodies);
}
final request = ConstructSummaryRequest(
constructs: constructUseOfCurrentLevel,
constructUseMessageContentBodies: constructUseMessageContentBodies,
language: _l1!.langCodeShort,
upperLevel: upperLevel,
lowerLevel: lowerLevel,
);
final response = await ConstructRepo.generateConstructSummary(request);
summary = response.summary;
summary.levelVocabConstructs = MatrixState
.pangeaController.getAnalytics.constructListModel.vocabLemmas;
summary.levelGrammarConstructs = MatrixState
.pangeaController.getAnalytics.constructListModel.grammarLemmas;
} catch (e) {
debugPrint("Error generating level up analytics: $e");
ErrorHandler.logError(e: e, data: {'e': e});
return null;
constructUseMessageContentBodies.addAll(messageBodies);
}
try {
final Room? analyticsRoom = await _client.getMyAnalyticsRoom(_l2!);
if (analyticsRoom == null) {
throw "Analytics room not found for user";
}
final request = ConstructSummaryRequest(
constructs: constructUseOfCurrentLevel,
constructUseMessageContentBodies: constructUseMessageContentBodies,
language: _l1!.langCodeShort,
upperLevel: upperLevel,
lowerLevel: lowerLevel,
);
// don't await this, just return the original response
_saveConstructSummaryResponseToStateEvent(
summary,
);
} catch (e, s) {
debugPrint("Error saving construct summary room: $e");
ErrorHandler.logError(e: e, s: s, data: {'e': e});
final response = await ConstructRepo.generateConstructSummary(request);
final ConstructSummary summary = response.summary;
summary.levelVocabConstructs = MatrixState
.pangeaController.getAnalytics.constructListModel.vocabLemmas;
summary.levelGrammarConstructs = MatrixState
.pangeaController.getAnalytics.constructListModel.grammarLemmas;
final Room? analyticsRoom = await _client.getMyAnalyticsRoom(_l2!);
if (analyticsRoom == null) {
throw "Analytics room not found for user";
}
// don't await this, just return the original response
_saveConstructSummaryResponseToStateEvent(
summary,
);
return summary;
}
}

@ -12,6 +12,7 @@ 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/config/environment.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 {
@ -95,10 +96,15 @@ class LevelUpBannerState extends State<LevelUpBanner>
bool _showedDetails = false;
final Completer<ConstructSummary> _constructSummaryCompleter =
Completer<ConstructSummary>();
@override
void initState() {
super.initState();
_loadConstructSummary();
LevelUpManager.instance.preloadAnalytics(
context,
widget.level,
@ -149,10 +155,23 @@ class LevelUpBannerState extends State<LevelUpBanner>
await showDialog(
context: context,
builder: (context) => const LevelUpPopup(),
builder: (context) => LevelUpPopup(
constructSummaryCompleter: _constructSummaryCompleter,
),
);
}
Future<void> _loadConstructSummary() async {
try {
final summary = MatrixState.pangeaController.getAnalytics
.generateLevelUpAnalytics(widget.prevLevel, widget.level);
_constructSummaryCompleter.complete(summary);
} catch (e) {
debugPrint("Error generating level up analytics: $e");
_constructSummaryCompleter.completeError(e);
}
}
@override
Widget build(BuildContext context) {
final isColumnMode = FluffyThemes.isColumnMode(context);

@ -22,13 +22,8 @@ class LevelUpManager {
int prevVocab = 0;
int nextVocab = 0;
String? userL2Code;
ConstructSummary? constructSummary;
bool hasSeenPopup = false;
bool shouldAutoPopup = false;
String? error;
Future<void> preloadAnalytics(
BuildContext context,
@ -46,12 +41,6 @@ class LevelUpManager {
nextVocab = MatrixState
.pangeaController.getAnalytics.constructListModel.vocabLemmas;
userL2Code = MatrixState.pangeaController.languageController
.activeL2Code()
?.toUpperCase();
getConstructFromLevelUp();
final LanguageModel? l2 =
MatrixState.pangeaController.languageController.userL2;
final Room? analyticsRoom =
@ -91,28 +80,6 @@ class LevelUpManager {
}
}
//for testing, just fetch last level up from saved analytics
void getConstructFromButton() {
constructSummary = MatrixState.pangeaController.getAnalytics
.getConstructSummaryFromStateEvent();
debugPrint(
"Last saved construct summary from analytics controller function: ${constructSummary?.toJson()}",
);
}
//for getting real level up data when leveled up
void getConstructFromLevelUp() async {
try {
constructSummary = await MatrixState.pangeaController.getAnalytics
.generateLevelUpAnalytics(
prevLevel,
level,
);
} catch (e) {
error = e.toString();
}
}
void markPopupSeen() {
hasSeenPopup = true;
shouldAutoPopup = false;
@ -127,7 +94,5 @@ class LevelUpManager {
nextGrammar = 0;
prevVocab = 0;
nextVocab = 0;
constructSummary = null;
error = null;
}
}

@ -20,12 +20,15 @@ import 'package:fluffychat/pangea/analytics_summary/progress_bar/level_bar.dart'
import 'package:fluffychat/pangea/analytics_summary/progress_bar/progress_bar_details.dart';
import 'package:fluffychat/pangea/common/widgets/full_width_dialog.dart';
import 'package:fluffychat/pangea/constructs/construct_repo.dart';
import 'package:fluffychat/pangea/learning_settings/constants/language_constants.dart';
import 'package:fluffychat/widgets/avatar.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:fluffychat/widgets/mxc_image.dart';
class LevelUpPopup extends StatelessWidget {
final Completer<ConstructSummary> constructSummaryCompleter;
const LevelUpPopup({
required this.constructSummaryCompleter,
super.key,
});
@ -50,6 +53,7 @@ class LevelUpPopup extends StatelessWidget {
body: LevelUpPopupContent(
prevLevel: LevelUpManager.instance.prevLevel,
level: LevelUpManager.instance.level,
constructSummaryCompleter: constructSummaryCompleter,
),
),
);
@ -59,11 +63,13 @@ class LevelUpPopup extends StatelessWidget {
class LevelUpPopupContent extends StatefulWidget {
final int prevLevel;
final int level;
final Completer<ConstructSummary> constructSummaryCompleter;
const LevelUpPopupContent({
super.key,
required this.prevLevel,
required this.level,
required this.constructSummaryCompleter,
});
@override
@ -72,55 +78,39 @@ class LevelUpPopupContent extends StatefulWidget {
class _LevelUpPopupContentState extends State<LevelUpPopupContent>
with SingleTickerProviderStateMixin {
late int _endGrammar;
late int _endVocab;
final int _startGrammar = LevelUpManager.instance.prevGrammar;
final int _startVocab = LevelUpManager.instance.prevVocab;
Timer? _summaryPollTimer;
final String? _error = LevelUpManager.instance.error;
String language = LevelUpManager.instance.userL2Code ?? "N/A";
late final AnimationController _controller;
late final ConfettiController _confettiController;
bool _hasBlastedConfetti = false;
final Duration _animationDuration = const Duration(seconds: 5);
Uri? avatarUrl;
late final Future<Profile> profile;
int displayedLevel = -1;
late ConstructSummary? _constructSummary;
Uri? avatarUrl;
bool _hasBlastedConfetti = false;
String language = MatrixState.pangeaController.languageController
.activeL2Code()
?.toUpperCase() ??
LanguageKeys.unknownLanguage;
ConstructSummary? _constructSummary;
Object? _error;
bool _loading = true;
@override
void initState() {
super.initState();
_loadConstructSummary();
LevelUpManager.instance.markPopupSeen();
displayedLevel = widget.prevLevel;
_confettiController =
ConfettiController(duration: const Duration(seconds: 1));
_endGrammar = LevelUpManager.instance.nextGrammar;
_endVocab = LevelUpManager.instance.nextVocab;
_constructSummary = LevelUpManager.instance.constructSummary;
// Poll for constructSummary if not available
if (_constructSummary == null) {
_summaryPollTimer =
Timer.periodic(const Duration(milliseconds: 300), (timer) {
final summary = LevelUpManager.instance.constructSummary;
if (summary != null) {
setState(() {
_constructSummary = summary;
});
timer.cancel();
}
});
}
final client = Matrix.of(context).client;
client.fetchOwnProfile().then((profile) {
setState(() {
avatarUrl = profile.avatarUrl;
});
setState(() => avatarUrl = profile.avatarUrl);
});
_controller = AnimationController(
duration: _animationDuration,
duration: const Duration(seconds: 5),
vsync: this,
);
@ -135,7 +125,6 @@ class _LevelUpPopupContentState extends State<LevelUpPopupContent>
_controller.addListener(() {
if (_controller.value >= 0.5 && !_hasBlastedConfetti) {
//_confettiController.play();
_hasBlastedConfetti = true;
rainConfetti(context);
}
@ -146,7 +135,6 @@ class _LevelUpPopupContentState extends State<LevelUpPopupContent>
@override
void dispose() {
_summaryPollTimer?.cancel();
_controller.dispose();
_confettiController.dispose();
LevelUpManager.instance.reset();
@ -154,6 +142,22 @@ class _LevelUpPopupContentState extends State<LevelUpPopupContent>
super.dispose();
}
int get _startGrammar => LevelUpManager.instance.prevGrammar;
int get _startVocab => LevelUpManager.instance.prevVocab;
get _endGrammar => LevelUpManager.instance.nextGrammar;
get _endVocab => LevelUpManager.instance.nextVocab;
Future<void> _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) {
@ -368,52 +372,60 @@ class _LevelUpPopupContentState extends State<LevelUpPopupContent>
),
),
const SizedBox(height: 16),
// Skills section
AnimatedBuilder(
animation: skillsOpacity,
builder: (_, __) => Opacity(
opacity: skillsOpacity.value,
child: _error == null
? Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
_buildSkillsTable(context),
const SizedBox(height: 8),
Padding(
padding: const EdgeInsets.only(top: 16),
child: Text(
_constructSummary?.textSummary ??
L10n.of(context).loadingPleaseWait,
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,
),
),
],
)
// if error getting construct summary
: Row(
children: [
Tooltip(
message: L10n.of(context).oopsSomethingWentWrong,
child: Icon(
Icons.error,
color: Theme.of(context).colorScheme.error,
),
),
],
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: Text(
L10n.of(context).oopsSomethingWentWrong,
style: TextStyle(
color: Theme.of(context).colorScheme.error,
fontSize: 16,
),
),
)
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: () {

Loading…
Cancel
Save