Skip to content

Commit b104069

Browse files
WilsonLegithub-actions[bot]ggurdin
authored
feat: level up summary (#2182)
* remove send local analytics to matrix on level up * complete implementation of level up summary * generated * fix model key issues that prevents parsing request and response * fix env * generated * improve level up summary to utilize existing state event * generated --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: ggurdin <46800240+ggurdin@users.noreply.github.com>
1 parent bf102a3 commit b104069

File tree

10 files changed

+447
-110
lines changed

10 files changed

+447
-110
lines changed

assets/l10n/intl_en.arb

+11-1
Original file line numberDiff line numberDiff line change
@@ -4814,8 +4814,18 @@
48144814
"home": "Home",
48154815
"join": "Join",
48164816
"learnByTexting": "Learn by texting",
4817+
"levelSummaryTrigger": "View summary",
4818+
"levelSummaryPopupTitle": "Level {level} Summary",
4819+
"@levelSummaryPopupTitle": {
4820+
"type": "String",
4821+
"placeholders": {
4822+
"level": {
4823+
"type": "int"
4824+
}
4825+
}
4826+
},
48174827
"startChatting": "Start chatting",
48184828
"referFriends": "Refer friends",
48194829
"referFriendDialogTitle": "Invite a friend to your conversation",
48204830
"referFriendDialogDesc": "Do you have a friend who is excited to learn a new language with you? Then copy and send this invitation link to join and start chatting with you today."
4821-
}
4831+
}

assets/l10n/intl_vi.arb

+10
Original file line numberDiff line numberDiff line change
@@ -3793,5 +3793,15 @@
37933793
"@createASpace": {
37943794
"type": "String",
37953795
"placeholders": {}
3796+
},
3797+
"levelSummaryTrigger": "Đọc báo cáo",
3798+
"levelSummaryPopupTitle": "Tóm tắt cấp {level}",
3799+
"@levelSummaryPopupTitle": {
3800+
"type": "String",
3801+
"placeholders": {
3802+
"level": {
3803+
"type": "int"
3804+
}
3805+
}
37963806
}
37973807
}

lib/pages/chat/chat.dart

+22-10
Original file line numberDiff line numberDiff line change
@@ -365,18 +365,30 @@ class ChatController extends State<ChatPageWithRoom>
365365

