Merge pull request #4824 from pangeachat/4819-tokenfeedback-request-doesnt-update-lemma_info
fix: always pass lemma info and phonetic transcription in token feedb…
This commit is contained in:
commit
be0ed2fa1a
21 changed files with 748 additions and 341 deletions
|
|
@ -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<T> {
|
|||
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,
|
||||
|
|
|
|||
|
|
@ -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<LemmaInfoResponse> getLemmaInfo() => LemmaInfoRepo.get(
|
||||
_lemmaInfoRequest,
|
||||
Future<Result<LemmaInfoResponse>> getLemmaInfo() => LemmaInfoRepo.get(
|
||||
MatrixState.pangeaController.userController.accessToken,
|
||||
lemmaInfoRequest,
|
||||
);
|
||||
|
||||
List<String> get userSetEmoji => userLemmaInfo.emojis ?? [];
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Result<LemmaInfoResponse>> 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<String, _LemmaInfoCacheItem> _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<Result<LemmaInfoResponse>> 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<LemmaInfoResponse> get(LemmaInfoRequest request) async {
|
||||
final cached = getCached(request);
|
||||
if (cached != null) return cached;
|
||||
static Future<void> 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<Result<LemmaInfoResponse>> _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<LemmaInfoResponse> _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<Result<LemmaInfoResponse>>? _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<void> writeToDisk(
|
||||
LemmaInfoRequest request,
|
||||
Future<Result<LemmaInfoResponse>> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,12 +3,10 @@ import 'package:fluffychat/pangea/events/models/content_feedback.dart';
|
|||
class LemmaInfoResponse implements JsonSerializable {
|
||||
final List<String> emoji;
|
||||
final String meaning;
|
||||
DateTime? expireAt;
|
||||
|
||||
LemmaInfoResponse({
|
||||
required this.emoji,
|
||||
required this.meaning,
|
||||
this.expireAt,
|
||||
});
|
||||
|
||||
factory LemmaInfoResponse.fromJson(Map<String, dynamic> 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<dynamic>).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<String, dynamic> toJson() {
|
||||
return {
|
||||
'emoji': emoji,
|
||||
'meaning': meaning,
|
||||
'expireAt': expireAt?.toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<LemmaInfoResponse> {
|
||||
final LemmaInfoRequest request;
|
||||
_LemmaMeaningLoader(this.request) : super();
|
||||
|
||||
@override
|
||||
Future<LemmaInfoResponse> 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<LemmaMeaningBuilder> {
|
||||
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<LemmaMeaningBuilder> {
|
|||
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<LemmaMeaningBuilder> {
|
|||
LanguageKeys.defaultLanguage,
|
||||
);
|
||||
|
||||
Future<void> _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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<String> {
|
||||
final PhoneticTranscriptionRequest request;
|
||||
_TranscriptLoader(this.request) : super();
|
||||
|
||||
@override
|
||||
Future<String> 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<PhoneticTranscriptionBuilder> {
|
||||
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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Result<PhoneticTranscriptionResponse>> resultFuture;
|
||||
final DateTime timestamp;
|
||||
|
||||
const _PhoneticTranscriptionCacheItem({
|
||||
required this.resultFuture,
|
||||
required this.timestamp,
|
||||
});
|
||||
}
|
||||
|
||||
class PhoneticTranscriptionRepo {
|
||||
// In-memory cache
|
||||
static final Map<String, _PhoneticTranscriptionCacheItem> _cache = {};
|
||||
static const Duration _cacheDuration = Duration(minutes: 10);
|
||||
|
||||
// Persistent storage
|
||||
static final GetStorage _storage =
|
||||
GetStorage('phonetic_transcription_storage');
|
||||
|
||||
static Future<Result<PhoneticTranscriptionResponse>> 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<void> 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<Result<PhoneticTranscriptionResponse>> _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<PhoneticTranscriptionResponse> _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<PhoneticTranscriptionResponse> get(
|
||||
static Future<Result<PhoneticTranscriptionResponse>>? _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<void> writeToDisk(
|
||||
PhoneticTranscriptionRequest request,
|
||||
Future<Result<PhoneticTranscriptionResponse>> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -102,14 +102,12 @@ class PhoneticTranscriptionResponse {
|
|||
final Map<String, dynamic>
|
||||
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<String, dynamic> json) {
|
||||
|
|
@ -121,9 +119,6 @@ class PhoneticTranscriptionResponse {
|
|||
phoneticTranscriptionResult: PhoneticTranscription.fromJson(
|
||||
json['phonetic_transcription_result'] as Map<String, dynamic>,
|
||||
),
|
||||
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(),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<PhoneticTranscriptionWidget> {
|
||||
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<void> _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<void> _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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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<Future<LemmaInfoResponse>> lemmaInfoFutures = missingEmojis
|
||||
.map((token) => token.vocabConstructID.getLemmaInfo())
|
||||
.toList();
|
||||
final List<Future<Result<LemmaInfoResponse>>> lemmaInfoFutures =
|
||||
missingEmojis
|
||||
.map((token) => token.vocabConstructID.getLemmaInfo())
|
||||
.toList();
|
||||
|
||||
final List<LemmaInfoResponse> lemmaInfos =
|
||||
final List<Result<LemmaInfoResponse>> 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",
|
||||
|
|
|
|||
|
|
@ -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<MessageActivityResponse> get(
|
||||
MessageActivityRequest req,
|
||||
) async {
|
||||
final List<Future<LemmaInfoResponse>> lemmaInfoFutures = req.targetTokens
|
||||
final List<Future<Result<LemmaInfoResponse>>> lemmaInfoFutures = req
|
||||
.targetTokens
|
||||
.map((token) => token.vocabConstructID.getLemmaInfo())
|
||||
.toList();
|
||||
|
||||
final List<LemmaInfoResponse> lemmaInfos =
|
||||
final List<Result<LemmaInfoResponse>> lemmaInfos =
|
||||
await Future.wait(lemmaInfoFutures);
|
||||
|
||||
if (lemmaInfos.any((result) => result.isError)) {
|
||||
throw lemmaInfos.firstWhere((result) => result.isError).error!;
|
||||
}
|
||||
|
||||
final Map<ConstructForm, List<String>> matchInfo = Map.fromIterables(
|
||||
req.targetTokens.map((token) => token.vocabForm),
|
||||
lemmaInfos.map((lemmaInfo) => [lemmaInfo.meaning]),
|
||||
lemmaInfos.map((lemmaInfo) => [lemmaInfo.asValue!.value.meaning]),
|
||||
);
|
||||
|
||||
return MessageActivityResponse(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<void> _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<void> _updatePhoneticTranscription(
|
||||
PhoneticTranscriptionResponse response,
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@ class TokenInfoFeedbackRequestData {
|
|||
final String detectedLanguage;
|
||||
final List<PangeaToken> 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,
|
||||
|
|
|
|||
72
lib/pangea/toolbar/word_card/lemma_meaning_display.dart
Normal file
72
lib/pangea/toolbar/word_card/lemma_meaning_display.dart
Normal file
|
|
@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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<String> 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<void> setEmoji(String emoji) async {
|
||||
Future<void> setEmoji(
|
||||
String emoji,
|
||||
List<String> 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 = <String>{};
|
||||
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<String, Object?>('m.relates_to')
|
||||
?.tryGet<String>('key'),
|
||||
)
|
||||
.whereType<String>(),
|
||||
);
|
||||
}
|
||||
return LemmaMeaningBuilder(
|
||||
langCode: langCode,
|
||||
constructId: construct,
|
||||
builder: (context, controller) {
|
||||
final sentReactions = <String>{};
|
||||
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<String, Object?>('m.relates_to')
|
||||
?.tryGet<String>('key'),
|
||||
)
|
||||
.whereType<String>(),
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
62
lib/pangea/toolbar/word_card/token_feedback_button.dart
Normal file
62
lib/pangea/toolbar/word_card/token_feedback_button.dart
Normal file
|
|
@ -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,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue