3873 add superlatives for most vocab grammar and xp (#3977)

* add grammar and vocab superlatives

- adds superlatives to the summary cards for each user
- WIP, doesn't include XP superlative yet and it continuously reinitializes state which reloads the superlatives

* get analytic superlatives from saved state event

Revert activity user summaries widget to stateless, get info from saved state event instead of awaiting it, and fix some spacing issues.

* add xp superlative

- simplify 3 loop logic into one
- change from constant updates to only generate superlatives on analytics save

* sort imports

* put analytics loading inside buttonControlledCarouselView

* delete commented out code

* return superlative map instead of setting value
This commit is contained in:
avashilling 2025-09-15 10:57:17 -04:00 committed by GitHub
parent 6c05ffaf2a
commit 617030cd78
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 127 additions and 28 deletions

View file

@ -127,7 +127,6 @@ extension ActivityRoomExtension on Room {
if (activitySummary?.summary != null) {
return;
}
await setActivitySummary(
ActivitySummaryModel(
requestedAt: DateTime.now(),

View file

@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:collection/collection.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/config/app_config.dart';
@ -87,10 +88,10 @@ class ButtonControlledCarouselView extends StatelessWidget {
@override
Widget build(BuildContext context) {
final room = controller.room;
final superlatives =
room.activitySummary?.analytics?.generateSuperlatives();
final availableRoles = room.activityPlan!.roles;
final assignedRoles = room.assignedRoles ?? {};
final userSummaries = summary.participants
.where(
(p) => assignedRoles.values.any(
@ -160,21 +161,17 @@ class ButtonControlledCarouselView extends StatelessWidget {
),
),
),
Row(
spacing: 14.0,
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.all(4.0),
child: Row(
spacing: 4.0,
mainAxisSize: MainAxisSize.min,
Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Wrap(
alignment: WrapAlignment.center,
spacing: 12,
runSpacing: 8,
//crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Icon(
Icons.school,
size: 12.0,
color: AppConfig.yellowDark,
),
Text(
p.cefrLevel,
style: const TextStyle(
@ -182,18 +179,42 @@ class ButtonControlledCarouselView extends StatelessWidget {
fontSize: 12.0,
),
),
//const SizedBox(width: 8),
if (superlatives != null &&
(superlatives['vocab']!.contains(
p.participantId,
))) ...[
const SuperlativeTile(
icon: Symbols.dictionary,
),
],
if (superlatives != null &&
(superlatives['grammar']!.contains(
p.participantId,
))) ...[
const SuperlativeTile(
icon: Symbols.toys_and_games,
),
],
if (superlatives != null &&
(superlatives['xp']!.contains(
p.participantId,
))) ...[
const SuperlativeTile(
icon: Icons.star,
),
],
if (p.superlatives.isNotEmpty) ...[
//const SizedBox(width: 8),
Text(
p.superlatives.first,
style: const TextStyle(fontSize: 12.0),
),
],
],
),
),
if (p.superlatives.isNotEmpty)
Padding(
padding: const EdgeInsets.all(4.0),
child: Text(
p.superlatives.first,
style: const TextStyle(fontSize: 12.0),
),
),
],
],
),
),
],
),
@ -229,3 +250,30 @@ class ButtonControlledCarouselView extends StatelessWidget {
);
}
}
class SuperlativeTile extends StatelessWidget {
final IconData icon;
const SuperlativeTile({
super.key,
required this.icon,
});
@override
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 14, color: AppConfig.gold),
const SizedBox(width: 2),
const Text(
"1st",
style: TextStyle(
color: AppConfig.gold,
fontSize: 12.0,
),
),
],
);
}
}

View file

@ -55,9 +55,61 @@ class ActivitySummaryAnalyticsModel {
}
}
Map<String, List> generateSuperlatives() {
final Map<String, List<String>> superlatives = {
'vocab': [],
'grammar': [],
'xp': [],
};
// Find all user IDs
final userIds = constructs.keys.toList();
if (userIds.isEmpty) {
return superlatives;
}
int maxVocab = 0;
int maxGrammar = 0;
int maxXp = 0;
final Map<String, int> allVocabs = {};
final Map<String, int> allGrammars = {};
final Map<String, int> allXPs = {};
for (final userId in userIds) {
//vocab
final vocabCount =
uniqueConstructCountForUser(userId, ConstructTypeEnum.vocab);
allVocabs[userId] = vocabCount;
if (vocabCount > maxVocab) maxVocab = vocabCount;
//grammar
final grammarCount =
uniqueConstructCountForUser(userId, ConstructTypeEnum.morph);
allGrammars[userId] = grammarCount;
if (grammarCount > maxGrammar) maxGrammar = grammarCount;
//XP
final xpCount = totalXPForUser(userId);
allXPs[userId] = xpCount;
if (xpCount > maxXp) maxXp = xpCount;
}
superlatives['vocab'] = allVocabs.entries
.where((e) => e.value == maxVocab && maxVocab > 0)
.map((e) => e.key)
.toList();
superlatives['grammar'] = allGrammars.entries
.where((e) => e.value == maxGrammar && maxGrammar > 0)
.map((e) => e.key)
.toList();
superlatives['xp'] = allXPs.entries
.where((e) => e.value == maxXp && maxXp > 0)
.map((e) => e.key)
.toList();
return superlatives;
}
factory ActivitySummaryAnalyticsModel.fromJson(Map<String, dynamic> json) {
final model = ActivitySummaryAnalyticsModel();
for (final userEntry in json.entries) {
final userId = userEntry.key;
final constructList = userEntry.value as List<dynamic>;