1380 content challenges (#1391)

* use gold consistently for positive xp color

* fix: dont point to local choreo
pull/1593/head
wcjord 10 months ago committed by GitHub
parent a26895ad81
commit 8084cc24cc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -16,6 +16,7 @@ analyzer:
exclude:
- lib/generated_plugin_registrant.dart
- lib/l10n/*.dart
- assets/l10n/*.arb
dart_code_metrics:
metrics:

@ -4659,7 +4659,7 @@
"publicProfileTitle": "Allow my profile to be found in search",
"publicProfileDesc": "By enabling this option, I confirm that I am of legal age in my country of residence",
"clickWordsInstructions": "Click on individual words for more activities.",
"chooseBestDefinition": "Choose the best definition",
"chooseBestDefinition": "What does this word mean?",
"chooseBaseForm": "Choose the base form",
"notTheCodeError": "Sorry, that's not the code!",
"totalXP": "Total XP",

@ -1,12 +1,4 @@
import 'package:flutter/material.dart';
import 'package:collection/collection.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:get_storage/get_storage.dart';
import 'package:matrix/matrix.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pangea/config/environment.dart';
import 'package:fluffychat/pangea/controllers/language_list_controller.dart';
@ -15,6 +7,13 @@ import 'package:fluffychat/pangea/utils/firebase_analytics.dart';
import 'package:fluffychat/utils/client_manager.dart';
import 'package:fluffychat/utils/platform_infos.dart';
import 'package:fluffychat/widgets/error_widget.dart';
import 'package:flutter/material.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:get_storage/get_storage.dart';
import 'package:matrix/matrix.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'config/setting_keys.dart';
import 'utils/background_push.dart';
import 'widgets/fluffy_chat_app.dart';

@ -1,12 +1,7 @@
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:badges/badges.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:go_router/go_router.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pages/chat/chat.dart';
import 'package:fluffychat/pages/chat/chat_app_bar_list_tile.dart';
@ -29,6 +24,11 @@ import 'package:fluffychat/widgets/future_loading_dialog.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:fluffychat/widgets/mxc_image.dart';
import 'package:fluffychat/widgets/unread_rooms_badge.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:go_router/go_router.dart';
import 'package:matrix/matrix.dart';
import '../../utils/stream_extension.dart';
enum _EventContextAction { info, report }
@ -445,10 +445,8 @@ class ChatView extends StatelessWidget {
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
PointsGainedAnimation(
gainColor: Theme.of(context)
.colorScheme
.onPrimary,
const PointsGainedAnimation(
gainColor: AppConfig.gold,
origin:
AnalyticsUpdateOrigin.sendMessage,
),

@ -0,0 +1,25 @@
import 'package:fluffychat/config/app_config.dart';
import 'package:flutter/material.dart';
import 'package:shimmer/shimmer.dart';
class TextLoadingShimmer extends StatelessWidget {
final double width;
const TextLoadingShimmer({
super.key,
this.width = 140.0,
});
@override
Widget build(BuildContext context) {
return Shimmer.fromColors(
baseColor: Colors.transparent, // Base color of the shimmer effect
// for higlight, use white with 50 opacity
highlightColor: AppConfig.primaryColor.withAlpha(70),
child: Container(
height: AppConfig.messageFontSize * AppConfig.fontSizeFactor,
width: width, // Width of the rectangle
color: AppConfig.primaryColor, // Background color of the rectangle
),
);
}
}

@ -1,12 +1,11 @@
import 'dart:math';
import 'package:flutter/foundation.dart';
import 'package:fluffychat/pangea/controllers/get_analytics_controller.dart';
import 'package:fluffychat/pangea/enum/activity_type_enum.dart';
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
import 'package:fluffychat/pangea/models/pangea_token_model.dart';
import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart';
import 'package:flutter/foundation.dart';
/// Picks which tokens to do activities on and what types of activities to do
/// Caches result so that we don't have to recompute it
@ -160,9 +159,11 @@ class MessageAnalyticsEntry {
return null;
}
// we will only do hidden word listening 50% of the time
// we will only do hidden word listening 30% of the time
// if there are no other activities to do, we will always do hidden word listening
if (numOtherActivities >= _maxQueueLength && Random().nextDouble() < 0.5) {
if (Random().nextDouble() < 0.7) {
// @ggurdin - just want you to review this change. i'm not sure what numOtherActivities >= _maxQueueLength was doing
// if (numOtherActivities >= _maxQueueLength && Random().nextDouble() < 0.5) {
return null;
}

@ -0,0 +1,31 @@
abstract class JsonSerializable {
Map<String, dynamic> toJson();
factory JsonSerializable.fromJson(Map<String, dynamic> json) {
throw UnimplementedError();
}
}
class ContentFeedback<T extends JsonSerializable> {
final JsonSerializable content;
final String feedback;
ContentFeedback(this.content, this.feedback);
toJson() {
return {
'content': content.toJson(),
'feedback': feedback,
};
}
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is ContentFeedback &&
runtimeType == other.runtimeType &&
content == other.content &&
feedback == other.feedback;
@override
int get hashCode => content.hashCode ^ feedback.hashCode;
}

@ -1,11 +1,7 @@
import 'dart:developer';
import 'dart:math';
import 'package:flutter/foundation.dart';
import 'package:collection/collection.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/pangea/constants/language_constants.dart';
import 'package:fluffychat/pangea/constants/pangea_event_types.dart';
import 'package:fluffychat/pangea/enum/activity_type_enum.dart';
@ -16,9 +12,13 @@ import 'package:fluffychat/pangea/models/analytics/construct_use_model.dart';
import 'package:fluffychat/pangea/models/analytics/constructs_model.dart';
import 'package:fluffychat/pangea/models/pangea_token_text_model.dart';
import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart';
import 'package:fluffychat/pangea/repo/lemma_definition_repo.dart';
import 'package:fluffychat/pangea/repo/lemma_info/lemma_info_repo.dart';
import 'package:fluffychat/pangea/repo/lemma_info/lemma_info_request.dart';
import 'package:fluffychat/pangea/utils/error_handler.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:flutter/foundation.dart';
import 'package:matrix/matrix.dart';
import '../constants/model_keys.dart';
import 'lemma.dart';
@ -373,7 +373,7 @@ class PangeaToken {
);
return distractors.isNotEmpty;
case ActivityTypeEnum.wordMeaning:
return LemmaDictionaryRepo.getDistractorDefinitions(
return LemmaInfoRepo.getDistractorDefinitions(
lemma.text,
1,
).isNotEmpty;
@ -519,9 +519,9 @@ class PangeaToken {
};
}
Future<List<String>> getEmojiChoices() => LemmaDictionaryRepo.get(
LemmaDefinitionRequest(
lemma: lemma,
Future<List<String>> getEmojiChoices() => LemmaInfoRepo.get(
LemmaInfoRequest(
lemma: lemma.text,
partOfSpeech: pos,
lemmaLang: MatrixState
.pangeaController.languageController.userL2?.langCode ??

@ -1,175 +0,0 @@
import 'dart:convert';
import 'package:http/http.dart';
import 'package:fluffychat/pangea/config/environment.dart';
import 'package:fluffychat/pangea/models/lemma.dart';
import 'package:fluffychat/pangea/network/requests.dart';
import 'package:fluffychat/pangea/network/urls.dart';
import 'package:fluffychat/pangea/utils/error_handler.dart';
import 'package:fluffychat/widgets/matrix.dart';
class LemmaDefinitionRequest {
final Lemma _lemma;
final String partOfSpeech;
final String lemmaLang;
final String userL1;
LemmaDefinitionRequest({
required this.partOfSpeech,
required this.lemmaLang,
required this.userL1,
required Lemma lemma,
}) : _lemma = lemma;
String get lemma {
if (_lemma.text.isNotEmpty) {
return _lemma.text;
}
ErrorHandler.logError(
e: "Found lemma with empty text",
data: {
'lemma': _lemma,
'part_of_speech': partOfSpeech,
'lemma_lang': lemmaLang,
'user_l1': userL1,
},
);
return _lemma.form;
}
Map<String, dynamic> toJson() {
return {
'lemma': lemma,
'part_of_speech': partOfSpeech,
'lemma_lang': lemmaLang,
'user_l1': userL1,
};
}
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is LemmaDefinitionRequest &&
runtimeType == other.runtimeType &&
lemma == other.lemma &&
partOfSpeech == other.partOfSpeech &&
lemmaLang == other.lemmaLang &&
userL1 == other.userL1;
@override
int get hashCode =>
lemma.hashCode ^
partOfSpeech.hashCode ^
lemmaLang.hashCode ^
userL1.hashCode;
}
class LemmaDefinitionResponse {
final List<String> emoji;
final String meaning;
LemmaDefinitionResponse({
required this.emoji,
required this.meaning,
});
factory LemmaDefinitionResponse.fromJson(Map<String, dynamic> json) {
return LemmaDefinitionResponse(
emoji: (json['emoji'] as List<dynamic>).map((e) => e as String).toList(),
meaning: json['meaning'] as String,
);
}
Map<String, dynamic> toJson() {
return {
'emoji': emoji,
'meaning': meaning,
};
}
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is LemmaDefinitionResponse &&
runtimeType == other.runtimeType &&
emoji.length == other.emoji.length &&
emoji.every((element) => other.emoji.contains(element)) &&
meaning == other.meaning;
@override
int get hashCode =>
emoji.fold(0, (prev, element) => prev ^ element.hashCode) ^
meaning.hashCode;
}
class LemmaDictionaryRepo {
// In-memory cache with timestamps
static final Map<LemmaDefinitionRequest, LemmaDefinitionResponse> _cache = {};
static final Map<LemmaDefinitionRequest, DateTime> _cacheTimestamps = {};
static const Duration _cacheDuration = Duration(days: 2);
static Future<LemmaDefinitionResponse> get(
LemmaDefinitionRequest request,
) async {
_clearExpiredEntries();
// Check the cache first
if (_cache.containsKey(request)) {
return _cache[request]!;
}
final Requests req = Requests(
choreoApiKey: Environment.choreoApiKey,
accessToken: MatrixState.pangeaController.userController.accessToken,
);
final requestBody = request.toJson();
final Response res = await req.post(
url: PApiUrls.lemmaDictionary,
body: requestBody,
);
final decodedBody = jsonDecode(utf8.decode(res.bodyBytes));
final response = LemmaDefinitionResponse.fromJson(decodedBody);
// Store the response and timestamp in the cache
_cache[request] = response;
_cacheTimestamps[request] = DateTime.now();
return response;
}
/// From the cache, get a random set of cached definitions that are not for a specific lemma
static List<String> getDistractorDefinitions(
String lemma,
int count,
) {
_clearExpiredEntries();
final List<String> definitions = [];
for (final entry in _cache.entries) {
if (entry.key.lemma != lemma) {
definitions.add(entry.value.meaning);
}
}
definitions.shuffle();
return definitions.take(count).toList();
}
static void _clearExpiredEntries() {
final now = DateTime.now();
final expiredKeys = _cacheTimestamps.entries
.where((entry) => now.difference(entry.value) > _cacheDuration)
.map((entry) => entry.key)
.toList();
for (final key in expiredKeys) {
_cache.remove(key);
_cacheTimestamps.remove(key);
}
}
}

@ -0,0 +1,107 @@
import 'dart:convert';
import 'dart:developer';
import 'package:fluffychat/pangea/models/content_feedback.dart';
import 'package:fluffychat/pangea/network/urls.dart';
import 'package:fluffychat/pangea/repo/lemma_info/lemma_info_request.dart';
import 'package:fluffychat/pangea/repo/lemma_info/lemma_info_response.dart';
import 'package:fluffychat/pangea/utils/error_handler.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:flutter/foundation.dart';
import 'package:http/http.dart';
import '../../config/environment.dart';
import '../../network/requests.dart';
class LemmaInfoRepo {
// In-memory cache with timestamps
static final Map<LemmaInfoRequest, LemmaInfoResponse> _cache = {};
static final Map<LemmaInfoRequest, DateTime> _cacheTimestamps = {};
static const Duration _cacheDuration = Duration(days: 2);
static Future<LemmaInfoResponse> get(
LemmaInfoRequest request, [
String? feedback,
]) async {
_clearExpiredEntries();
if (_cache.containsKey(request)) {
final cached = _cache[request]!;
if (feedback == null) {
// in this case, we just return the cached response
return cached;
} else {
// we're adding this within the service to avoid needing to have the widgets
// save state including the bad response
request.feedback = ContentFeedback(
cached,
feedback,
);
}
} else if (feedback != null) {
// the cache should have the request in order for the user to provide feedback
// this would be a strange situation and indicate some error in our logic
debugger(when: kDebugMode);
ErrorHandler.logError(
m: 'Feedback provided for a non-cached request',
data: request.toJson(),
);
} else {
debugPrint('No cached response for lemma ${request.lemma}');
}
final Requests req = Requests(
choreoApiKey: Environment.choreoApiKey,
accessToken: MatrixState.pangeaController.userController.accessToken,
);
final requestBody = request.toJson();
final Response res = await req.post(
url: PApiUrls.lemmaDictionary,
body: requestBody,
);
final decodedBody = jsonDecode(utf8.decode(res.bodyBytes));
final response = LemmaInfoResponse.fromJson(decodedBody);
// Store the response and timestamp in the cache
_cache[request] = response;
_cacheTimestamps[request] = DateTime.now();
return response;
}
/// From the cache, get a random set of cached definitions that are not for a specific lemma
static List<String> getDistractorDefinitions(
String lemma,
int count,
) {
_clearExpiredEntries();
final Set<String> definitions = {};
for (final entry in _cache.entries) {
if (entry.key.lemma != lemma) {
definitions.add(entry.value.meaning);
}
}
definitions.toList().shuffle();
return definitions.take(count).toList();
}
static void _clearExpiredEntries() {
final now = DateTime.now();
final expiredKeys = _cacheTimestamps.entries
.where((entry) => now.difference(entry.value) > _cacheDuration)
.map((entry) => entry.key)
.toList();
for (final key in expiredKeys) {
_cache.remove(key);
_cacheTimestamps.remove(key);
}
}
}

@ -0,0 +1,43 @@
import 'package:fluffychat/pangea/models/content_feedback.dart';
import 'package:fluffychat/pangea/repo/lemma_info/lemma_info_response.dart';
class LemmaInfoRequest {
final String lemma;
final String partOfSpeech;
final String lemmaLang;
final String userL1;
ContentFeedback<LemmaInfoResponse>? feedback;
LemmaInfoRequest({
required String partOfSpeech,
required String lemmaLang,
required this.userL1,
required this.lemma,
this.feedback,
}) : partOfSpeech = partOfSpeech.toLowerCase(),
lemmaLang = lemmaLang.toLowerCase();
Map<String, dynamic> toJson() {
return {
'lemma': lemma,
'part_of_speech': partOfSpeech,
'lemma_lang': lemmaLang,
'user_l1': userL1,
'feedback': feedback?.toJson(),
};
}
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is LemmaInfoRequest &&
runtimeType == other.runtimeType &&
lemma == other.lemma &&
partOfSpeech == other.partOfSpeech &&
feedback == other.feedback;
@override
int get hashCode =>
lemma.hashCode ^ partOfSpeech.hashCode ^ feedback.hashCode;
}

@ -0,0 +1,40 @@
import 'package:fluffychat/pangea/models/content_feedback.dart';
class LemmaInfoResponse implements JsonSerializable {
final List<String> emoji;
final String meaning;
LemmaInfoResponse({
required this.emoji,
required this.meaning,
});
factory LemmaInfoResponse.fromJson(Map<String, dynamic> json) {
return LemmaInfoResponse(
emoji: (json['emoji'] as List<dynamic>).map((e) => e as String).toList(),
meaning: json['meaning'] as String,
);
}
@override
Map<String, dynamic> toJson() {
return {
'emoji': emoji,
'meaning': meaning,
};
}
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is LemmaInfoResponse &&
runtimeType == other.runtimeType &&
emoji.length == other.emoji.length &&
emoji.every((element) => other.emoji.contains(element)) &&
meaning == other.meaning;
@override
int get hashCode =>
emoji.fold(0, (prev, element) => prev ^ element.hashCode) ^
meaning.hashCode;
}

@ -1,13 +1,12 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:fluffychat/pangea/enum/activity_type_enum.dart';
import 'package:fluffychat/pangea/enum/construct_type_enum.dart';
import 'package:fluffychat/pangea/models/practice_activities.dart/message_activity_request.dart';
import 'package:fluffychat/pangea/models/practice_activities.dart/multiple_choice_activity_model.dart';
import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart';
import 'package:fluffychat/pangea/repo/lemma_definition_repo.dart';
import 'package:fluffychat/pangea/repo/lemma_info/lemma_info_repo.dart';
import 'package:fluffychat/pangea/repo/lemma_info/lemma_info_request.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
class WordMeaningActivityGenerator {
Future<MessageActivityResponse> get(
@ -15,24 +14,25 @@ class WordMeaningActivityGenerator {
BuildContext context,
) async {
final ConstructIdentifier lemmaId = ConstructIdentifier(
lemma: req.targetTokens[0].lemma.text,
lemma: req.targetTokens[0].lemma.text.isNotEmpty
? req.targetTokens[0].lemma.text
: req.targetTokens[0].lemma.form,
type: ConstructTypeEnum.vocab,
category: req.targetTokens[0].pos,
);
final LemmaDefinitionRequest lemmaDefReq = LemmaDefinitionRequest(
lemma: req.targetTokens[0].lemma,
final lemmaDefReq = LemmaInfoRequest(
lemma: lemmaId.lemma,
partOfSpeech: lemmaId.category,
/// This assumes that the user's L2 is the language of the lemma
// Note that this assumes that the user's L2 is the language of the lemma.
lemmaLang: req.userL2,
userL1: req.userL1,
);
final res = await LemmaDictionaryRepo.get(lemmaDefReq);
final res = await LemmaInfoRepo.get(lemmaDefReq);
final choices =
LemmaDictionaryRepo.getDistractorDefinitions(lemmaDefReq.lemma, 3);
LemmaInfoRepo.getDistractorDefinitions(lemmaDefReq.lemma, 3);
if (!choices.contains(res.meaning)) {
choices.add(res.meaning);

@ -1,11 +1,11 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pangea/controllers/get_analytics_controller.dart';
import 'package:fluffychat/pangea/controllers/put_analytics_controller.dart';
import 'package:fluffychat/pangea/utils/bot_style.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:flutter/material.dart';
class PointsGainedAnimation extends StatefulWidget {
final Color? gainColor;
@ -15,7 +15,7 @@ class PointsGainedAnimation extends StatefulWidget {
const PointsGainedAnimation({
super.key,
required this.origin,
this.gainColor,
this.gainColor = AppConfig.gold,
this.loseColor = Colors.red,
});

@ -1,7 +1,3 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pangea/controllers/get_analytics_controller.dart';
import 'package:fluffychat/pangea/enum/construct_type_enum.dart';
@ -10,6 +6,8 @@ import 'package:fluffychat/pangea/models/analytics/constructs_model.dart';
import 'package:fluffychat/pangea/utils/grammar/get_grammar_copy.dart';
import 'package:fluffychat/pangea/widgets/chat_list/analytics_summary/learning_progress_bar.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
class LevelBarPopup extends StatelessWidget {
const LevelBarPopup({
@ -156,20 +154,23 @@ class LevelBarPopup extends StatelessWidget {
children: [
Text(
"${use.pointValue > 0 ? '+' : ''}${use.pointValue}",
style: const TextStyle(
style: TextStyle(
fontSize: 16,
height: 1,
),
),
const SizedBox(width: 5),
const CircleAvatar(
radius: 8,
child: Icon(
size: 10,
Icons.star,
color: Colors.white,
),
),
color: use.pointValue > 0
? AppConfig.gold
: Colors.red,
),
),
// const SizedBox(width: 5),
// const CircleAvatar(
// radius: 8,
// child: Icon(
// size: 10,
// Icons.star,
// color: Colors.white,
// ),
// ),
],
),
),

@ -1,9 +1,7 @@
import 'package:fluffychat/utils/feedback_dialog.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:fluffychat/pangea/widgets/common/bot_face_svg.dart';
class ContentIssueButton extends StatelessWidget {
final bool isActive;
final void Function(String) submitFeedback;
@ -27,61 +25,8 @@ class ContentIssueButton extends StatelessWidget {
if (!isActive) {
return;
}
final TextEditingController feedbackController =
TextEditingController();
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text(
L10n.of(context).reportContentIssueTitle,
textAlign: TextAlign.center,
),
content: SingleChildScrollView(
child: Container(
constraints: const BoxConstraints(maxWidth: 300),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const BotFace(
width: 60,
expression: BotExpression.addled,
),
const SizedBox(height: 10),
Text(L10n.of(context).reportContentIssueDescription),
const SizedBox(height: 10),
TextField(
controller: feedbackController,
decoration: InputDecoration(
labelText: L10n.of(context).feedback,
border: const OutlineInputBorder(),
),
maxLines: 4,
),
],
),
),
),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop(); // Close the dialog
},
child: Text(L10n.of(context).cancel),
),
ElevatedButton(
onPressed: () {
// Call the additional callback function
submitFeedback(feedbackController.text);
Navigator.of(context).pop(); // Close the dialog
},
child: Text(L10n.of(context).submit),
),
],
);
},
);
showFeedbackDialog(context, submitFeedback);
},
),
),

@ -1,62 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:fluffychat/pangea/constants/language_constants.dart';
import 'package:fluffychat/pangea/models/pangea_token_model.dart';
import 'package:fluffychat/pangea/repo/lemma_definition_repo.dart';
import 'package:fluffychat/pangea/widgets/igc/card_error_widget.dart';
import 'package:fluffychat/widgets/matrix.dart';
class ContextualTranslationWidget extends StatelessWidget {
final PangeaToken token;
final String langCode;
const ContextualTranslationWidget({
super.key,
required this.token,
required this.langCode,
});
Future<String> _fetchDefinition() async {
final LemmaDefinitionRequest lemmaDefReq = LemmaDefinitionRequest(
lemma: token.lemma,
partOfSpeech: token.pos,
/// This assumes that the user's L2 is the language of the lemma
lemmaLang: langCode,
userL1:
MatrixState.pangeaController.languageController.userL1?.langCode ??
LanguageKeys.defaultLanguage,
);
final res = await LemmaDictionaryRepo.get(lemmaDefReq);
return res.meaning;
}
@override
Widget build(BuildContext context) {
return FutureBuilder(
future: _fetchDefinition(),
builder: (context, snapshot) {
return Center(
child: Padding(
padding: const EdgeInsets.all(16),
child: snapshot.connectionState != ConnectionState.done
? const CircularProgressIndicator()
: snapshot.hasError
? CardErrorWidget(
error: L10n.of(context).oopsSomethingWentWrong,
padding: 0,
maxWidth: 500,
)
: Text(
snapshot.data ?? "...",
textAlign: TextAlign.center,
),
),
);
},
);
}
}

@ -1,71 +0,0 @@
import 'package:flutter/material.dart';
import 'package:fluffychat/pangea/constants/language_constants.dart';
import 'package:fluffychat/pangea/models/pangea_token_model.dart';
import 'package:fluffychat/pangea/repo/lemma_definition_repo.dart';
import 'package:fluffychat/widgets/matrix.dart';
class LemmaDefinitionWidget extends StatefulWidget {
final PangeaToken token;
final String tokenLang;
final VoidCallback onPressed;
const LemmaDefinitionWidget({
super.key,
required this.token,
required this.tokenLang,
required this.onPressed,
});
@override
LemmaDefinitionWidgetState createState() => LemmaDefinitionWidgetState();
}
class LemmaDefinitionWidgetState extends State<LemmaDefinitionWidget> {
late Future<String> _definition;
@override
void initState() {
super.initState();
_definition = _fetchDefinition();
}
Future<String> _fetchDefinition() async {
if (widget.token.shouldDoPosActivity) {
return '?';
} else {
final res = await LemmaDictionaryRepo.get(
LemmaDefinitionRequest(
lemma: widget.token.lemma,
partOfSpeech: widget.token.pos,
lemmaLang: widget.tokenLang,
userL1: MatrixState
.pangeaController.languageController.userL1?.langCode ??
LanguageKeys.defaultLanguage,
),
);
return res.meaning;
}
}
@override
Widget build(BuildContext context) {
return FutureBuilder<String>(
future: _definition,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const CircularProgressIndicator();
} else if (snapshot.hasError) {
// TODO better error widget
return Text('Error: ${snapshot.error}');
} else {
return ActionChip(
avatar: const Icon(Icons.book),
label: Text(snapshot.data ?? 'No definition found'),
onPressed: widget.onPressed,
);
}
},
);
}
}

@ -0,0 +1,97 @@
import 'dart:developer';
import 'package:fluffychat/pangea/choreographer/widgets/text_loading_shimmer.dart';
import 'package:fluffychat/pangea/constants/language_constants.dart';
import 'package:fluffychat/pangea/repo/lemma_info/lemma_info_repo.dart';
import 'package:fluffychat/pangea/repo/lemma_info/lemma_info_request.dart';
import 'package:fluffychat/utils/feedback_dialog.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
class LemmaMeaningWidget extends StatefulWidget {
final String lemma;
final String pos;
final String langCode;
const LemmaMeaningWidget({
super.key,
required this.lemma,
required this.pos,
required this.langCode,
});
@override
_LemmaMeaningWidgetState createState() => _LemmaMeaningWidgetState();
}
class _LemmaMeaningWidgetState extends State<LemmaMeaningWidget> {
late Future<String> _definitionFuture;
@override
void initState() {
super.initState();
_definitionFuture = _fetchDefinition();
}
Future<String> _fetchDefinition([String? feedback]) async {
final LemmaInfoRequest lemmaDefReq = LemmaInfoRequest(
lemma: widget.lemma,
partOfSpeech: widget.pos,
/// This assumes that the user's L2 is the language of the lemma
lemmaLang: widget.langCode,
userL1:
MatrixState.pangeaController.languageController.userL1?.langCode ??
LanguageKeys.defaultLanguage,
);
final res = await LemmaInfoRepo.get(lemmaDefReq, feedback);
return res.meaning;
}
void _showFeedbackDialog(String offendingContentString) async {
// setState(() {
// _definitionFuture = _fetchDefinition(offendingContentString);
// });
await showFeedbackDialog(
context,
Text(offendingContentString),
(feedback) async {
setState(() {
_definitionFuture = _fetchDefinition(feedback);
});
return;
},
);
}
@override
Widget build(BuildContext context) {
return FutureBuilder(
future: _definitionFuture,
builder: (context, snapshot) {
if (snapshot.connectionState != ConnectionState.done) {
return const TextLoadingShimmer();
}
if (snapshot.hasError) {
debugger(when: kDebugMode);
return Text(
snapshot.error.toString(),
textAlign: TextAlign.center,
);
}
return GestureDetector(
onLongPress: () => _showFeedbackDialog(snapshot.data as String),
onDoubleTap: () => _showFeedbackDialog(snapshot.data as String),
child: Text(
snapshot.data as String,
textAlign: TextAlign.center,
),
);
},
);
}
}

@ -0,0 +1,74 @@
import 'package:fluffychat/pangea/controllers/message_analytics_controller.dart';
import 'package:fluffychat/pangea/widgets/chat/toolbar_content_loading_indicator.dart';
import 'package:fluffychat/pangea/widgets/practice_activity/practice_activity_card.dart';
import 'package:fluffychat/pangea/widgets/word_zoom/lemma_meaning_widget.dart';
import 'package:fluffychat/pangea/widgets/word_zoom/word_zoom_widget.dart';
import 'package:flutter/material.dart';
class WordZoomCenterWidget extends StatelessWidget {
final WordZoomSelection? selectionType;
final String? selectedMorphFeature;
final bool shouldDoActivity;
final bool locked;
final WordZoomWidgetState wordDetailsController;
const WordZoomCenterWidget({
required this.selectionType,
required this.selectedMorphFeature,
required this.shouldDoActivity,
required this.locked,
required this.wordDetailsController,
super.key,
});
@override
Widget build(BuildContext context) {
if (selectionType == null) {
return const ToolbarContentLoadingIndicator();
}
if (shouldDoActivity || locked) {
return PracticeActivityCard(
pangeaMessageEvent: wordDetailsController.widget.messageEvent,
targetTokensAndActivityType: TargetTokensAndActivityType(
tokens: [wordDetailsController.widget.token],
activityType: selectionType!.activityType,
),
overlayController: wordDetailsController.widget.overlayController,
morphFeature: selectedMorphFeature,
wordDetailsController: wordDetailsController,
);
}
if (selectionType == WordZoomSelection.meaning) {
return Center(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: LemmaMeaningWidget(
lemma: wordDetailsController.widget.token.lemma.text.isNotEmpty
? wordDetailsController.widget.token.lemma.text
: wordDetailsController.widget.token.lemma.form,
pos: wordDetailsController.widget.token.pos,
langCode: wordDetailsController
.widget.messageEvent.messageDisplayLangCode,
),
),
);
}
return Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
ActivityAnswerWidget(
token: wordDetailsController.widget.token,
selectionType: selectionType!,
selectedMorphFeature: selectedMorphFeature,
),
],
),
);
}
}

@ -1,34 +1,30 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pangea/controllers/message_analytics_controller.dart';
import 'package:fluffychat/pangea/enum/activity_type_enum.dart';
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
import 'package:fluffychat/pangea/models/pangea_token_model.dart';
import 'package:fluffychat/pangea/utils/grammar/get_grammar_copy.dart';
import 'package:fluffychat/pangea/widgets/chat/message_selection_overlay.dart';
import 'package:fluffychat/pangea/widgets/chat/toolbar_content_loading_indicator.dart';
import 'package:fluffychat/pangea/widgets/chat/tts_controller.dart';
import 'package:fluffychat/pangea/widgets/practice_activity/emoji_practice_button.dart';
import 'package:fluffychat/pangea/widgets/practice_activity/practice_activity_card.dart';
import 'package:fluffychat/pangea/widgets/practice_activity/word_text_with_audio_button.dart';
import 'package:fluffychat/pangea/widgets/word_zoom/contextual_translation_widget.dart';
import 'package:fluffychat/pangea/widgets/word_zoom/lemma_widget.dart';
import 'package:fluffychat/pangea/widgets/word_zoom/morphological_widget.dart';
import 'package:fluffychat/pangea/widgets/word_zoom/word_zoom_center_widget.dart';
import 'package:flutter/material.dart';
enum WordZoomSelection {
translation,
meaning,
emoji,
lemma,
morph,
}
extension on WordZoomSelection {
extension WordZoomSelectionUtils on WordZoomSelection {
ActivityTypeEnum get activityType {
switch (this) {
case WordZoomSelection.translation:
case WordZoomSelection.meaning:
return ActivityTypeEnum.wordMeaning;
case WordZoomSelection.emoji:
return ActivityTypeEnum.emoji;
@ -117,7 +113,7 @@ class WordZoomWidgetState extends State<WordZoomWidget> {
WordZoomSelection get _defaultSelectionType =>
_shouldShowActivity(WordZoomSelection.lemma)
? WordZoomSelection.lemma
: WordZoomSelection.translation;
: WordZoomSelection.meaning;
Future<void> _setSelectionType(
WordZoomSelection type, {
@ -167,7 +163,7 @@ class WordZoomWidgetState extends State<WordZoomWidget> {
_lockActivity();
Future.delayed(savorTheJoyDuration, () {
if (_selectionType == WordZoomSelection.lemma) {
_setSelectionType(WordZoomSelection.translation);
_setSelectionType(WordZoomSelection.meaning);
}
_unlockActivity();
});
@ -186,7 +182,7 @@ class WordZoomWidgetState extends State<WordZoomWidget> {
switch (selection) {
case WordZoomSelection.lemma:
return _canGenerateLemmaActivity;
case WordZoomSelection.translation:
case WordZoomSelection.meaning:
case WordZoomSelection.morph:
return widget.token.canGenerateDistractors(
selection.activityType,
@ -316,68 +312,8 @@ class ActivityAnswerWidget extends StatelessWidget {
return token.getEmoji() != null
? Text(token.getEmoji()!)
: const Text("emoji is null");
case WordZoomSelection.translation:
case WordZoomSelection.meaning:
return const SizedBox();
}
}
}
class WordZoomCenterWidget extends StatelessWidget {
final WordZoomSelection? selectionType;
final String? selectedMorphFeature;
final bool shouldDoActivity;
final bool locked;
final WordZoomWidgetState wordDetailsController;
const WordZoomCenterWidget({
required this.selectionType,
required this.selectedMorphFeature,
required this.shouldDoActivity,
required this.locked,
required this.wordDetailsController,
super.key,
});
@override
Widget build(BuildContext context) {
if (selectionType == null) {
return const ToolbarContentLoadingIndicator();
}
if (shouldDoActivity || locked) {
return PracticeActivityCard(
pangeaMessageEvent: wordDetailsController.widget.messageEvent,
targetTokensAndActivityType: TargetTokensAndActivityType(
tokens: [wordDetailsController.widget.token],
activityType: selectionType!.activityType,
),
overlayController: wordDetailsController.widget.overlayController,
morphFeature: selectedMorphFeature,
wordDetailsController: wordDetailsController,
);
}
if (selectionType == WordZoomSelection.translation) {
return ContextualTranslationWidget(
token: wordDetailsController.widget.token,
langCode:
wordDetailsController.widget.messageEvent.messageDisplayLangCode,
);
}
return Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
ActivityAnswerWidget(
token: wordDetailsController.widget.token,
selectionType: selectionType!,
selectedMorphFeature: selectedMorphFeature,
),
],
),
);
}
}

@ -0,0 +1,76 @@
import 'package:fluffychat/config/app_config.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import '../pangea/widgets/common/bot_face_svg.dart';
Future<dynamic> showFeedbackDialog(
BuildContext context,
Widget offendingContent,
void Function(String) submitFeedback,
) {
final TextEditingController feedbackController = TextEditingController();
return showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text(
L10n.of(context).reportContentIssueTitle,
textAlign: TextAlign.center,
),
content: SingleChildScrollView(
child: Container(
constraints: const BoxConstraints(maxWidth: 300),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const BotFace(
width: 60,
expression: BotExpression.addled,
),
const SizedBox(height: 10),
Text(L10n.of(context).reportContentIssueDescription),
const SizedBox(height: 10),
Container(
padding: const EdgeInsets.all(8.0),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8.0),
border: Border.all(
color: AppConfig.warning,
),
),
child: offendingContent,
),
const SizedBox(height: 10),
TextField(
controller: feedbackController,
decoration: InputDecoration(
labelText: L10n.of(context).feedback,
border: const OutlineInputBorder(),
),
maxLines: 4,
),
],
),
),
),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop(); // Close the dialog
},
child: Text(L10n.of(context).cancel),
),
ElevatedButton(
onPressed: () {
// Call the additional callback function
submitFeedback(feedbackController.text);
Navigator.of(context).pop(); // Close the dialog
},
child: Text(L10n.of(context).submit),
),
],
);
},
);
}
Loading…
Cancel
Save