3921 display unsubscribed errors for users (#3991)

* url cleanup

* chore: display unsubscribed errors differently
This commit is contained in:
ggurdin 2025-09-15 15:58:08 -04:00 committed by GitHub
parent d166f40849
commit d951d5eee9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
29 changed files with 175 additions and 1319 deletions

View file

@ -5243,5 +5243,8 @@
}
}
},
"inviteFriendsToCourse": "Invite friends to my course"
"inviteFriendsToCourse": "Invite friends to my course",
"subscribeToUnlockActivitySummaries": "Subscribe to unlock activity summaries",
"subscribeToUnlockDefinitions": "Subscribe to unlock definitions",
"subscribeToUnlockTranscriptions": "Subscribe to unlock transcriptions"
}

View file

@ -15,6 +15,7 @@ import 'package:fluffychat/pangea/activity_summary/activity_summary_request_mode
import 'package:fluffychat/pangea/bot/utils/bot_name.dart';
import 'package:fluffychat/pangea/chat_settings/constants/pangea_room_types.dart';
import 'package:fluffychat/pangea/common/config/environment.dart';
import 'package:fluffychat/pangea/common/network/requests.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/course_plans/course_plan_room_extension.dart';
import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart';
@ -190,15 +191,17 @@ extension ActivityRoomExtension on Room {
ActivitySummaryRepo.delete(id, activityPlan!);
} catch (e, s) {
ErrorHandler.logError(
e: e,
s: s,
data: {
"roomID": id,
"activityPlan": activityPlan?.toJson(),
"activityResults": messages.map((m) => m.toJson()).toList(),
},
);
if (e is! UnsubscribedException) {
ErrorHandler.logError(
e: e,
s: s,
data: {
"roomID": id,
"activityPlan": activityPlan?.toJson(),
"activityResults": messages.map((m) => m.toJson()).toList(),
},
);
}
if (activitySummary?.summary == null) {
await setActivitySummary(

View file

@ -8,6 +8,7 @@ import 'package:fluffychat/pages/chat/chat.dart';
import 'package:fluffychat/pangea/activity_sessions/activity_room_extension.dart';
import 'package:fluffychat/pangea/activity_sessions/activity_session_chat/saved_activity_analytics_dialog.dart';
import 'package:fluffychat/pangea/activity_summary/activity_summary_model.dart';
import 'package:fluffychat/pangea/common/widgets/error_indicator.dart';
import 'package:fluffychat/pangea/course_plans/course_plan_room_extension.dart';
import 'package:fluffychat/widgets/future_loading_dialog.dart';
import 'package:fluffychat/widgets/matrix.dart';
@ -70,6 +71,9 @@ class ActivityFinishedStatusMessage extends StatelessWidget {
}
final theme = Theme.of(context);
final isSubscribed =
MatrixState.pangeaController.subscriptionController.isSubscribed;
return AnimatedSize(
duration: FluffyThemes.animationDuration,
child: Container(
@ -103,7 +107,16 @@ class ActivityFinishedStatusMessage extends StatelessWidget {
width: 36.0,
child: CircularProgressIndicator(),
),
] else if (summary?.hasError ?? false) ...[
] else if (isSubscribed == false)
ErrorIndicator(
message: L10n.of(context)
.subscribeToUnlockActivitySummaries,
onTap: () {
MatrixState.pangeaController.subscriptionController
.showPaywall(context);
},
)
else if (summary?.hasError ?? false) ...[
Row(
mainAxisSize: MainAxisSize.min,
children: [

View file

@ -1,15 +1,15 @@
import 'dart:developer';
import 'dart:math';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pangea/analytics_misc/text_loading_shimmer.dart';
import 'package:fluffychat/pangea/common/network/requests.dart';
import 'package:fluffychat/pangea/common/widgets/error_indicator.dart';
import 'package:fluffychat/pangea/morphs/get_grammar_copy.dart';
import 'package:fluffychat/pangea/morphs/morph_features_enum.dart';
import 'package:fluffychat/pangea/morphs/morph_meaning/morph_info_repo.dart';
import 'package:fluffychat/widgets/matrix.dart';
class MorphMeaningWidget extends StatefulWidget {
final MorphFeaturesEnum feature;
@ -63,7 +63,6 @@ class MorphMeaningWidgetState extends State<MorphMeaningWidget> {
final response = await _morphMeaning();
_setMeaningText(response);
} catch (e) {
debugger(when: kDebugMode);
_error = e;
} finally {
if (mounted) setState(() => _isLoading = false);
@ -117,9 +116,17 @@ class MorphMeaningWidgetState extends State<MorphMeaningWidget> {
if (_error != null) {
return Center(
child: ErrorIndicator(
message: L10n.of(context).errorFetchingDefinition,
),
child: _error is UnsubscribedException
? ErrorIndicator(
message: L10n.of(context).subscribeToUnlockDefinitions,
onTap: () {
MatrixState.pangeaController.subscriptionController
.showPaywall(context);
},
)
: ErrorIndicator(
message: L10n.of(context).errorFetchingDefinition,
),
);
}

View file

@ -70,7 +70,7 @@ class VocabDetailsView extends StatelessWidget {
},
),
if (MatrixState
.pangeaController.languageController.showTrancription)
.pangeaController.languageController.showTranscription)
Padding(
padding: const EdgeInsets.only(top: 4.0),
child: PhoneticTranscriptionWidget(

View file

@ -1,120 +1,4 @@
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:collection/collection.dart';
import 'package:http/http.dart';
import 'package:fluffychat/pangea/common/config/environment.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import '../../common/constants/model_keys.dart';
import '../../common/controllers/pangea_controller.dart';
import '../../common/network/requests.dart';
import '../../common/network/urls.dart';
class ITFeedbackController {
late PangeaController _pangeaController;
final List<_ITFeedbackCacheItem> _feedback = [];
ITFeedbackController(PangeaController pangeaController) {
_pangeaController = pangeaController;
}
_ITFeedbackCacheItem? _getLocal(
ITFeedbackRequestModel req,
) =>
_feedback.firstWhereOrNull(
(e) =>
e.chosenContinuance == req.chosenContinuance &&
e.sourceText == req.sourceText,
);
Future<ITFeedbackResponseModel?> get(
ITFeedbackRequestModel req,
) {
final _ITFeedbackCacheItem? localItem = _getLocal(req);
if (localItem != null) return localItem.data;
_feedback.add(
_ITFeedbackCacheItem(
chosenContinuance: req.chosenContinuance,
sourceText: req.sourceText,
data: _get(req),
),
);
return _feedback.last.data;
}
Future<ITFeedbackResponseModel?> _get(
ITFeedbackRequestModel request,
) async {
try {
final ITFeedbackResponseModel res = await _ITFeedbackRepo.get(
_pangeaController.userController.accessToken,
request,
);
return res;
} catch (err, stack) {
debugPrint(
"error getting contextual definition for ${request.chosenContinuance} in '${request.sourceText}'",
);
ErrorHandler.logError(e: err, s: stack, data: request.toJson());
return null;
}
}
}
class _ITFeedbackCacheItem {
String chosenContinuance;
String sourceText;
Future<ITFeedbackResponseModel?> data;
_ITFeedbackCacheItem({
required this.chosenContinuance,
required this.sourceText,
required this.data,
});
}
class _ITFeedbackRepo {
static Future<ITFeedbackResponseModel> get(
String accessToken,
ITFeedbackRequestModel request,
) async {
final Requests req = Requests(
choreoApiKey: Environment.choreoApiKey,
accessToken: accessToken,
);
final Response res = await req.post(
url: PApiUrls.itFeedback,
body: request.toJson(),
);
final ITFeedbackResponseModel response = ITFeedbackResponseModel.fromJson(
jsonDecode(
utf8.decode(res.bodyBytes).toString(),
),
);
if (response.text.isEmpty) {
ErrorHandler.logError(
e: Exception(
"empty text in contextual definition response",
),
data: {
"request": request.toJson(),
"accessToken": accessToken,
},
);
}
return response;
}
}
import 'package:fluffychat/pangea/common/constants/model_keys.dart';
class ITFeedbackRequestModel {
final String sourceText;

View file

@ -1,83 +0,0 @@
import 'package:collection/collection.dart';
import 'package:http/http.dart' as http;
import 'package:fluffychat/pangea/choreographer/repo/word_repo.dart';
import 'package:fluffychat/pangea/learning_settings/constants/language_constants.dart';
import '../../common/controllers/base_controller.dart';
import '../../common/controllers/pangea_controller.dart';
import '../models/word_data_model.dart';
class WordController extends BaseController {
late PangeaController _pangeaController;
final List<WordData> _wordData = [];
WordController(PangeaController pangeaController) : super() {
_pangeaController = pangeaController;
}
WordData? getWordDataLocal({
required String word,
required String fullText,
required String? userL1,
required String? userL2,
}) =>
_wordData.firstWhereOrNull(
(e) => e.isMatch(
w: word,
f: fullText,
l1: userL1,
l2: userL2,
),
);
Future<WordData> getWordDataGlobal({
required String word,
required String fullText,
required String? userL1,
required String? userL2,
}) async {
if (userL1 == null ||
userL2 == null ||
userL1 == LanguageKeys.unknownLanguage ||
userL2 == LanguageKeys.unknownLanguage) {
throw http.Response("", 405);
}
final WordData? local = getWordDataLocal(
word: word,
fullText: fullText,
userL1: userL1,
userL2: userL2,
);
if (local != null) return local;
final WordData remote = await WordRepo.getWordNetData(
accessToken: _pangeaController.userController.accessToken,
fullText: fullText,
word: word,
userL1: userL1,
userL2: userL2,
);
_addWordData(remote);
return remote;
}
_addWordData(WordData w) {
final WordData? local = getWordDataLocal(
word: w.word,
fullText: w.fullText,
userL1: w.userL1,
userL2: w.userL2,
);
if (local == null) {
if (_wordData.length > 100) _wordData.clear();
_wordData.add(w);
setState(null);
}
}
}

View file

@ -1,106 +0,0 @@
import 'dart:convert';
import 'package:http/http.dart';
import 'package:fluffychat/pangea/common/config/environment.dart';
import '../../common/network/requests.dart';
import '../../common/network/urls.dart';
class SimilarityRepo {
static Future<SimilartyResponseModel> get({
required String accessToken,
required SimilarityRequestModel request,
}) async {
final Requests req = Requests(
choreoApiKey: Environment.choreoApiKey,
accessToken: accessToken,
);
final Response res = await req.post(
url: PApiUrls.similarity,
body: request.toJson(),
);
final SimilartyResponseModel response = SimilartyResponseModel.fromJson(
jsonDecode(
utf8.decode(res.bodyBytes).toString(),
),
);
return response;
}
}
class SimilarityRequestModel {
String benchmark;
List<String> toCompare;
SimilarityRequestModel({required this.benchmark, required this.toCompare});
Map<String, dynamic> toJson() => {
"original": benchmark,
"to_compare": toCompare,
};
}
class SimilartyResponseModel {
String benchmark;
List<SimilarityScore> scores;
SimilartyResponseModel({required this.benchmark, required this.scores});
factory SimilartyResponseModel.fromJson(
Map<String, dynamic> json,
) =>
SimilartyResponseModel(
benchmark: json["original"],
scores: List<SimilarityScore>.from(
json["scores"].map(
(x) => SimilarityScore.fromJson(x),
),
),
);
SimilarityScore get highestScore {
SimilarityScore highest = scores.first;
for (final SimilarityScore score in scores) {
if (score.score > highest.score) {
highest = score;
}
}
return highest;
}
bool userTranslationIsDifferentButBetter(String userTranslation) {
return highestScore.text == userTranslation;
}
bool userTranslationIsSameAsBotTranslation(String userTranslation) {
return highestScore.text == userTranslation &&
scores.where((e) => e.text == userTranslation).length == 2;
}
num userScore(String userTranslation) {
return scores.firstWhere((e) => e.text == userTranslation).score;
}
}
class SimilarityScore {
String text;
double score;
int index;
SimilarityScore({
required this.text,
required this.score,
required this.index,
});
factory SimilarityScore.fromJson(Map<String, dynamic> json) {
return SimilarityScore(
text: json["text"],
score: json["score"],
index: json["index"],
);
}
}

View file

@ -1,45 +0,0 @@
import 'dart:convert';
import 'package:http/http.dart';
import 'package:fluffychat/pangea/common/config/environment.dart';
import '../../common/constants/model_keys.dart';
import '../../common/network/requests.dart';
import '../../common/network/urls.dart';
import '../models/word_data_model.dart';
class WordRepo {
static Future<WordData> getWordNetData({
required String accessToken,
required String fullText,
required String word,
required String userL1,
required String userL2,
}) async {
final Requests req = Requests(
choreoApiKey: Environment.choreoApiKey,
accessToken: accessToken,
);
final Response res = await req.post(
url: PApiUrls.wordNet,
body: {
ModelKey.word: word,
ModelKey.fullText: fullText,
ModelKey.userL1: userL1,
ModelKey.userL2: userL2,
},
);
final json = jsonDecode(utf8.decode(res.bodyBytes));
final WordData wordData = WordData.fromJson(
json,
fullText: fullText,
word: word,
userL1: userL1,
userL2: userL2,
);
return wordData;
}
}

View file

@ -1,203 +0,0 @@
import 'dart:developer';
import 'dart:ui';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pages/chat/chat.dart';
import 'package:fluffychat/pangea/common/controllers/pangea_controller.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart';
import 'package:fluffychat/pangea/events/models/representation_content_model.dart';
import 'package:fluffychat/pangea/instructions/instructions_enum.dart';
import 'package:fluffychat/pangea/instructions/instructions_show_popup.dart';
import 'package:fluffychat/pangea/toolbar/widgets/message_toolbar_selection_area.dart';
import 'package:fluffychat/widgets/matrix.dart';
import '../../models/pangea_match_model.dart';
class PangeaRichText extends StatefulWidget {
final PangeaMessageEvent pangeaMessageEvent;
final bool immersionMode;
final TextStyle? style;
final bool isOverlay;
final ChatController controller;
final Event? nextEvent;
final Event? prevEvent;
const PangeaRichText({
super.key,
required this.pangeaMessageEvent,
required this.immersionMode,
required this.isOverlay,
required this.controller,
this.nextEvent,
this.prevEvent,
this.style,
});
@override
PangeaRichTextState createState() => PangeaRichTextState();
}
class PangeaRichTextState extends State<PangeaRichText> {
final PangeaController pangeaController = MatrixState.pangeaController;
bool _fetchingRepresentation = false;
double get blur => (_fetchingRepresentation && widget.immersionMode) ||
!pangeaController.languageController.languagesSet
? 5
: 0;
String textSpan = "";
PangeaRepresentation? repEvent;
@override
void initState() {
super.initState();
setTextSpan();
}
@override
void didUpdateWidget(PangeaRichText oldWidget) {
super.didUpdateWidget(oldWidget);
setTextSpan();
}
void _setTextSpan(String newTextSpan) {
try {
if (!mounted) return; // Early exit if the widget is no longer in the tree
setState(() {
textSpan = newTextSpan;
});
} catch (error, stackTrace) {
ErrorHandler.logError(
e: PangeaWarningError(error),
s: stackTrace,
m: "Error setting text span in PangeaRichText",
data: {
"newTextSpan": newTextSpan,
},
);
}
}
void setTextSpan() {
if (_fetchingRepresentation) {
_setTextSpan(
widget.pangeaMessageEvent.event
.getDisplayEvent(widget.pangeaMessageEvent.timeline)
.body,
);
return;
}
if (widget.pangeaMessageEvent.eventId.contains("webdebug")) {
debugger(when: kDebugMode);
}
repEvent = widget.pangeaMessageEvent.messageDisplayRepresentation?.content;
if (repEvent == null) {
setState(() => _fetchingRepresentation = true);
widget.pangeaMessageEvent
.representationByLanguageGlobal(
langCode: widget.pangeaMessageEvent.messageDisplayLangCode,
)
.onError((error, stackTrace) {
ErrorHandler.logError(
e: PangeaWarningError(error),
s: stackTrace,
m: "Error fetching representation",
data: {
"langCode": widget.pangeaMessageEvent.messageDisplayLangCode,
},
);
return null;
}).then((event) {
if (!mounted) return;
repEvent = event;
_setTextSpan(repEvent?.text ?? widget.pangeaMessageEvent.body);
}).whenComplete(() {
if (mounted) {
setState(() => _fetchingRepresentation = false);
}
});
_setTextSpan(widget.pangeaMessageEvent.body);
} else {
_setTextSpan(repEvent!.text);
}
}
@override
Widget build(BuildContext context) {
if (blur > 0) {
instructionsShowPopup(
context,
InstructionsEnum.blurMeansTranslate,
widget.pangeaMessageEvent.eventId,
);
}
//TODO - take out of build function of every message
final Widget richText = ToolbarSelectionArea(
event: widget.pangeaMessageEvent.event,
isOverlay: widget.isOverlay,
pangeaMessageEvent: widget.pangeaMessageEvent,
controller: widget.controller,
nextEvent: widget.nextEvent,
prevEvent: widget.prevEvent,
child: RichText(
text: TextSpan(
text: textSpan,
style: widget.style,
children: [
if (_fetchingRepresentation)
const WidgetSpan(
child: Padding(
padding: EdgeInsets.only(left: 5.0),
child: SizedBox(
height: 14,
width: 14,
child: CircularProgressIndicator(
strokeWidth: 2.0,
color: AppConfig.secondaryColor,
),
),
),
),
],
),
),
);
return blur > 0
? ImageFiltered(
imageFilter: ImageFilter.blur(
sigmaX: blur,
sigmaY: blur,
),
child: richText,
)
: richText;
}
Future<void> onIgnore() async {
debugPrint("PTODO implement onIgnore");
}
Future<void> onITStart() async {
debugPrint("PTODO implement onITStart");
}
Future<void> onReplacementSelect(
PangeaMatch pangeaMatch,
String replacement,
) async {
debugPrint("PTODO implement onReplacementSelect");
}
Future<void> onSentenceRewrite(String sentenceRewrite) async {
debugPrint("PTODO implement onSentenceRewrite");
}
}

View file

@ -48,11 +48,8 @@ class WordDataCard extends StatefulWidget {
class WordDataCardController extends State<WordDataCard> {
final PangeaController controller = MatrixState.pangeaController;
bool isLoadingWordNet = false;
bool isLoadingContextualDefinition = false;
ContextualDefinitionResponseModel? contextualDefinitionRes;
WordData? wordData;
Object? wordNetError;
Object? definitionError;
LanguageModel? activeL1;
@ -66,12 +63,9 @@ class WordDataCardController extends State<WordDataCard> {
activeL1 = controller.languageController.activeL1Model()!;
activeL2 = controller.languageController.activeL2Model()!;
if (activeL1 == null || activeL2 == null) {
wordNetError = noLanguages;
definitionError = noLanguages;
} else if (!widget.hasInfo) {
getContextualDefinition();
} else {
getWordNet();
getContextualDefinition();
}
super.initState();
}
@ -80,11 +74,7 @@ class WordDataCardController extends State<WordDataCard> {
void didUpdateWidget(covariant WordDataCard oldWidget) {
// debugger(when: kDebugMode);
if (oldWidget.word != widget.word) {
if (!widget.hasInfo) {
getContextualDefinition();
} else {
getWordNet();
}
getContextualDefinition();
}
super.didUpdateWidget(oldWidget);
}
@ -127,32 +117,6 @@ class WordDataCardController extends State<WordDataCard> {
}
}
Future<void> getWordNet() async {
if (mounted) {
setState(() {
wordData = null;
isLoadingWordNet = true;
});
}
try {
wordData = await controller.wordNet.getWordDataGlobal(
word: widget.word,
fullText: widget.fullText,
userL1: activeL1?.langCode,
userL2: activeL2?.langCode,
);
} catch (err) {
ErrorHandler.logError(
e: err,
s: StackTrace.current,
data: {"word": widget.word, "hasInfo": widget.hasInfo},
);
wordNetError = err;
} finally {
if (mounted) setState(() => isLoadingWordNet = false);
}
}
void handleGetDefinitionButtonPress() {
if (isLoadingContextualDefinition) return;
getContextualDefinition();
@ -172,12 +136,6 @@ class WordDataCardView extends StatelessWidget {
@override
Widget build(BuildContext context) {
if (controller.wordNetError != null) {
return CardErrorWidget(
error: controller.wordNetError!.toString(),
maxWidth: AppConfig.toolbarMinWidth,
);
}
if (controller.activeL1 == null || controller.activeL2 == null) {
ErrorHandler.logError(
m: "should not be here",
@ -210,30 +168,6 @@ class WordDataCardView extends StatelessWidget {
style: BotStyle.text(context),
),
const SizedBox(height: 5.0),
if (controller.wordData != null &&
controller.wordNetError == null &&
controller.activeL1 != null &&
controller.activeL2 != null)
WordNetInfo(
wordData: controller.wordData!,
activeL1: controller.activeL1!,
activeL2: controller.activeL2!,
),
if (controller.isLoadingWordNet)
const ToolbarContentLoadingIndicator(),
const SizedBox(height: 5.0),
// if (controller.widget.hasInfo &&
// !controller.isLoadingContextualDefinition &&
// controller.contextualDefinitionRes == null)
// Material(
// type: MaterialType.transparency,
// child: ListTile(
// leading: const BotFace(
// width: 40, expression: BotExpression.surprised),
// title: Text(L10n.of(context).askPangeaBot),
// onTap: controller.handleGetDefinitionButtonPress,
// ),
// ),
if (controller.isLoadingContextualDefinition)
const ToolbarContentLoadingIndicator(),
if (controller.contextualDefinitionRes != null)

View file

@ -8,6 +8,7 @@ import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pangea/choreographer/constants/choreo_constants.dart';
import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart';
import 'package:fluffychat/pangea/choreographer/controllers/it_controller.dart';
import 'package:fluffychat/pangea/choreographer/controllers/it_feedback_controller.dart';
import 'package:fluffychat/pangea/choreographer/widgets/igc/word_data_card.dart';
import 'package:fluffychat/pangea/choreographer/widgets/it_feedback_card.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
@ -17,7 +18,6 @@ import 'package:fluffychat/pangea/instructions/instructions_inline_tooltip.dart'
import 'package:fluffychat/pangea/learning_settings/pages/settings_learning.dart';
import 'package:fluffychat/widgets/matrix.dart';
import '../../common/utils/overlay.dart';
import '../controllers/it_feedback_controller.dart';
import '../models/it_response_model.dart';
import 'choice_array.dart';

View file

@ -12,7 +12,6 @@ import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pangea/analytics_misc/get_analytics_controller.dart';
import 'package:fluffychat/pangea/analytics_misc/put_analytics_controller.dart';
import 'package:fluffychat/pangea/choreographer/controllers/contextual_definition_controller.dart';
import 'package:fluffychat/pangea/choreographer/controllers/word_net_controller.dart';
import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart';
import 'package:fluffychat/pangea/events/controllers/message_data_controller.dart';
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
@ -28,7 +27,6 @@ import 'package:fluffychat/pangea/toolbar/controllers/tts_controller.dart';
import 'package:fluffychat/pangea/user/controllers/permissions_controller.dart';
import 'package:fluffychat/pangea/user/controllers/user_controller.dart';
import 'package:fluffychat/widgets/matrix.dart';
import '../../choreographer/controllers/it_feedback_controller.dart';
import '../utils/firebase_analytics.dart';
class PangeaController {
@ -39,12 +37,10 @@ class PangeaController {
late PermissionsController permissionsController;
late GetAnalyticsController getAnalytics;
late PutAnalyticsController putAnalytics;
late WordController wordNet;
late MessageDataController messageData;
// TODO: make these static so we can remove from here
late ContextualDefinitionController definitions;
late ITFeedbackController itFeedback;
late SubscriptionController subscriptionController;
late TextToSpeechController textToSpeech;
late SpeechToTextController speechToText;
@ -91,10 +87,8 @@ class PangeaController {
getAnalytics = GetAnalyticsController(this);
putAnalytics = PutAnalyticsController(this);
messageData = MessageDataController(this);
wordNet = WordController(this);
definitions = ContextualDefinitionController(this);
subscriptionController = SubscriptionController(this);
itFeedback = ITFeedbackController(this);
textToSpeech = TextToSpeechController(this);
speechToText = SpeechToTextController(this);
PAuthGaurd.pController = this;

View file

@ -10,14 +10,11 @@ import 'package:fluffychat/pangea/learning_settings/enums/language_level_type_en
import 'package:fluffychat/widgets/matrix.dart';
class Requests {
late String? baseUrl;
// Matrix access token
late String? accessToken;
late String? choreoApiKey;
//Question: How can we make baseUrl optional?
Requests({
this.accessToken,
this.baseUrl = '',
this.choreoApiKey,
});
@ -31,84 +28,31 @@ class Requests {
dynamic encoded;
encoded = jsonEncode(body);
debugPrint(baseUrl! + url);
final http.Response response = await http.post(
_uriBuilder(url),
body: encoded,
headers: _headers,
);
handleError(response, body: body);
return response;
}
Future<http.Response> put({
required String url,
required Map<dynamic, dynamic> body,
}) async {
body[ModelKey.cefrLevel] = MatrixState
.pangeaController.userController.profile.userSettings.cefrLevel.string;
dynamic encoded;
encoded = jsonEncode(body);
debugPrint(baseUrl! + url);
final http.Response response = await http.put(
_uriBuilder(url),
Uri.parse(url),
body: encoded,
headers: _headers,
);
handleError(response, body: body);
return response;
}
Future<http.Response> patch({
required String url,
required Map<dynamic, dynamic> body,
}) async {
body[ModelKey.cefrLevel] = MatrixState
.pangeaController.userController.profile.userSettings.cefrLevel.string;
dynamic encoded;
encoded = jsonEncode(body);
debugPrint(baseUrl! + url);
final http.Response response = await http.patch(
_uriBuilder(url),
body: encoded,
headers: _headers,
);
handleError(response, body: body);
return response;
}
Future<http.Response> get({required String url, String objectId = ""}) async {
Future<http.Response> get({required String url}) async {
final http.Response response =
await http.get(_uriBuilder(url + objectId), headers: _headers);
handleError(response, objectId: objectId);
await http.get(Uri.parse(url), headers: _headers);
handleError(response);
return response;
}
Uri _uriBuilder(url) =>
baseUrl != null ? Uri.parse(baseUrl! + url) : Uri.parse(url);
void addBreadcrumb(
http.Response response, {
Map<dynamic, dynamic>? body,
String? objectId,
}) {
debugPrint("Error - code: ${response.statusCode}");
debugPrint("api: ${response.request?.url}");
debugPrint("request body: ${body ?? objectId}");
debugPrint("request body: $body");
Sentry.addBreadcrumb(
Breadcrumb.http(
url: response.request?.url ?? Uri(path: "not available"),
@ -117,20 +61,23 @@ class Requests {
),
);
Sentry.addBreadcrumb(
Breadcrumb(data: {"body": body, "objectId": objectId}),
Breadcrumb(data: {"body": body}),
);
}
void handleError(
http.Response response, {
Map<dynamic, dynamic>? body,
String? objectId,
}) {
//PTODO - handle 401 error - unauthorized call
//kick them back to login?
if (response.statusCode == 401) {
final responseBody = jsonDecode(utf8.decode(response.bodyBytes));
if (responseBody['detail'] == 'No active subscription found') {
throw UnsubscribedException();
}
}
if (response.statusCode >= 400) {
addBreadcrumb(response, body: body, objectId: objectId);
addBreadcrumb(response, body: body);
throw response;
}
}
@ -142,7 +89,6 @@ class Requests {
};
if (accessToken != null) {
headers["Authorization"] = 'Bearer ${accessToken!}';
//headers["Matrix-Access-Token"] = accessToken!;
}
if (choreoApiKey != null) {
headers['api_key'] = choreoApiKey!;
@ -150,3 +96,5 @@ class Requests {
return headers;
}
}
class UnsubscribedException implements Exception {}

View file

@ -9,79 +9,73 @@ import 'package:fluffychat/pangea/common/config/environment.dart';
///
/// https://api.staging.pangea.chat/api/v1/
class PApiUrls {
static String choreoPrefix = "/choreo";
static String subscriptionPrefix = "/subscription";
static String accountPrefix = "/account";
static const String _choreoPrefix = "/choreo";
static const String _subscriptionPrefix = "/subscription";
static String get choreoEndpoint =>
"${Environment.choreoApi}${PApiUrls.choreoPrefix}";
static String get subscriptionEndpoint =>
"${Environment.choreoApi}${PApiUrls.subscriptionPrefix}";
static String get accountEndpoint =>
"${Environment.choreoApi}${PApiUrls.accountPrefix}";
static String get _choreoEndpoint =>
"${Environment.choreoApi}${PApiUrls._choreoPrefix}";
static String get _subscriptionEndpoint =>
"${Environment.choreoApi}${PApiUrls._subscriptionPrefix}";
/// ---------------------- Util --------------------------------------
static String appVersion = "${PApiUrls.choreoEndpoint}/version";
static String appVersion = "${PApiUrls._choreoEndpoint}/version";
/// ---------------------- Languages --------------------------------------
static String getLanguages = "${PApiUrls.choreoEndpoint}/languages_v2";
static String getLanguages = "${PApiUrls._choreoEndpoint}/languages_v2";
/// ---------------------- Users --------------------------------------
static String paymentLink = "${PApiUrls.subscriptionEndpoint}/payment_link";
static String paymentLink = "${PApiUrls._subscriptionEndpoint}/payment_link";
static String languageDetection =
"${PApiUrls.choreoEndpoint}/language_detection";
"${PApiUrls._choreoEndpoint}/language_detection";
static String igcLite = "${PApiUrls.choreoEndpoint}/grammar_lite";
static String spanDetails = "${PApiUrls.choreoEndpoint}/span_details";
static String igcLite = "${PApiUrls._choreoEndpoint}/grammar_lite";
static String spanDetails = "${PApiUrls._choreoEndpoint}/span_details";
static String wordNet = "${PApiUrls.choreoEndpoint}/wordnet";
static String simpleTranslation =
"${PApiUrls.choreoEndpoint}/translation/direct";
static String tokenize = "${PApiUrls.choreoEndpoint}/tokenize";
"${PApiUrls._choreoEndpoint}/translation/direct";
static String tokenize = "${PApiUrls._choreoEndpoint}/tokenize";
static String contextualDefinition =
"${PApiUrls.choreoEndpoint}/contextual_definition";
static String similarity = "${PApiUrls.choreoEndpoint}/similarity";
"${PApiUrls._choreoEndpoint}/contextual_definition";
static String itFeedback = "${PApiUrls.choreoEndpoint}/translation/feedback";
static String firstStep = "${PApiUrls._choreoEndpoint}/it_initialstep";
static String firstStep = "${PApiUrls.choreoEndpoint}/it_initialstep";
static String textToSpeech = "${PApiUrls.choreoEndpoint}/text_to_speech";
static String speechToText = "${PApiUrls.choreoEndpoint}/speech_to_text";
static String textToSpeech = "${PApiUrls._choreoEndpoint}/text_to_speech";
static String speechToText = "${PApiUrls._choreoEndpoint}/speech_to_text";
static String phoneticTranscription =
"${PApiUrls._choreoEndpoint}/phonetic_transcription";
static String messageActivityGeneration =
"${PApiUrls.choreoEndpoint}/practice";
"${PApiUrls._choreoEndpoint}/practice";
static String lemmaDictionary = "${PApiUrls.choreoEndpoint}/lemma_definition";
static String lemmaDictionary =
"${PApiUrls._choreoEndpoint}/lemma_definition";
static String lemmaDictionaryEdit =
"${PApiUrls.choreoEndpoint}/lemma_definition/edit";
static String morphDictionary = "${PApiUrls.choreoEndpoint}/morph_meaning";
"${PApiUrls._choreoEndpoint}/lemma_definition/edit";
static String morphDictionary = "${PApiUrls._choreoEndpoint}/morph_meaning";
static String activityPlan = "${PApiUrls.choreoEndpoint}/activity_plan";
static String activityPlanGeneration =
"${PApiUrls.choreoEndpoint}/activity_plan/generate";
static String activityPlanSearch =
"${PApiUrls.choreoEndpoint}/activity_plan/search";
// static String activityPlan = "${PApiUrls._choreoEndpoint}/activity_plan";
// static String activityPlanGeneration =
// "${PApiUrls._choreoEndpoint}/activity_plan/generate";
// static String activityPlanSearch =
// "${PApiUrls._choreoEndpoint}/activity_plan/search";
// static String activityModeList = "${PApiUrls._choreoEndpoint}/modes";
// static String objectiveList = "${PApiUrls._choreoEndpoint}/objectives";
// static String topicList = "${PApiUrls._choreoEndpoint}/topics";
static String activityModeList = "${PApiUrls.choreoEndpoint}/modes";
static String objectiveList = "${PApiUrls.choreoEndpoint}/objectives";
static String topicList = "${PApiUrls.choreoEndpoint}/topics";
static String activitySummary =
"${PApiUrls._choreoEndpoint}/activity_summary";
static String activitySummary = "${PApiUrls.choreoEndpoint}/activity_summary";
static String morphFeaturesAndTags = "${PApiUrls.choreoEndpoint}/morphs";
static String morphFeaturesAndTags = "${PApiUrls._choreoEndpoint}/morphs";
static String constructSummary =
"${PApiUrls.choreoEndpoint}/construct_summary";
"${PApiUrls._choreoEndpoint}/construct_summary";
///-------------------------------- revenue cat --------------------------
static String rcAppsChoreo = "${PApiUrls.subscriptionEndpoint}/app_ids";
static String rcAppsChoreo = "${PApiUrls._subscriptionEndpoint}/app_ids";
static String rcProductsChoreo =
"${PApiUrls.subscriptionEndpoint}/all_products";
static String rcProductsTrial = "${PApiUrls.subscriptionEndpoint}/free_trial";
"${PApiUrls._subscriptionEndpoint}/all_products";
static String rcProductsTrial =
"${PApiUrls._subscriptionEndpoint}/free_trial";
static String rcSubscription = PApiUrls.subscriptionEndpoint;
static String phoneticTranscription =
"${PApiUrls.choreoEndpoint}/phonetic_transcription";
static String rcSubscription = PApiUrls._subscriptionEndpoint;
}

View file

@ -4,17 +4,19 @@ class ErrorIndicator extends StatelessWidget {
final String message;
final double? iconSize;
final TextStyle? style;
final VoidCallback? onTap;
const ErrorIndicator({
super.key,
required this.message,
this.iconSize,
this.style,
this.onTap,
});
@override
Widget build(BuildContext context) {
return Row(
final content = Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
@ -32,5 +34,14 @@ class ErrorIndicator extends StatelessWidget {
),
],
);
if (onTap != null) {
return TextButton(
onPressed: onTap,
child: content,
);
}
return content;
}
}

View file

@ -506,40 +506,6 @@ class PangeaMessageEvent {
(filter?.call(element) ?? true),
);
Future<PangeaRepresentation?> representationByLanguageGlobal({
required String langCode,
}) async {
final RepresentationEvent? repLocal = representationByLanguage(langCode);
if (repLocal != null ||
langCode == LanguageKeys.unknownLanguage ||
langCode == LanguageKeys.mixedLanguage ||
langCode == LanguageKeys.multiLanguage) {
return repLocal?.content;
}
if (eventId.contains("Pangea Chat")) return null;
// should this just be the original event body?
// worth a conversation with the team
final PangeaRepresentation? basis = originalSent?.content;
// clear representations cache so the new representation event can be added
// when next requested
_representations = null;
return MatrixState.pangeaController.messageData.getPangeaRepresentation(
req: FullTextTranslationRequestModel(
text: basis?.text ?? _latestEdit.body,
srcLang: basis?.langCode,
tgtLang: langCode,
userL2: l2Code ?? LanguageKeys.unknownLanguage,
userL1: l1Code ?? LanguageKeys.unknownLanguage,
),
messageEvent: _event,
);
}
Future<String?> representationByDetectedLanguage() async {
LanguageDetectionResponse? resp;
try {

View file

@ -14,9 +14,6 @@ import 'package:fluffychat/pangea/constructs/construct_form.dart';
import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
import 'package:fluffychat/pangea/constructs/construct_level_enum.dart';
import 'package:fluffychat/pangea/events/models/pangea_token_text_model.dart';
import 'package:fluffychat/pangea/learning_settings/constants/language_constants.dart';
import 'package:fluffychat/pangea/lemmas/lemma_info_repo.dart';
import 'package:fluffychat/pangea/lemmas/lemma_info_request.dart';
import 'package:fluffychat/pangea/lemmas/user_set_lemma_info.dart';
import 'package:fluffychat/pangea/morphs/morph_features_enum.dart';
import 'package:fluffychat/pangea/morphs/morph_repo.dart';
@ -438,19 +435,6 @@ class PangeaToken {
.cast<ConstructUses>()
.toList();
Future<List<String>> getEmojiChoices() => LemmaInfoRepo.get(
LemmaInfoRequest(
lemma: lemma.text,
partOfSpeech: pos,
lemmaLang: MatrixState
.pangeaController.languageController.userL2?.langCode ??
LanguageKeys.unknownLanguage,
userL1: MatrixState
.pangeaController.languageController.userL1?.langCode ??
LanguageKeys.defaultLanguage,
),
).then((onValue) => onValue.emoji);
ConstructIdentifier get vocabConstructID => ConstructIdentifier(
lemma: lemma.text,
type: ConstructTypeEnum.vocab,

View file

@ -113,7 +113,7 @@ class LanguageController {
// return model;
}
bool get showTrancription =>
bool get showTranscription =>
(_pangeaController.languageController.userL1 != null &&
_pangeaController.languageController.userL2 != null &&
_pangeaController.languageController.userL1?.script !=

View file

@ -1,7 +1,4 @@
import 'dart:convert';
import 'dart:developer';
import 'package:flutter/foundation.dart';
import 'package:get_storage/get_storage.dart';
import 'package:http/http.dart';
@ -9,7 +6,6 @@ import 'package:http/http.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/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/lemmas/lemma_edit_request.dart';
import 'package:fluffychat/pangea/lemmas/lemma_info_request.dart';
import 'package:fluffychat/pangea/lemmas/lemma_info_response.dart';
@ -40,7 +36,8 @@ class LemmaInfoRepo {
return null;
}
static Future<LemmaInfoResponse> _fetch(LemmaInfoRequest request) async {
/// Get lemma info, prefering user set data over fetched data
static Future<LemmaInfoResponse> get(LemmaInfoRequest request) async {
final cached = getCached(request);
if (cached != null) return cached;
@ -58,52 +55,9 @@ class LemmaInfoRepo {
final response = LemmaInfoResponse.fromJson(decodedBody);
set(request, response);
// debugPrint(
// 'fetched data for ${request.lemma} ${response.toJson()}',
// );
return response;
}
/// Get lemma info, prefering user set data over fetched data
static Future<LemmaInfoResponse> get(LemmaInfoRequest request) async {
try {
return await _fetch(request);
// if the user has either emojis or meaning in the past, use those first
// final UserSetLemmaInfo? userSetLemmaInfo = request.cId.userLemmaInfo;
// final List<String> emojis = userSetLemmaInfo?.emojis ?? [];
// String? meaning = userSetLemmaInfo?.meaning;
// if the user has not set these, fetch from the server
// if (emojis.length < maxEmojisPerLemma || meaning == null) {
// final LemmaInfoResponse fetched = await _fetch(request);
// while (emojis.length < maxEmojisPerLemma && fetched.emoji.isNotEmpty) {
// final maybeToAdd = fetched.emoji.removeAt(0);
// if (!emojis.contains(maybeToAdd)) {
// emojis.add(maybeToAdd);
// }
// }
// meaning ??= fetched.meaning;
// } else {
// // debugPrint(
// // 'using user set data for ${request.lemma} ${userSetLemmaInfo?.toJson()}',
// // );
// }
// return LemmaInfoResponse(
// emoji: emojis,
// meaning: meaning,
// );
} catch (e) {
debugger(when: kDebugMode);
ErrorHandler.logError(e: e, data: request.toJson());
rethrow;
}
}
static Future<void> edit(LemmaEditRequest request) async {
final Requests req = Requests(
choreoApiKey: Environment.choreoApiKey,

View file

@ -3,6 +3,7 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pangea/common/network/requests.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/common/widgets/error_indicator.dart';
import 'package:fluffychat/pangea/events/models/pangea_token_text_model.dart';
@ -98,14 +99,16 @@ class _PhoneticTranscriptionWidgetState
.first.phoneticL1Transcription.content;
} catch (e, s) {
_error = e;
ErrorHandler.logError(
e: e,
s: s,
data: {
'text': widget.text,
'textLanguageCode': widget.textLanguage.langCode,
},
);
if (e is! UnsubscribedException) {
ErrorHandler.logError(
e: e,
s: s,
data: {
'text': widget.text,
'textLanguageCode': widget.textLanguage.langCode,
},
);
}
} finally {
if (mounted) {
setState(() {
@ -157,9 +160,20 @@ class _PhoneticTranscriptionWidgetState
mainAxisSize: MainAxisSize.min,
children: [
if (_error != null)
ErrorIndicator(
message: L10n.of(context).failedToFetchTranscription,
)
_error is UnsubscribedException
? ErrorIndicator(
message: L10n.of(context)
.subscribeToUnlockTranscriptions,
onTap: () {
MatrixState
.pangeaController.subscriptionController
.showPaywall(context);
},
)
: ErrorIndicator(
message:
L10n.of(context).failedToFetchTranscription,
)
else if (_isLoading || _transcription == null)
const SizedBox(
width: 16,

View file

@ -1,79 +0,0 @@
import 'dart:convert';
import 'dart:developer';
import 'package:flutter/foundation.dart';
import 'package:http/http.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/widgets/matrix.dart';
import '../../common/config/environment.dart';
import '../../common/network/requests.dart';
class GenerateImageeResponse {
final String imageUrl;
final String prompt;
GenerateImageeResponse({
required this.imageUrl,
required this.prompt,
});
factory GenerateImageeResponse.fromJson(Map<String, dynamic> json) {
return GenerateImageeResponse(
imageUrl: json['image_url'],
prompt: json['prompt'],
);
}
factory GenerateImageeResponse.error() {
// TODO: Implement better error handling
return GenerateImageeResponse(
imageUrl: 'https://i.imgur.com/2L2JYqk.png',
prompt: 'Error',
);
}
}
class GenerateImageRequest {
String prompt;
GenerateImageRequest({required this.prompt});
Map<String, dynamic> toJson() => {
'prompt': prompt,
};
}
class ImageRepo {
static Future<GenerateImageeResponse> fetchImage(
GenerateImageRequest request,
) async {
final Requests req = Requests(
choreoApiKey: Environment.choreoApiKey,
accessToken: MatrixState.pangeaController.userController.accessToken,
); // Set your API base URL
final requestBody = request.toJson();
try {
final Response res = await req.post(
url: '/generate-image/', // Endpoint in your FastAPI server
body: requestBody,
);
if (res.statusCode == 200) {
final decodedBody = jsonDecode(utf8.decode(res.bodyBytes));
return GenerateImageeResponse.fromJson(
decodedBody,
); // Convert response to ImageModel
} else {
throw Exception('Failed to load image');
}
} catch (err, stack) {
debugger(when: kDebugMode);
ErrorHandler.logError(e: err, s: stack, data: requestBody);
return GenerateImageeResponse
.error(); // Return an error model or handle accordingly
}
}
}

View file

@ -3,22 +3,13 @@ import 'package:flutter/material.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pangea/bot/utils/bot_style.dart';
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart';
import 'package:fluffychat/widgets/matrix.dart';
class MessageUnsubscribedCard extends StatelessWidget {
final MessageOverlayController controller;
const MessageUnsubscribedCard({
super.key,
required this.controller,
});
const MessageUnsubscribedCard({super.key});
@override
Widget build(BuildContext context) {
final bool inTrialWindow =
MatrixState.pangeaController.userController.inTrialWindow();
return Container(
constraints: const BoxConstraints(
maxWidth: AppConfig.toolbarMinWidth,
@ -31,31 +22,11 @@ class MessageUnsubscribedCard extends StatelessWidget {
L10n.of(context).subscribedToUnlockTools,
textAlign: TextAlign.center,
),
if (inTrialWindow) ...[
const SizedBox(height: 10),
SizedBox(
width: double.infinity,
child: TextButton(
onPressed: () {
MatrixState.pangeaController.subscriptionController
.activateNewUserTrial();
controller.updateToolbarMode(controller.toolbarMode);
},
style: ButtonStyle(
backgroundColor: WidgetStateProperty.all<Color>(
(Theme.of(context).colorScheme.primary).withAlpha(25),
),
),
child: Text(L10n.of(context).activateTrial),
),
),
],
const SizedBox(height: 10),
SizedBox(
width: double.infinity,
child: TextButton(
onPressed: () {
controller.widget.chatController.clearSelectedEvents();
MatrixState.pangeaController.subscriptionController
.showPaywall(context);
},

View file

@ -137,14 +137,20 @@ class OverlayMessage extends StatelessWidget {
event.numberEmotes > 0 &&
event.numberEmotes <= 3);
final isSubscribed =
MatrixState.pangeaController.subscriptionController.isSubscribed;
final showTranslation = overlayController.showTranslation &&
overlayController.translation != null;
overlayController.translation != null &&
isSubscribed != false;
final showTranscription =
overlayController.pangeaMessageEvent?.isAudioMessage == true;
overlayController.pangeaMessageEvent?.isAudioMessage == true &&
isSubscribed != false;
final showSpeechTranslation = overlayController.showSpeechTranslation &&
overlayController.speechTranslation != null;
overlayController.speechTranslation != null &&
isSubscribed != false;
final transcription = showTranscription
? Container(
@ -200,7 +206,7 @@ class OverlayMessage extends StatelessWidget {
isSelected: overlayController.isTokenSelected,
),
if (MatrixState.pangeaController
.languageController.showTrancription)
.languageController.showTranscription)
PhoneticTranscriptionWidget(
text: overlayController
.transcription!.transcript.text,

View file

@ -42,9 +42,7 @@ class ReadingAssistanceContentState extends State<ReadingAssistanceContent> {
MatrixState.pangeaController.subscriptionController.isSubscribed;
if (subscribed != null && !subscribed) {
return MessageUnsubscribedCard(
controller: widget.overlayController,
);
return const MessageUnsubscribedCard();
}
if (widget.overlayController.practiceSelection?.hasHiddenWordActivity ??

View file

@ -549,12 +549,15 @@ class SelectModeButtonsState extends State<SelectModeButtons> {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final modes = widget.overlayController.showLanguageAssistance
final isSubscribed =
MatrixState.pangeaController.subscriptionController.isSubscribed;
List<SelectMode> modes = widget.overlayController.showLanguageAssistance
? messageEvent?.isAudioMessage == true
? audioModes
: textModes
: [];
if (isSubscribed == false) modes = [];
return Material(
type: MaterialType.transparency,
child: SizedBox(

View file

@ -1,13 +1,12 @@
import 'dart:developer';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pangea/analytics_misc/construct_use_model.dart';
import 'package:fluffychat/pangea/analytics_misc/text_loading_shimmer.dart';
import 'package:fluffychat/pangea/common/network/requests.dart';
import 'package:fluffychat/pangea/common/widgets/error_indicator.dart';
import 'package:fluffychat/pangea/toolbar/widgets/word_zoom/lemma_meaning_builder.dart';
import 'package:fluffychat/widgets/matrix.dart';
class LemmaMeaningWidget extends StatelessWidget {
final LemmaMeaningBuilderState controller;
@ -31,7 +30,16 @@ class LemmaMeaningWidget extends StatelessWidget {
}
if (controller.error != null) {
debugger(when: kDebugMode);
if (controller.error is UnsubscribedException) {
return ErrorIndicator(
message: L10n.of(context).subscribeToUnlockDefinitions,
style: style,
onTap: () {
MatrixState.pangeaController.subscriptionController
.showPaywall(context);
},
);
}
return ErrorIndicator(
message: L10n.of(context).errorFetchingDefinition,
style: style,

View file

@ -1,323 +0,0 @@
import 'dart:developer';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/common/utils/overlay.dart';
import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart';
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
import 'package:fluffychat/pangea/lemmas/construct_xp_widget.dart';
import 'package:fluffychat/pangea/morphs/edit_morph_widget.dart';
import 'package:fluffychat/pangea/morphs/get_grammar_copy.dart';
import 'package:fluffychat/pangea/morphs/morph_features_enum.dart';
import 'package:fluffychat/pangea/morphs/morph_icon.dart';
import 'package:fluffychat/pangea/morphs/morph_meaning/morph_info_repo.dart';
import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart';
import 'package:fluffychat/pangea/toolbar/enums/message_mode_enum.dart';
import 'package:fluffychat/pangea/toolbar/reading_assistance_input_row/morph_selection.dart';
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart';
import 'package:fluffychat/pangea/toolbar/widgets/practice_activity/word_zoom_activity_button.dart';
import 'package:fluffychat/widgets/matrix.dart';
class MorphologicalListItem extends StatelessWidget {
final MorphFeaturesEnum morphFeature;
final PangeaToken token;
final MessageOverlayController overlayController;
// final VoidCallback editMorph;
const MorphologicalListItem({
required this.morphFeature,
required this.token,
required this.overlayController,
// required this.editMorph,
super.key,
});
bool get shouldDoActivity =>
overlayController.hideWordCardContent &&
overlayController.practiceSelection?.hasActiveActivityByToken(
ActivityTypeEnum.morphId,
token,
morphFeature,
) ==
true;
bool get isSelected =>
overlayController.toolbarMode == MessageMode.wordMorph &&
overlayController.selectedMorph?.morph == morphFeature;
String get morphTag => token.getMorphTag(morphFeature) ?? "X";
ConstructIdentifier get cId =>
token.morphIdByFeature(morphFeature) ??
ConstructIdentifier(
type: ConstructTypeEnum.morph,
category: morphFeature.name,
lemma: morphTag,
);
void _openDefintionPopup(BuildContext context) async {
const width = 300.0;
const height = 200.0;
try {
if (overlayController.pangeaMessageEvent == null) {
return;
}
OverlayUtil.showPositionedCard(
context: context,
cardToShow: MorphMeaningPopup(
token: token,
pangeaMessageEvent: overlayController.pangeaMessageEvent!,
cId: cId,
width: width,
height: height,
refresh: () {
overlayController.onMorphActivitySelect(
MorphSelection(token, morphFeature),
);
},
),
transformTargetId: cId.string,
backDropToDismiss: true,
borderColor: Theme.of(context).colorScheme.primary,
closePrevOverlay: false,
addBorder: false,
maxHeight: height,
maxWidth: width,
);
} catch (e, s) {
debugger(when: kDebugMode);
ErrorHandler.logError(
data: cId.toJson(),
e: e,
s: s,
);
}
}
@override
Widget build(BuildContext context) {
return CompositedTransformTarget(
link: MatrixState.pAnyState.layerLinkAndKey(cId.string).link,
child: SizedBox(
key: MatrixState.pAnyState.layerLinkAndKey(cId.string).key,
width: 40,
height: 40,
child: WordZoomActivityButton(
icon: shouldDoActivity
? const Icon(Symbols.toys_and_games)
: MorphIcon(
morphFeature: morphFeature,
morphTag: token.getMorphTag(morphFeature),
size: const Size(24, 24),
),
isSelected: isSelected,
onPressed: () {
overlayController
.onMorphActivitySelect(MorphSelection(token, morphFeature));
_openDefintionPopup(context);
},
tooltip: shouldDoActivity
? morphFeature.getDisplayCopy(context)
: getGrammarCopy(
category: morphFeature.name,
lemma: morphTag,
context: context,
),
opacity: isSelected ? 1 : 0.7,
),
),
);
}
}
class MorphMeaningPopup extends StatefulWidget {
final PangeaToken token;
final PangeaMessageEvent pangeaMessageEvent;
final ConstructIdentifier cId;
final double width;
final double height;
final VoidCallback refresh;
const MorphMeaningPopup({
super.key,
required this.token,
required this.pangeaMessageEvent,
required this.cId,
required this.width,
required this.height,
required this.refresh,
});
@override
State<MorphMeaningPopup> createState() => MorphMeaningPopupState();
}
class MorphMeaningPopupState extends State<MorphMeaningPopup> {
MorphFeaturesEnum get _morphFeature =>
MorphFeaturesEnumExtension.fromString(widget.cId.category);
String get _morphTag => widget.cId.lemma;
String? _defintion;
bool _isEditMode = false;
@override
void initState() {
super.initState();
_fetchDefinition();
}
@override
void didUpdateWidget(covariant MorphMeaningPopup oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.cId != widget.cId) {
_fetchDefinition();
}
}
Future<void> _fetchDefinition() async {
try {
final response = await MorphInfoRepo.get(
feature: _morphFeature,
tag: _morphTag,
);
if (mounted) {
setState(
() => _defintion = response ?? L10n.of(context).meaningNotFound,
);
}
} catch (e, s) {
debugger(when: kDebugMode);
ErrorHandler.logError(
data: widget.cId.toJson(),
e: e,
s: s,
);
}
}
void _setEditMode(bool editing) {
setState(() => _isEditMode = editing);
}
@override
Widget build(BuildContext context) {
return Material(
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
child: Stack(
children: [
Container(
padding: const EdgeInsets.all(8),
height: widget.height,
width: widget.width,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
boxShadow: [
BoxShadow(
color: Theme.of(context).colorScheme.onSurface.withAlpha(50),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
alignment: Alignment.center,
child: _isEditMode
? EditMorphWidget(
token: widget.token,
pangeaMessageEvent: widget.pangeaMessageEvent,
morphFeature: _morphFeature,
onClose: () {
_setEditMode(false);
_fetchDefinition();
widget.refresh();
},
)
: SingleChildScrollView(
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
spacing: 16.0,
children: [
SizedBox(
width: 24.0,
height: 24.0,
child: MorphIcon(
morphFeature: _morphFeature,
morphTag: _morphTag,
),
),
Flexible(
child: Text(
textAlign: TextAlign.center,
getGrammarCopy(
category: _morphFeature.name,
lemma: _morphTag,
context: context,
) ??
_morphTag,
style: Theme.of(context).textTheme.titleMedium,
),
),
if (MatrixState.pangeaController.getAnalytics
.constructListModel
.getConstructUses(widget.cId) !=
null)
ConstructXpWidget(
id: widget.cId,
onTap: () => context.go(
"/rooms/analytics/${ConstructTypeEnum.morph.string}/${widget.cId.string}",
),
),
],
),
Padding(
padding: const EdgeInsets.only(top: 16.0),
child: _defintion != null
? Text(
_defintion!,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyMedium,
)
: const LinearProgressIndicator(),
),
],
),
),
),
if (!_isEditMode)
Positioned(
top: 12.0,
right: 12.0,
child: MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
child: Icon(
Icons.edit_outlined,
size: 20.0,
color: Theme.of(context).disabledColor,
),
onTap: () {
_setEditMode(true);
},
),
),
),
],
),
);
}
}

View file

@ -190,7 +190,7 @@ class WordZoomWidget extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
children: [
if (MatrixState.pangeaController.languageController
.showTrancription)
.showTranscription)
PhoneticTranscriptionWidget(
text: token.text.content,
textLanguage: PLanguageStore.byLangCode(