366366
_levelSubscription = pangeaController.getAnalytics.stateStream
367367
.where(
368-
(update) =>
369-
update is Map<String, dynamic> && update['level_up'] != null,
370-
)
368+
(update) => update is Map<String, dynamic> && update['level_up'] != null,
369+
)
370+
// .listen(
371+
// (update) => Future.delayed(
372+
// const Duration(milliseconds: 500),
373+
// () => LevelUpUtil.showLevelUpDialog(
374+
// update['level_up'],
375+
// context,
376+
// ),
377+
// ),
378+
// )
371379
.listen(
372-
(update) => Future.delayed(
373-
const Duration(milliseconds: 500),
374-
() => LevelUpUtil.showLevelUpDialog(
375-
update['level_up'],
376-
context,
377-
),
378-
),
380+
// remove delay now that GetAnalyticsController._onLevelUp
381+
// is async is should take roughly 500ms to make requests anyway
382+
(update) {
383+
LevelUpUtil.showLevelUpDialog(
384+
update['level_up'],
385+
update['analytics_room_id'],
386+
update["construct_summary_state_event_id"],
387+
update['construct_summary'],
388+
context,
379389
);
390+
},
391+
);
380392
// Pangea#
381393
_tryLoadTimeline();
382394
if (kIsWeb) {

lib/pangea/analytics_misc/get_analytics_controller.dart

+105-6
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import 'package:fluffychat/pangea/common/constants/local.key.dart';
1818
import 'package:fluffychat/pangea/common/controllers/base_controller.dart';
1919
import 'package:fluffychat/pangea/common/controllers/pangea_controller.dart';
2020
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
21+
import 'package:fluffychat/pangea/constructs/construct_repo.dart';
22+
import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart';
2123
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
2224
import 'package:fluffychat/pangea/learning_settings/models/language_model.dart';
2325

@@ -141,8 +143,12 @@ class GetAnalyticsController extends BaseController {
141143
if (analyticsUpdate.type == AnalyticsUpdateType.server) {
142144
await _getConstructs(forceUpdate: true);
143145
}
144-
if (oldLevel < constructListModel.level) _onLevelUp();
145-
if (oldLevel > constructListModel.level) await _onLevelDown(oldLevel);
146+
if (oldLevel < constructListModel.level) {
147+
await _onLevelUp(oldLevel, constructListModel.level);
148+
}
149+
if (oldLevel > constructListModel.level) {
150+
await _onLevelDown(constructListModel.level, oldLevel);
151+
}
146152
_updateAnalyticsStream(origin: analyticsUpdate.origin);
147153
// Update public profile each time that new analytics are added.
148154
// If the level hasn't changed, this will not send an update to the server.
@@ -158,13 +164,22 @@ class GetAnalyticsController extends BaseController {
158164
}) =>
159165
analyticsStream.add(AnalyticsStreamUpdate(origin: origin));
160166

161-
void _onLevelUp() {
162-
setState({'level_up': constructListModel.level});
167+
Future<void> _onLevelUp(final int lowerLevel, final int upperLevel) async {
168+
final result = await _generateLevelUpAnalyticsAndSaveToStateEvent(
169+
lowerLevel,
170+
upperLevel,
171+
);
172+
setState({
173+
'level_up': constructListModel.level,
174+
'analytics_room_id': _client.analyticsRoomLocal(_l2!)?.id,
175+
"construct_summary_state_event_id": result?.stateEventId,
176+
"construct_summary": result?.summary,
177+
});
163178
}
164179

165-
Future<void> _onLevelDown(final prevLevel) async {
180+
Future<void> _onLevelDown(final int lowerLevel, final int upperLevel) async {
166181
final offset =
167-
_calculateMinXpForLevel(prevLevel) - constructListModel.totalXP;
182+
_calculateMinXpForLevel(lowerLevel) - constructListModel.totalXP;
168183
await _pangeaController.userController.addXPOffset(offset);
169184
constructListModel.updateConstructs(
170185
[],
@@ -344,6 +359,90 @@ class GetAnalyticsController extends BaseController {
344359
);
345360
_cache.add(entry);
346361
}
362+
363+
Future<GenerateConstructSummaryResult?>
364+
_generateLevelUpAnalyticsAndSaveToStateEvent(
365+
final int lowerLevel,
366+
final int upperLevel,
367+
) async {
368+
// generate level up analytics as a construct summary
369+
ConstructSummary summary;
370+
try {
371+
final int maxXP = _calculateMinXpForLevel(upperLevel);
372+
final int minXP = _calculateMinXpForLevel(lowerLevel);
373+
int diffXP = maxXP - minXP;
374+
if (diffXP < 0) diffXP = 0;
375+
376+
// compute construct use of current level
377+
final List<OneConstructUse> constructUseOfCurrentLevel = [];
378+
int score = 0;
379+
for (final use in constructListModel.uses) {
380+
constructUseOfCurrentLevel.add(use);
381+
score += use.pointValue;
382+
if (score >= diffXP) break;
383+
}
384+
385+
// extract construct use message bodies for analytics
386+
List<String?>? constructUseMessageContentBodies = [];
387+
for (final use in constructUseOfCurrentLevel) {
388+
try {
389+
final useMessage = await use.getEvent(_client);
390+
final useMessageBody = useMessage?.content["body"];
391+
if (useMessageBody is String) {
392+
constructUseMessageContentBodies.add(useMessageBody);
393+
} else {
394+
constructUseMessageContentBodies.add(null);
395+
}
396+
} catch (e) {
397+
constructUseMessageContentBodies.add(null);
398+
}
399+
}
400+
if (constructUseMessageContentBodies.length !=
401+
constructUseOfCurrentLevel.length) {
402+
constructUseMessageContentBodies = null;
403+
}
404+
405+
final request = ConstructSummaryRequest(
406+
constructs: constructUseOfCurrentLevel,
407+
constructUseMessageContentBodies: constructUseMessageContentBodies,
408+
language: _l2!.langCodeShort,
409+
upperLevel: upperLevel,
410+
lowerLevel: lowerLevel,
411+
);
412+
413+
final response = await ConstructRepo.generateConstructSummary(request);
414+
summary = response.summary;
415+
} catch (e) {
416+
debugPrint("Error generating level up analytics: $e");
417+
ErrorHandler.logError(e: e, data: {'e': e});
418+
return null;
419+
}
420+
String stateEventId;
421+
try {
422+
final Room? analyticsRoom = _client.analyticsRoomLocal(_l2!);
423+
if (analyticsRoom == null) {
424+
ErrorHandler.logError(
425+
e: e,
426+
data: {'e': e, 'message': "Analytics room not found for user"},
427+
);
428+
return null;
429+
}
430+
stateEventId = await _client.setRoomStateWithKey(
431+
analyticsRoom.id,
432+
PangeaEventTypes.constructSummary,
433+
'',
434+
summary.toJson(),
435+
);
436+
} catch (e) {
437+
debugPrint("Error saving construct summary room: $e");
438+
ErrorHandler.logError(e: e, data: {'e': e});
439+
return null;
440+
}
441+
return GenerateConstructSummaryResult(
442+
stateEventId: stateEventId,
443+
summary: summary,
444+
);
445+
}
347446
}
348447

349448
class AnalyticsCacheEntry {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import 'package:flutter/material.dart';
2+
3+
import 'package:flutter_gen/gen_l10n/l10n.dart';
4+
import 'package:matrix/matrix.dart';
5+
6+
import 'package:fluffychat/pangea/constructs/construct_repo.dart';
7+
import 'package:fluffychat/widgets/matrix.dart';
8+
9+
// New component renamed to ConstructSummaryAlertDialog with a max width
10+
class ConstructSummaryAlertDialog extends StatelessWidget {
11+
final String title;
12+
final String content;
13+
14+
const ConstructSummaryAlertDialog({
15+
super.key,
16+
required this.title,
17+
required this.content,
18+
});
19+
20+
@override
21+
Widget build(BuildContext context) {
22+
return AlertDialog(
23+
title: Text(title),
24+
content: ConstrainedBox(
25+
constraints: const BoxConstraints(maxWidth: 400),
26+
child: Text(content),
27+
),
28+
actions: [
29+
TextButton(
30+
onPressed: () => Navigator.of(context).pop(),
31+
child: Text(L10n.of(context).close),
32+
),
33+
],
34+
);
35+
}
36+
}
37+
38+
class LevelSummaryDialog extends StatelessWidget {
39+
final int level;
40+
final String analyticsRoomId;
41+
final String summaryStateEventId;
42+
final ConstructSummary? constructSummary;
43+
44+
const LevelSummaryDialog({
45+
super.key,
46+
required this.analyticsRoomId,
47+
required this.level,
48+
required this.summaryStateEventId,
49+
this.constructSummary,
50+
});
51+
52+
@override
53+
Widget build(BuildContext context) {
54+
final Client client = Matrix.of(context).client;
55+
final futureSummary = client
56+
.getOneRoomEvent(analyticsRoomId, summaryStateEventId)
57+
.then((rawEvent) => ConstructSummary.fromJson(rawEvent.content));
58+
if (constructSummary != null) {
59+
return ConstructSummaryAlertDialog(
60+
title: L10n.of(context).levelSummaryPopupTitle(level),
61+
content: constructSummary!.textSummary,
62+
);
63+
} else {
64+
return FutureBuilder<ConstructSummary>(
65+
future: futureSummary,
66+
builder: (context, snapshot) {
67+
if (snapshot.connectionState == ConnectionState.waiting) {
68+
return const Center(child: CircularProgressIndicator());
69+
} else if (snapshot.hasError) {
70+
return ConstructSummaryAlertDialog(
71+
title: L10n.of(context).levelSummaryPopupTitle(level),
72+
content: L10n.of(context).error502504Desc,
73+
);
74+
} else if (snapshot.hasData) {
75+
final constructSummary = snapshot.data!;
76+
return ConstructSummaryAlertDialog(
77+
title: L10n.of(context).levelSummaryPopupTitle(level),
78+
content: constructSummary.textSummary,
79+
);
80+
} else {
81+
return const SizedBox.shrink();
82+
}
83+
},
84+
);
85+
}
86+
}
87+
}

0 commit comments

Comments
 (0)