diff --git a/lib/pangea/common/utils/async_state.dart b/lib/pangea/common/utils/async_state.dart index a9fd60cb5..f14b88a61 100644 --- a/lib/pangea/common/utils/async_state.dart +++ b/lib/pangea/common/utils/async_state.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:fluffychat/pangea/common/utils/error_handler.dart'; @@ -88,6 +90,10 @@ abstract class AsyncLoader { final result = await fetch(); if (_disposed) return; state.value = AsyncState.loaded(result); + } on HttpException catch (e) { + if (!_disposed) { + state.value = AsyncState.error(e); + } } catch (e, s) { ErrorHandler.logError( e: e, diff --git a/lib/pangea/constructs/construct_identifier.dart b/lib/pangea/constructs/construct_identifier.dart index 75878cb74..aa980be0a 100644 --- a/lib/pangea/constructs/construct_identifier.dart +++ b/lib/pangea/constructs/construct_identifier.dart @@ -2,6 +2,7 @@ import 'dart:developer'; import 'package:flutter/foundation.dart'; +import 'package:async/async.dart'; import 'package:collection/collection.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; @@ -150,7 +151,7 @@ class ConstructIdentifier { uses: [], ); - LemmaInfoRequest get _lemmaInfoRequest => LemmaInfoRequest( + LemmaInfoRequest get lemmaInfoRequest => LemmaInfoRequest( partOfSpeech: category, lemmaLang: MatrixState.pangeaController.userController.userL2?.langCodeShort ?? @@ -162,8 +163,9 @@ class ConstructIdentifier { ); /// [lemmmaLang] if not set, assumed to be userL2 - Future getLemmaInfo() => LemmaInfoRepo.get( - _lemmaInfoRequest, + Future> getLemmaInfo() => LemmaInfoRepo.get( + MatrixState.pangeaController.userController.accessToken, + lemmaInfoRequest, ); List get userSetEmoji => userLemmaInfo.emojis ?? []; diff --git a/lib/pangea/languages/language_arc_model.dart b/lib/pangea/languages/language_arc_model.dart index 3e450c9c2..c55d8a66b 100644 --- a/lib/pangea/languages/language_arc_model.dart +++ b/lib/pangea/languages/language_arc_model.dart @@ -22,4 +22,12 @@ class LanguageArc { 'l2': l2.toJson(), }; } + + @override + int get hashCode => l1.hashCode ^ l2.hashCode; + + @override + bool operator ==(Object other) { + return other is LanguageArc && other.l1 == l1 && other.l2 == l2; + } } diff --git a/lib/pangea/lemmas/lemma_info_repo.dart b/lib/pangea/lemmas/lemma_info_repo.dart index bdb3d3763..02d6712ef 100644 --- a/lib/pangea/lemmas/lemma_info_repo.dart +++ b/lib/pangea/lemmas/lemma_info_repo.dart @@ -1,48 +1,105 @@ import 'dart:convert'; +import 'dart:io'; +import 'package:async/async.dart'; import 'package:get_storage/get_storage.dart'; 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_info_request.dart'; import 'package:fluffychat/pangea/lemmas/lemma_info_response.dart'; -import 'package:fluffychat/widgets/matrix.dart'; + +class _LemmaInfoCacheItem { + final Future> resultFuture; + final DateTime timestamp; + + const _LemmaInfoCacheItem({ + required this.resultFuture, + required this.timestamp, + }); +} class LemmaInfoRepo { - static final GetStorage _lemmaStorage = GetStorage('lemma_storage'); + // In-memory cache + static final Map _cache = {}; + static const Duration _cacheDuration = Duration(minutes: 10); - static void set(LemmaInfoRequest request, LemmaInfoResponse response) { - // set expireAt if not set - response.expireAt ??= DateTime.now().add(const Duration(days: 100)); - _lemmaStorage.write(request.storageKey, response.toJson()); - } - - static LemmaInfoResponse? getCached(LemmaInfoRequest request) { - final cachedJson = _lemmaStorage.read(request.storageKey); - - final cached = - cachedJson == null ? null : LemmaInfoResponse.fromJson(cachedJson); + // Persistent storage + static final GetStorage _storage = GetStorage('lemma_storage'); + /// Public entry point + static Future> get( + String accessToken, + LemmaInfoRequest request, + ) { + // 1. Try memory cache + final cached = _getCached(request); if (cached != null) { - if (DateTime.now().isBefore(cached.expireAt!)) { - return cached; - } else { - _lemmaStorage.remove(request.storageKey); - } + return cached; } - return null; + + // 2. Try disk cache + final stored = _getStored(request); + if (stored != null) { + return Future.value(Result.value(stored)); + } + + // 3. Fetch from network (safe future) + final future = _safeFetch(accessToken, request); + + // 4. Save to in-memory cache + _cache[request.hashCode.toString()] = _LemmaInfoCacheItem( + resultFuture: future, + timestamp: DateTime.now(), + ); + + // 5. Write to disk *after* the fetch finishes, without rethrowing + writeToDisk(request, future); + + return future; } - /// Get lemma info, prefering user set data over fetched data - static Future get(LemmaInfoRequest request) async { - final cached = getCached(request); - if (cached != null) return cached; + static Future set( + LemmaInfoRequest request, + LemmaInfoResponse resultFuture, + ) async { + final key = request.hashCode.toString(); + try { + await _storage.write(key, resultFuture.toJson()); + _cache.remove(key); // Invalidate in-memory cache + } catch (e, s) { + ErrorHandler.logError( + e: e, + s: s, + data: {'lemma': request.lemma}, + ); + } + } - final Requests req = Requests( + static Future> _safeFetch( + String token, + LemmaInfoRequest request, + ) async { + try { + final resp = await _fetch(token, request); + return Result.value(resp); + } catch (e, s) { + // Ensure error is logged and converted to a Result + ErrorHandler.logError(e: e, s: s, data: request.toJson()); + return Result.error(e); + } + } + + static Future _fetch( + String accessToken, + LemmaInfoRequest request, + ) async { + final req = Requests( choreoApiKey: Environment.choreoApiKey, - accessToken: MatrixState.pangeaController.userController.accessToken, + accessToken: accessToken, ); final Response res = await req.post( @@ -50,10 +107,59 @@ class LemmaInfoRepo { body: request.toJson(), ); - final decodedBody = jsonDecode(utf8.decode(res.bodyBytes)); - final response = LemmaInfoResponse.fromJson(decodedBody); + if (res.statusCode != 200) { + throw HttpException( + 'Failed to fetch lemma info: ${res.statusCode} ${res.reasonPhrase}', + ); + } - set(request, response); - return response; + return LemmaInfoResponse.fromJson( + jsonDecode(utf8.decode(res.bodyBytes)), + ); + } + + static Future>? _getCached( + LemmaInfoRequest request, + ) { + final now = DateTime.now(); + final key = request.hashCode.toString(); + + // Remove stale entries first + _cache.removeWhere( + (_, item) => now.difference(item.timestamp) >= _cacheDuration, + ); + + final item = _cache[key]; + return item?.resultFuture; + } + + static Future writeToDisk( + LemmaInfoRequest request, + Future> resultFuture, + ) async { + final result = await resultFuture; // SAFE: never throws + + if (!result.isValue) return; // only cache successful responses + await set(request, result.asValue!.value); + } + + static LemmaInfoResponse? _getStored( + LemmaInfoRequest request, + ) { + final key = request.hashCode.toString(); + try { + final entry = _storage.read(key); + if (entry == null) return null; + + return LemmaInfoResponse.fromJson(entry); + } catch (e, s) { + ErrorHandler.logError( + e: e, + s: s, + data: {'lemma': request.lemma}, + ); + _storage.remove(key); + return null; + } } } diff --git a/lib/pangea/lemmas/lemma_info_response.dart b/lib/pangea/lemmas/lemma_info_response.dart index fb5bca198..6edec1fc7 100644 --- a/lib/pangea/lemmas/lemma_info_response.dart +++ b/lib/pangea/lemmas/lemma_info_response.dart @@ -3,12 +3,10 @@ import 'package:fluffychat/pangea/events/models/content_feedback.dart'; class LemmaInfoResponse implements JsonSerializable { final List emoji; final String meaning; - DateTime? expireAt; LemmaInfoResponse({ required this.emoji, required this.meaning, - this.expireAt, }); factory LemmaInfoResponse.fromJson(Map json) { @@ -16,18 +14,19 @@ class LemmaInfoResponse implements JsonSerializable { // NOTE: This is a workaround for the fact that the server sometimes sends more than 3 emojis emoji: (json['emoji'] as List).map((e) => e as String).toList(), meaning: json['meaning'] as String, - expireAt: json['expireAt'] == null - ? null - : DateTime.parse(json['expireAt'] as String), ); } + static LemmaInfoResponse get error => LemmaInfoResponse( + emoji: [], + meaning: 'ERROR', + ); + @override Map toJson() { return { 'emoji': emoji, 'meaning': meaning, - 'expireAt': expireAt?.toIso8601String(), }; } diff --git a/lib/pangea/lemmas/lemma_meaning_builder.dart b/lib/pangea/lemmas/lemma_meaning_builder.dart index f1b1560dc..30603329c 100644 --- a/lib/pangea/lemmas/lemma_meaning_builder.dart +++ b/lib/pangea/lemmas/lemma_meaning_builder.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:fluffychat/pangea/common/utils/async_state.dart'; import 'package:fluffychat/pangea/constructs/construct_identifier.dart'; import 'package:fluffychat/pangea/languages/language_constants.dart'; import 'package:fluffychat/pangea/lemmas/lemma_info_repo.dart'; @@ -7,6 +8,25 @@ import 'package:fluffychat/pangea/lemmas/lemma_info_request.dart'; import 'package:fluffychat/pangea/lemmas/lemma_info_response.dart'; import 'package:fluffychat/widgets/matrix.dart'; +class _LemmaMeaningLoader extends AsyncLoader { + final LemmaInfoRequest request; + _LemmaMeaningLoader(this.request) : super(); + + @override + Future fetch() async { + final result = await LemmaInfoRepo.get( + MatrixState.pangeaController.userController.accessToken, + request, + ); + + if (result.isError) { + throw result.asError!.error; + } + + return result.asValue!.value; + } +} + class LemmaMeaningBuilder extends StatefulWidget { final String langCode; final ConstructIdentifier constructId; @@ -27,14 +47,12 @@ class LemmaMeaningBuilder extends StatefulWidget { } class LemmaMeaningBuilderState extends State { - LemmaInfoResponse? lemmaInfo; - bool isLoading = true; - Object? error; + late _LemmaMeaningLoader _loader; @override void initState() { super.initState(); - _fetchLemmaMeaning(); + _reload(); } @override @@ -42,10 +60,25 @@ class LemmaMeaningBuilderState extends State { super.didUpdateWidget(oldWidget); if (oldWidget.constructId != widget.constructId || oldWidget.langCode != widget.langCode) { - _fetchLemmaMeaning(); + _loader.dispose(); + _reload(); } } + @override + void dispose() { + _loader.dispose(); + super.dispose(); + } + + bool get isLoading => _loader.isLoading; + bool get isError => _loader.isError; + + Object? get error => + isError ? (_loader.state.value as AsyncError).error : null; + + LemmaInfoResponse? get lemmaInfo => _loader.value; + LemmaInfoRequest get _request => LemmaInfoRequest( lemma: widget.constructId.lemma, partOfSpeech: widget.constructId.category, @@ -54,27 +87,19 @@ class LemmaMeaningBuilderState extends State { LanguageKeys.defaultLanguage, ); - Future _fetchLemmaMeaning() async { - setState(() { - isLoading = true; - error = null; - }); - - try { - final resp = await LemmaInfoRepo.get(_request); - lemmaInfo = resp; - } catch (e) { - error = e; - } finally { - if (mounted) setState(() => isLoading = false); - } + void _reload() { + _loader = _LemmaMeaningLoader(_request); + _loader.load(); } @override Widget build(BuildContext context) { - return widget.builder( - context, - this, + return ValueListenableBuilder( + valueListenable: _loader.state, + builder: (context, _, __) => widget.builder( + context, + this, + ), ); } } diff --git a/lib/pangea/phonetic_transcription/phonetic_transcription_builder.dart b/lib/pangea/phonetic_transcription/phonetic_transcription_builder.dart new file mode 100644 index 000000000..366f9b1fe --- /dev/null +++ b/lib/pangea/phonetic_transcription/phonetic_transcription_builder.dart @@ -0,0 +1,110 @@ +import 'package:flutter/material.dart'; + +import 'package:fluffychat/pangea/common/utils/async_state.dart'; +import 'package:fluffychat/pangea/events/models/pangea_token_text_model.dart'; +import 'package:fluffychat/pangea/languages/language_arc_model.dart'; +import 'package:fluffychat/pangea/languages/language_model.dart'; +import 'package:fluffychat/pangea/phonetic_transcription/phonetic_transcription_request.dart'; +import 'package:fluffychat/widgets/matrix.dart'; +import 'phonetic_transcription_repo.dart'; + +class _TranscriptLoader extends AsyncLoader { + final PhoneticTranscriptionRequest request; + _TranscriptLoader(this.request) : super(); + + @override + Future fetch() async { + final resp = await PhoneticTranscriptionRepo.get( + MatrixState.pangeaController.userController.accessToken, + request, + ); + + if (resp.isError) { + throw resp.asError!.error; + } + + return resp.asValue!.value.phoneticTranscriptionResult.phoneticTranscription + .first.phoneticL1Transcription.content; + } +} + +class PhoneticTranscriptionBuilder extends StatefulWidget { + final LanguageModel textLanguage; + final String text; + + final Widget Function( + BuildContext context, + PhoneticTranscriptionBuilderState controller, + ) builder; + + const PhoneticTranscriptionBuilder({ + super.key, + required this.textLanguage, + required this.text, + required this.builder, + }); + + @override + PhoneticTranscriptionBuilderState createState() => + PhoneticTranscriptionBuilderState(); +} + +class PhoneticTranscriptionBuilderState + extends State { + late _TranscriptLoader _loader; + + @override + void initState() { + super.initState(); + _reload(); + } + + @override + void didUpdateWidget(covariant PhoneticTranscriptionBuilder oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.text != widget.text || + oldWidget.textLanguage != widget.textLanguage) { + _loader.dispose(); + _reload(); + } + } + + @override + void dispose() { + _loader.dispose(); + super.dispose(); + } + + bool get isLoading => _loader.isLoading; + bool get isError => _loader.isError; + + Object? get error => + isError ? (_loader.state.value as AsyncError).error : null; + + String? get transcription => _loader.value; + + PhoneticTranscriptionRequest get _transcriptRequest => + PhoneticTranscriptionRequest( + arc: LanguageArc( + l1: MatrixState.pangeaController.userController.userL1!, + l2: widget.textLanguage, + ), + content: PangeaTokenText.fromString(widget.text), + ); + + void _reload() { + _loader = _TranscriptLoader(_transcriptRequest); + _loader.load(); + } + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder( + valueListenable: _loader.state, + builder: (context, _, __) => widget.builder( + context, + this, + ), + ); + } +} diff --git a/lib/pangea/phonetic_transcription/phonetic_transcription_repo.dart b/lib/pangea/phonetic_transcription/phonetic_transcription_repo.dart index 7fe6885fe..33760bc9a 100644 --- a/lib/pangea/phonetic_transcription/phonetic_transcription_repo.dart +++ b/lib/pangea/phonetic_transcription/phonetic_transcription_repo.dart @@ -1,8 +1,7 @@ import 'dart:convert'; -import 'dart:developer'; - -import 'package:flutter/foundation.dart'; +import 'dart:io'; +import 'package:async/async.dart'; import 'package:get_storage/get_storage.dart'; import 'package:http/http.dart'; @@ -12,39 +11,95 @@ import 'package:fluffychat/pangea/common/network/urls.dart'; import 'package:fluffychat/pangea/common/utils/error_handler.dart'; import 'package:fluffychat/pangea/phonetic_transcription/phonetic_transcription_request.dart'; import 'package:fluffychat/pangea/phonetic_transcription/phonetic_transcription_response.dart'; -import 'package:fluffychat/widgets/matrix.dart'; + +class _PhoneticTranscriptionCacheItem { + final Future> resultFuture; + final DateTime timestamp; + + const _PhoneticTranscriptionCacheItem({ + required this.resultFuture, + required this.timestamp, + }); +} class PhoneticTranscriptionRepo { + // In-memory cache + static final Map _cache = {}; + static const Duration _cacheDuration = Duration(minutes: 10); + +// Persistent storage static final GetStorage _storage = GetStorage('phonetic_transcription_storage'); + static Future> get( + String accessToken, + PhoneticTranscriptionRequest request, + ) { + // 1. Try memory cache + final cached = _getCached(request); + if (cached != null) { + return cached; + } + + // 2. Try disk cache + final stored = _getStored(request); + if (stored != null) { + return Future.value(Result.value(stored)); + } + + // 3. Fetch from network (safe future) + final future = _safeFetch(accessToken, request); + + // 4. Save to in-memory cache + _cache[request.hashCode.toString()] = _PhoneticTranscriptionCacheItem( + resultFuture: future, + timestamp: DateTime.now(), + ); + + // 5. Write to disk *after* the fetch finishes, without rethrowing + writeToDisk(request, future); + + return future; + } + static Future set( PhoneticTranscriptionRequest request, - PhoneticTranscriptionResponse response, + PhoneticTranscriptionResponse resultFuture, ) async { - response.expireAt ??= DateTime.now().add(const Duration(days: 100)); - await _storage.write(request.storageKey, response.toJson()); + final key = request.hashCode.toString(); + try { + await _storage.write(key, resultFuture.toJson()); + _cache.remove(key); // Invalidate in-memory cache + } catch (e, s) { + ErrorHandler.logError( + e: e, + s: s, + data: {'request': request.toJson()}, + ); + } + } + + static Future> _safeFetch( + String token, + PhoneticTranscriptionRequest request, + ) async { + try { + final resp = await _fetch(token, request); + return Result.value(resp); + } catch (e, s) { + // Ensure error is logged and converted to a Result + ErrorHandler.logError(e: e, s: s, data: request.toJson()); + return Result.error(e); + } } static Future _fetch( + String accessToken, PhoneticTranscriptionRequest request, ) async { - final cachedJson = _storage.read(request.storageKey); - final cached = cachedJson == null - ? null - : PhoneticTranscriptionResponse.fromJson(cachedJson); - - if (cached != null) { - if (DateTime.now().isBefore(cached.expireAt!)) { - return cached; - } else { - _storage.remove(request.storageKey); - } - } - - final Requests req = Requests( + final req = Requests( choreoApiKey: Environment.choreoApiKey, - accessToken: MatrixState.pangeaController.userController.accessToken, + accessToken: accessToken, ); final Response res = await req.post( @@ -52,21 +107,59 @@ class PhoneticTranscriptionRepo { body: request.toJson(), ); - final decodedBody = jsonDecode(utf8.decode(res.bodyBytes)); - final response = PhoneticTranscriptionResponse.fromJson(decodedBody); - set(request, response); - return response; + if (res.statusCode != 200) { + throw HttpException( + 'Failed to fetch phonetic transcription: ${res.statusCode} ${res.reasonPhrase}', + ); + } + + return PhoneticTranscriptionResponse.fromJson( + jsonDecode(utf8.decode(res.bodyBytes)), + ); } - static Future get( + static Future>? _getCached( PhoneticTranscriptionRequest request, + ) { + final now = DateTime.now(); + final key = request.hashCode.toString(); + + // Remove stale entries first + _cache.removeWhere( + (_, item) => now.difference(item.timestamp) >= _cacheDuration, + ); + + final item = _cache[key]; + return item?.resultFuture; + } + + static Future writeToDisk( + PhoneticTranscriptionRequest request, + Future> resultFuture, ) async { + final result = await resultFuture; // SAFE: never throws + + if (!result.isValue) return; // only cache successful responses + await set(request, result.asValue!.value); + } + + static PhoneticTranscriptionResponse? _getStored( + PhoneticTranscriptionRequest request, + ) { + final key = request.hashCode.toString(); try { - return await _fetch(request); - } catch (e) { - debugger(when: kDebugMode); - ErrorHandler.logError(e: e, data: request.toJson()); - rethrow; + final entry = _storage.read(key); + if (entry == null) return null; + + return PhoneticTranscriptionResponse.fromJson(entry); + } catch (e, s) { + ErrorHandler.logError( + e: e, + s: s, + data: {'request': request.toJson()}, + ); + _storage.remove(key); + return null; } } } diff --git a/lib/pangea/phonetic_transcription/phonetic_transcription_request.dart b/lib/pangea/phonetic_transcription/phonetic_transcription_request.dart index be229cba1..464193b01 100644 --- a/lib/pangea/phonetic_transcription/phonetic_transcription_request.dart +++ b/lib/pangea/phonetic_transcription/phonetic_transcription_request.dart @@ -30,4 +30,16 @@ class PhoneticTranscriptionRequest { } String get storageKey => '${arc.l1}-${arc.l2}-${content.hashCode}'; + + @override + int get hashCode => + content.hashCode ^ arc.hashCode ^ requiresTokenization.hashCode; + + @override + bool operator ==(Object other) { + return other is PhoneticTranscriptionRequest && + other.content == content && + other.arc == arc && + other.requiresTokenization == requiresTokenization; + } } diff --git a/lib/pangea/phonetic_transcription/phonetic_transcription_response.dart b/lib/pangea/phonetic_transcription/phonetic_transcription_response.dart index 0f9a62b43..612ef42c1 100644 --- a/lib/pangea/phonetic_transcription/phonetic_transcription_response.dart +++ b/lib/pangea/phonetic_transcription/phonetic_transcription_response.dart @@ -102,14 +102,12 @@ class PhoneticTranscriptionResponse { final Map tokenization; // You can define a typesafe model if needed final PhoneticTranscription phoneticTranscriptionResult; - DateTime? expireAt; PhoneticTranscriptionResponse({ required this.arc, required this.content, required this.tokenization, required this.phoneticTranscriptionResult, - this.expireAt, }); factory PhoneticTranscriptionResponse.fromJson(Map json) { @@ -121,9 +119,6 @@ class PhoneticTranscriptionResponse { phoneticTranscriptionResult: PhoneticTranscription.fromJson( json['phonetic_transcription_result'] as Map, ), - expireAt: json['expireAt'] == null - ? null - : DateTime.parse(json['expireAt'] as String), ); } @@ -133,7 +128,6 @@ class PhoneticTranscriptionResponse { 'content': content.toJson(), 'tokenization': tokenization, 'phonetic_transcription_result': phoneticTranscriptionResult.toJson(), - 'expireAt': expireAt?.toIso8601String(), }; } diff --git a/lib/pangea/phonetic_transcription/phonetic_transcription_widget.dart b/lib/pangea/phonetic_transcription/phonetic_transcription_widget.dart index ae7b533b1..afe549d73 100644 --- a/lib/pangea/phonetic_transcription/phonetic_transcription_widget.dart +++ b/lib/pangea/phonetic_transcription/phonetic_transcription_widget.dart @@ -4,13 +4,9 @@ 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'; -import 'package:fluffychat/pangea/languages/language_arc_model.dart'; import 'package:fluffychat/pangea/languages/language_model.dart'; -import 'package:fluffychat/pangea/phonetic_transcription/phonetic_transcription_repo.dart'; -import 'package:fluffychat/pangea/phonetic_transcription/phonetic_transcription_request.dart'; +import 'package:fluffychat/pangea/phonetic_transcription/phonetic_transcription_builder.dart'; import 'package:fluffychat/pangea/text_to_speech/tts_controller.dart'; import 'package:fluffychat/widgets/hover_builder.dart'; import 'package:fluffychat/widgets/matrix.dart'; @@ -43,79 +39,6 @@ class PhoneticTranscriptionWidget extends StatefulWidget { class _PhoneticTranscriptionWidgetState extends State { bool _isPlaying = false; - bool _isLoading = false; - Object? _error; - - String? _transcription; - - @override - void initState() { - super.initState(); - _fetchTranscription(); - } - - @override - void didUpdateWidget( - covariant PhoneticTranscriptionWidget oldWidget, - ) { - super.didUpdateWidget(oldWidget); - if (oldWidget.text != widget.text || - oldWidget.textLanguage != widget.textLanguage) { - _fetchTranscription(); - } - } - - Future _fetchTranscription() async { - try { - setState(() { - _isLoading = true; - _error = null; - _transcription = null; - }); - - if (MatrixState.pangeaController.userController.userL1 == null) { - ErrorHandler.logError( - e: Exception('User L1 is not set'), - data: { - 'text': widget.text, - 'textLanguageCode': widget.textLanguage.langCode, - }, - ); - _error = Exception('User L1 is not set'); - return; - } - final req = PhoneticTranscriptionRequest( - arc: LanguageArc( - l1: MatrixState.pangeaController.userController.userL1!, - l2: widget.textLanguage, - ), - content: PangeaTokenText.fromString(widget.text), - // arc can be omitted for default empty map - ); - final res = await PhoneticTranscriptionRepo.get(req); - _transcription = res.phoneticTranscriptionResult.phoneticTranscription - .first.phoneticL1Transcription.content; - } catch (e, s) { - _error = e; - if (e is! UnsubscribedException) { - ErrorHandler.logError( - e: e, - s: s, - data: { - 'text': widget.text, - 'textLanguageCode': widget.textLanguage.langCode, - }, - ); - } - } finally { - if (mounted) { - setState(() { - _isLoading = false; - widget.onTranscriptionFetched?.call(); - }); - } - } - } Future _handleAudioTap() async { if (_isPlaying) { @@ -156,14 +79,15 @@ class _PhoneticTranscriptionWidgetState link: MatrixState.pAnyState .layerLinkAndKey("phonetic-transcription-${widget.text}") .link, - child: Row( + child: PhoneticTranscriptionBuilder( key: MatrixState.pAnyState .layerLinkAndKey("phonetic-transcription-${widget.text}") .key, - mainAxisSize: MainAxisSize.min, - children: [ - if (_error != null) - _error is UnsubscribedException + textLanguage: widget.textLanguage, + text: widget.text, + builder: (context, controller) { + if (controller.isError) { + return controller.error is UnsubscribedException ? ErrorIndicator( message: L10n.of(context) .subscribeToUnlockTranscriptions, @@ -176,37 +100,44 @@ class _PhoneticTranscriptionWidgetState : ErrorIndicator( message: L10n.of(context).failedToFetchTranscription, - ) - else if (_isLoading || _transcription == null) - const SizedBox( + ); + } + + if (controller.isLoading || + controller.transcription == null) { + return const SizedBox( width: 16, height: 16, child: CircularProgressIndicator.adaptive(), - ) - else - Flexible( - child: Text( - _transcription!, - textScaler: TextScaler.noScaling, - style: widget.style ?? - Theme.of(context).textTheme.bodyMedium, + ); + } + + return Row( + spacing: 8.0, + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: Text( + controller.transcription!, + textScaler: TextScaler.noScaling, + style: widget.style ?? + Theme.of(context).textTheme.bodyMedium, + ), ), - ), - if (_transcription != null && _error == null) - const SizedBox(width: 8), - if (_transcription != null && _error == null) - Tooltip( - message: _isPlaying - ? L10n.of(context).stop - : L10n.of(context).playAudio, - child: Icon( - _isPlaying ? Icons.pause_outlined : Icons.volume_up, - size: widget.iconSize ?? 24, - color: widget.iconColor ?? - Theme.of(context).iconTheme.color, + Tooltip( + message: _isPlaying + ? L10n.of(context).stop + : L10n.of(context).playAudio, + child: Icon( + _isPlaying ? Icons.pause_outlined : Icons.volume_up, + size: widget.iconSize ?? 24, + color: widget.iconColor ?? + Theme.of(context).iconTheme.color, + ), ), - ), - ], + ], + ); + }, ), ), ), diff --git a/lib/pangea/practice_activities/emoji_activity_generator.dart b/lib/pangea/practice_activities/emoji_activity_generator.dart index 3354421c5..3cb5584bd 100644 --- a/lib/pangea/practice_activities/emoji_activity_generator.dart +++ b/lib/pangea/practice_activities/emoji_activity_generator.dart @@ -1,3 +1,5 @@ +import 'package:async/async.dart'; + import 'package:fluffychat/pangea/constructs/construct_form.dart'; import 'package:fluffychat/pangea/events/models/pangea_token_model.dart'; import 'package:fluffychat/pangea/lemmas/lemma_info_response.dart'; @@ -35,15 +37,20 @@ class EmojiActivityGenerator { } } - final List> lemmaInfoFutures = missingEmojis - .map((token) => token.vocabConstructID.getLemmaInfo()) - .toList(); + final List>> lemmaInfoFutures = + missingEmojis + .map((token) => token.vocabConstructID.getLemmaInfo()) + .toList(); - final List lemmaInfos = + final List> lemmaInfos = await Future.wait(lemmaInfoFutures); for (int i = 0; i < missingEmojis.length; i++) { - final e = lemmaInfos[i].emoji.firstWhere( + if (lemmaInfos[i].isError) { + throw lemmaInfos[i].asError!.error; + } + + final e = lemmaInfos[i].asValue!.value.emoji.firstWhere( (e) => !usedEmojis.contains(e), orElse: () => throw Exception( "Not enough unique emojis for tokens in message", diff --git a/lib/pangea/practice_activities/lemma_meaning_activity_generator.dart b/lib/pangea/practice_activities/lemma_meaning_activity_generator.dart index dfe8515d9..02d839940 100644 --- a/lib/pangea/practice_activities/lemma_meaning_activity_generator.dart +++ b/lib/pangea/practice_activities/lemma_meaning_activity_generator.dart @@ -1,26 +1,34 @@ import 'dart:async'; +import 'package:async/async.dart'; + import 'package:fluffychat/pangea/constructs/construct_form.dart'; import 'package:fluffychat/pangea/lemmas/lemma_info_response.dart'; import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart'; import 'package:fluffychat/pangea/practice_activities/message_activity_request.dart'; import 'package:fluffychat/pangea/practice_activities/practice_activity_model.dart'; import 'package:fluffychat/pangea/practice_activities/practice_match.dart'; +import 'package:fluffychat/widgets/future_loading_dialog.dart'; class LemmaMeaningActivityGenerator { static Future get( MessageActivityRequest req, ) async { - final List> lemmaInfoFutures = req.targetTokens + final List>> lemmaInfoFutures = req + .targetTokens .map((token) => token.vocabConstructID.getLemmaInfo()) .toList(); - final List lemmaInfos = + final List> lemmaInfos = await Future.wait(lemmaInfoFutures); + if (lemmaInfos.any((result) => result.isError)) { + throw lemmaInfos.firstWhere((result) => result.isError).error!; + } + final Map> matchInfo = Map.fromIterables( req.targetTokens.map((token) => token.vocabForm), - lemmaInfos.map((lemmaInfo) => [lemmaInfo.meaning]), + lemmaInfos.map((lemmaInfo) => [lemmaInfo.asValue!.value.meaning]), ); return MessageActivityResponse( diff --git a/lib/pangea/practice_activities/practice_generation_repo.dart b/lib/pangea/practice_activities/practice_generation_repo.dart index 008461d2e..423f4033a 100644 --- a/lib/pangea/practice_activities/practice_generation_repo.dart +++ b/lib/pangea/practice_activities/practice_generation_repo.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:developer'; +import 'dart:io'; import 'package:flutter/foundation.dart'; @@ -69,6 +70,8 @@ class PracticeRepo { _setCached(req, res); return Result.value(res.activity); + } on HttpException catch (e, s) { + return Result.error(e, s); } catch (e, s) { ErrorHandler.logError( e: e, diff --git a/lib/pangea/token_info_feedback/token_info_feedback_dialog.dart b/lib/pangea/token_info_feedback/token_info_feedback_dialog.dart index 0660185bf..dbe165a96 100644 --- a/lib/pangea/token_info_feedback/token_info_feedback_dialog.dart +++ b/lib/pangea/token_info_feedback/token_info_feedback_dialog.dart @@ -8,8 +8,8 @@ import 'package:fluffychat/pangea/events/models/tokens_event_content_model.dart' import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; import 'package:fluffychat/pangea/languages/language_arc_model.dart'; import 'package:fluffychat/pangea/languages/p_language_store.dart'; +import 'package:fluffychat/pangea/lemmas/lemma_info_repo.dart'; import 'package:fluffychat/pangea/lemmas/lemma_info_response.dart'; -import 'package:fluffychat/pangea/lemmas/user_set_lemma_info.dart'; import 'package:fluffychat/pangea/phonetic_transcription/phonetic_transcription_repo.dart'; import 'package:fluffychat/pangea/phonetic_transcription/phonetic_transcription_request.dart'; import 'package:fluffychat/pangea/phonetic_transcription/phonetic_transcription_response.dart'; @@ -115,19 +115,11 @@ class TokenInfoFeedbackDialog extends StatelessWidget { Future _updateLemmaInfo( PangeaToken token, LemmaInfoResponse response, - ) async { - final construct = token.vocabConstructID; - - final currentLemmaInfo = construct.userLemmaInfo; - final updatedLemmaInfo = UserSetLemmaInfo( - meaning: response.meaning, - emojis: response.emoji, - ); - - if (currentLemmaInfo != updatedLemmaInfo) { - await construct.setUserLemmaInfo(updatedLemmaInfo); - } - } + ) => + LemmaInfoRepo.set( + token.vocabConstructID.lemmaInfoRequest, + response, + ); Future _updatePhoneticTranscription( PhoneticTranscriptionResponse response, diff --git a/lib/pangea/token_info_feedback/token_info_feedback_request.dart b/lib/pangea/token_info_feedback/token_info_feedback_request.dart index 9caf6c4ea..815979493 100644 --- a/lib/pangea/token_info_feedback/token_info_feedback_request.dart +++ b/lib/pangea/token_info_feedback/token_info_feedback_request.dart @@ -8,8 +8,8 @@ class TokenInfoFeedbackRequestData { final String detectedLanguage; final List tokens; final int selectedToken; - final LemmaInfoResponse? lemmaInfo; - final String? phonetics; + final LemmaInfoResponse lemmaInfo; + final String phonetics; final String wordCardL1; TokenInfoFeedbackRequestData({ @@ -19,8 +19,8 @@ class TokenInfoFeedbackRequestData { required this.detectedLanguage, required this.tokens, required this.selectedToken, - this.lemmaInfo, - this.phonetics, + required this.lemmaInfo, + required this.phonetics, required this.wordCardL1, }); @@ -67,7 +67,7 @@ class TokenInfoFeedbackRequest { 'detected_language': data.detectedLanguage, 'tokens': data.tokens.map((token) => token.toJson()).toList(), 'selected_token': data.selectedToken, - 'lemma_info': data.lemmaInfo?.toJson(), + 'lemma_info': data.lemmaInfo.toJson(), 'phonetics': data.phonetics, 'user_feedback': userFeedback, 'word_card_l1': data.wordCardL1, diff --git a/lib/pangea/toolbar/word_card/lemma_meaning_display.dart b/lib/pangea/toolbar/word_card/lemma_meaning_display.dart new file mode 100644 index 000000000..aac4f1f8d --- /dev/null +++ b/lib/pangea/toolbar/word_card/lemma_meaning_display.dart @@ -0,0 +1,72 @@ +import 'package:flutter/material.dart'; + +import 'package:fluffychat/l10n/l10n.dart'; +import 'package:fluffychat/pangea/common/widgets/error_indicator.dart'; +import 'package:fluffychat/pangea/constructs/construct_identifier.dart'; +import 'package:fluffychat/pangea/lemmas/lemma_meaning_builder.dart'; + +class LemmaMeaningDisplay extends StatelessWidget { + final String langCode; + final ConstructIdentifier constructId; + final String text; + + const LemmaMeaningDisplay({ + super.key, + required this.langCode, + required this.constructId, + required this.text, + }); + + @override + Widget build(BuildContext context) { + return LemmaMeaningBuilder( + langCode: langCode, + constructId: constructId, + builder: (context, controller) { + if (controller.isError) { + return ErrorIndicator( + message: L10n.of(context).errorFetchingDefinition, + style: const TextStyle(fontSize: 14.0), + ); + } + + if (controller.isLoading || controller.lemmaInfo == null) { + return const CircularProgressIndicator.adaptive(); + } + + if (constructId.lemma.toLowerCase() == text.toLowerCase()) { + return Text( + controller.lemmaInfo!.meaning, + style: const TextStyle( + fontSize: 14.0, + ), + textAlign: TextAlign.center, + ); + } + + return RichText( + text: TextSpan( + style: DefaultTextStyle.of(context).style.copyWith( + fontSize: 14.0, + ), + children: [ + TextSpan( + text: constructId.lemma, + ), + const WidgetSpan( + child: SizedBox(width: 8.0), + ), + const TextSpan(text: ":"), + const WidgetSpan( + child: SizedBox(width: 8.0), + ), + TextSpan( + text: controller.lemmaInfo!.meaning, + ), + ], + ), + ); + }, + ); + } +} diff --git a/lib/pangea/toolbar/word_card/lemma_reaction_picker.dart b/lib/pangea/toolbar/word_card/lemma_reaction_picker.dart index b4d69b71c..00ba9ee27 100644 --- a/lib/pangea/toolbar/word_card/lemma_reaction_picker.dart +++ b/lib/pangea/toolbar/word_card/lemma_reaction_picker.dart @@ -4,21 +4,26 @@ import 'package:collection/collection.dart'; import 'package:matrix/matrix.dart'; import 'package:fluffychat/pangea/common/utils/error_handler.dart'; +import 'package:fluffychat/pangea/constructs/construct_identifier.dart'; +import 'package:fluffychat/pangea/lemmas/lemma_meaning_builder.dart'; import 'package:fluffychat/pangea/toolbar/reading_assistance/lemma_emoji_picker.dart'; class LemmaReactionPicker extends StatelessWidget { - final List emojis; - final bool loading; final Event? event; + final ConstructIdentifier construct; + final String langCode; const LemmaReactionPicker({ super.key, - required this.emojis, - required this.loading, - required this.event, + required this.construct, + required this.langCode, + this.event, }); - Future setEmoji(String emoji) async { + Future setEmoji( + String emoji, + List emojis, + ) async { if (event?.room.timeline == null) { throw Exception("Timeline is null in reaction picker"); } @@ -63,33 +68,44 @@ class LemmaReactionPicker extends StatelessWidget { @override Widget build(BuildContext context) { - final sentReactions = {}; - if (event?.room.timeline != null) { - sentReactions.addAll( - event! - .aggregatedEvents( - event!.room.timeline!, - RelationshipTypes.reaction, - ) - .where( - (event) => - event.senderId == event.room.client.userID && - event.type == 'm.reaction', - ) - .map( - (event) => event.content - .tryGetMap('m.relates_to') - ?.tryGet('key'), - ) - .whereType(), - ); - } + return LemmaMeaningBuilder( + langCode: langCode, + constructId: construct, + builder: (context, controller) { + final sentReactions = {}; + if (event?.room.timeline != null) { + sentReactions.addAll( + event! + .aggregatedEvents( + event!.room.timeline!, + RelationshipTypes.reaction, + ) + .where( + (event) => + event.senderId == event.room.client.userID && + event.type == 'm.reaction', + ) + .map( + (event) => event.content + .tryGetMap('m.relates_to') + ?.tryGet('key'), + ) + .whereType(), + ); + } - return LemmaEmojiPicker( - emojis: emojis, - onSelect: event?.room.timeline != null ? setEmoji : null, - disabled: (emoji) => sentReactions.contains(emoji), - loading: loading, + return LemmaEmojiPicker( + emojis: controller.lemmaInfo?.emoji ?? [], + onSelect: event?.room.timeline != null + ? (emoji) => setEmoji( + emoji, + controller.lemmaInfo?.emoji ?? [], + ) + : null, + disabled: (emoji) => sentReactions.contains(emoji), + loading: controller.isLoading, + ); + }, ); } } diff --git a/lib/pangea/toolbar/word_card/reading_assistance_content.dart b/lib/pangea/toolbar/word_card/reading_assistance_content.dart index b607b0d1d..71d2e0d97 100644 --- a/lib/pangea/toolbar/word_card/reading_assistance_content.dart +++ b/lib/pangea/toolbar/word_card/reading_assistance_content.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:matrix/matrix_api_lite/model/message_types.dart'; import 'package:fluffychat/config/themes.dart'; +import 'package:fluffychat/pangea/lemmas/lemma_info_response.dart'; import 'package:fluffychat/pangea/token_info_feedback/token_info_feedback_request.dart'; import 'package:fluffychat/pangea/toolbar/message_selection_overlay.dart'; import 'package:fluffychat/pangea/toolbar/word_card/word_zoom_widget.dart'; @@ -50,7 +51,7 @@ class ReadingAssistanceContent extends StatelessWidget { onClose: () => overlayController.updateSelectedSpan(null), langCode: overlayController.pangeaMessageEvent.messageDisplayLangCode, onDismissNewWordOverlay: () => overlayController.setState(() {}), - onFlagTokenInfo: () { + onFlagTokenInfo: (LemmaInfoResponse lemmaInfo, String phonetics) { if (selectedTokenIndex < 0) return; final requestData = TokenInfoFeedbackRequestData( userId: Matrix.of(context).client.userID!, @@ -61,6 +62,8 @@ class ReadingAssistanceContent extends StatelessWidget { tokens: tokens ?? [], selectedToken: selectedTokenIndex, wordCardL1: MatrixState.pangeaController.userController.userL1Code!, + lemmaInfo: lemmaInfo, + phonetics: phonetics, ); overlayController.widget.chatController.showTokenFeedbackDialog( requestData, diff --git a/lib/pangea/toolbar/word_card/token_feedback_button.dart b/lib/pangea/toolbar/word_card/token_feedback_button.dart new file mode 100644 index 000000000..dc2a6cff4 --- /dev/null +++ b/lib/pangea/toolbar/word_card/token_feedback_button.dart @@ -0,0 +1,62 @@ +import 'package:flutter/material.dart'; + +import 'package:fluffychat/l10n/l10n.dart'; +import 'package:fluffychat/pangea/constructs/construct_identifier.dart'; +import 'package:fluffychat/pangea/languages/language_model.dart'; +import 'package:fluffychat/pangea/lemmas/lemma_info_response.dart'; +import 'package:fluffychat/pangea/lemmas/lemma_meaning_builder.dart'; +import 'package:fluffychat/pangea/phonetic_transcription/phonetic_transcription_builder.dart'; + +class TokenFeedbackButton extends StatelessWidget { + final LanguageModel textLanguage; + final ConstructIdentifier constructId; + final String text; + + final Function(LemmaInfoResponse, String) onFlagTokenInfo; + + const TokenFeedbackButton({ + super.key, + required this.textLanguage, + required this.constructId, + required this.text, + required this.onFlagTokenInfo, + }); + + @override + Widget build(BuildContext context) { + return LemmaMeaningBuilder( + langCode: textLanguage.langCode, + constructId: constructId, + builder: (context, lemmaController) { + return PhoneticTranscriptionBuilder( + textLanguage: textLanguage, + text: text, + builder: (context, transcriptController) { + final enabled = (lemmaController.lemmaInfo != null || + lemmaController.isError) && + (transcriptController.transcription != null || + transcriptController.isError); + + final lemmaInfo = + lemmaController.lemmaInfo ?? LemmaInfoResponse.error; + + final transcript = transcriptController.transcription ?? 'ERROR'; + + return IconButton( + icon: const Icon(Icons.flag_outlined), + onPressed: enabled + ? () { + onFlagTokenInfo( + lemmaInfo, + transcript, + ); + } + : null, + tooltip: enabled ? L10n.of(context).reportWordIssueTooltip : null, + ); + }, + ); + }, + ); + } +} diff --git a/lib/pangea/toolbar/word_card/word_zoom_widget.dart b/lib/pangea/toolbar/word_card/word_zoom_widget.dart index d88a4e787..3bb8b0bb9 100644 --- a/lib/pangea/toolbar/word_card/word_zoom_widget.dart +++ b/lib/pangea/toolbar/word_card/word_zoom_widget.dart @@ -3,18 +3,18 @@ import 'package:flutter/material.dart'; import 'package:matrix/matrix.dart'; import 'package:fluffychat/config/app_config.dart'; -import 'package:fluffychat/l10n/l10n.dart'; -import 'package:fluffychat/pangea/common/widgets/error_indicator.dart'; import 'package:fluffychat/pangea/common/widgets/word_audio_button.dart'; import 'package:fluffychat/pangea/constructs/construct_identifier.dart'; import 'package:fluffychat/pangea/events/models/pangea_token_text_model.dart'; import 'package:fluffychat/pangea/languages/language_model.dart'; import 'package:fluffychat/pangea/languages/p_language_store.dart'; -import 'package:fluffychat/pangea/lemmas/lemma_meaning_builder.dart'; +import 'package:fluffychat/pangea/lemmas/lemma_info_response.dart'; import 'package:fluffychat/pangea/phonetic_transcription/phonetic_transcription_widget.dart'; import 'package:fluffychat/pangea/toolbar/reading_assistance/new_word_overlay.dart'; +import 'package:fluffychat/pangea/toolbar/word_card/lemma_meaning_display.dart'; import 'package:fluffychat/pangea/toolbar/word_card/lemma_reaction_picker.dart'; import 'package:fluffychat/pangea/toolbar/word_card/message_unsubscribed_card.dart'; +import 'package:fluffychat/pangea/toolbar/word_card/token_feedback_button.dart'; import 'package:fluffychat/widgets/matrix.dart'; class WordZoomWidget extends StatelessWidget { @@ -28,10 +28,7 @@ class WordZoomWidget extends StatelessWidget { final Event? event; final VoidCallback? onDismissNewWordOverlay; - final VoidCallback? onFlagTokenInfo; - - // final TokenInfoFeedbackRequestData? requestData; - // final PangeaMessageEvent? pangeaMessageEvent; + final Function(LemmaInfoResponse, String)? onFlagTokenInfo; const WordZoomWidget({ super.key, @@ -55,6 +52,8 @@ class WordZoomWidget extends StatelessWidget { final bool? subscribed = MatrixState.pangeaController.subscriptionController.isSubscribed; final overlayColor = Theme.of(context).scaffoldBackgroundColor; + final showTranscript = + MatrixState.pangeaController.userController.showTranscription; final Widget content = subscribed != null && !subscribed ? const MessageUnsubscribedCard() @@ -106,11 +105,14 @@ class WordZoomWidget extends StatelessWidget { ), ), onFlagTokenInfo != null - ? IconButton( - icon: const Icon(Icons.flag_outlined), - onPressed: onFlagTokenInfo, - tooltip: - L10n.of(context).reportWordIssueTooltip, + ? TokenFeedbackButton( + textLanguage: PLanguageStore.byLangCode( + langCode, + ) ?? + LanguageModel.unknown, + constructId: construct, + text: token.content, + onFlagTokenInfo: onFlagTokenInfo!, ) : const SizedBox( width: 40.0, @@ -118,17 +120,12 @@ class WordZoomWidget extends StatelessWidget { ), ], ), - LemmaMeaningBuilder( - langCode: langCode, - constructId: construct, - builder: (context, controller) { - return Column( - spacing: 12.0, - mainAxisSize: MainAxisSize.min, - children: [ - if (MatrixState.pangeaController.userController - .showTranscription) - PhoneticTranscriptionWidget( + Column( + spacing: 12.0, + mainAxisSize: MainAxisSize.min, + children: [ + showTranscript + ? PhoneticTranscriptionWidget( text: token.content, textLanguage: PLanguageStore.byLangCode( langCode, @@ -137,62 +134,23 @@ class WordZoomWidget extends StatelessWidget { style: const TextStyle(fontSize: 14.0), iconSize: 24.0, ) - else - WordAudioButton( + : WordAudioButton( text: token.content, uniqueID: "lemma-content-${token.content}", langCode: langCode, iconSize: 24.0, ), - LemmaReactionPicker( - emojis: controller.lemmaInfo?.emoji ?? [], - loading: controller.isLoading, - event: event, - ), - if (controller.error != null) - ErrorIndicator( - message: L10n.of(context) - .errorFetchingDefinition, - style: const TextStyle(fontSize: 14.0), - ) - else if (controller.isLoading || - controller.lemmaInfo == null) - const CircularProgressIndicator.adaptive() - else - construct.lemma.toLowerCase() == - token.content.toLowerCase() - ? Text( - controller.lemmaInfo!.meaning, - style: - const TextStyle(fontSize: 14.0), - textAlign: TextAlign.center, - ) - : RichText( - text: TextSpan( - style: DefaultTextStyle.of(context) - .style - .copyWith( - fontSize: 14.0, - ), - children: [ - TextSpan(text: construct.lemma), - const WidgetSpan( - child: SizedBox(width: 8.0), - ), - const TextSpan(text: ":"), - const WidgetSpan( - child: SizedBox(width: 8.0), - ), - TextSpan( - text: controller - .lemmaInfo!.meaning, - ), - ], - ), - ), - ], - ); - }, + LemmaReactionPicker( + construct: construct, + langCode: langCode, + event: event, + ), + LemmaMeaningDisplay( + langCode: langCode, + constructId: construct, + text: token.content, + ), + ], ), ], ),