1380 content challenges (#1391)
* use gold consistently for positive xp color * fix: dont point to local choreopull/1593/head
parent
a26895ad81
commit
8084cc24cc
@ -0,0 +1,25 @@
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:shimmer/shimmer.dart';
|
||||
|
||||
class TextLoadingShimmer extends StatelessWidget {
|
||||
final double width;
|
||||
const TextLoadingShimmer({
|
||||
super.key,
|
||||
this.width = 140.0,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Shimmer.fromColors(
|
||||
baseColor: Colors.transparent, // Base color of the shimmer effect
|
||||
// for higlight, use white with 50 opacity
|
||||
highlightColor: AppConfig.primaryColor.withAlpha(70),
|
||||
child: Container(
|
||||
height: AppConfig.messageFontSize * AppConfig.fontSizeFactor,
|
||||
width: width, // Width of the rectangle
|
||||
color: AppConfig.primaryColor, // Background color of the rectangle
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,31 @@
|
||||
abstract class JsonSerializable {
|
||||
Map<String, dynamic> toJson();
|
||||
factory JsonSerializable.fromJson(Map<String, dynamic> json) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
}
|
||||
|
||||
class ContentFeedback<T extends JsonSerializable> {
|
||||
final JsonSerializable content;
|
||||
final String feedback;
|
||||
|
||||
ContentFeedback(this.content, this.feedback);
|
||||
|
||||
toJson() {
|
||||
return {
|
||||
'content': content.toJson(),
|
||||
'feedback': feedback,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is ContentFeedback &&
|
||||
runtimeType == other.runtimeType &&
|
||||
content == other.content &&
|
||||
feedback == other.feedback;
|
||||
|
||||
@override
|
||||
int get hashCode => content.hashCode ^ feedback.hashCode;
|
||||
}
|
||||
@ -1,175 +0,0 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:http/http.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/config/environment.dart';
|
||||
import 'package:fluffychat/pangea/models/lemma.dart';
|
||||
import 'package:fluffychat/pangea/network/requests.dart';
|
||||
import 'package:fluffychat/pangea/network/urls.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
class LemmaDefinitionRequest {
|
||||
final Lemma _lemma;
|
||||
final String partOfSpeech;
|
||||
final String lemmaLang;
|
||||
final String userL1;
|
||||
|
||||
LemmaDefinitionRequest({
|
||||
required this.partOfSpeech,
|
||||
required this.lemmaLang,
|
||||
required this.userL1,
|
||||
required Lemma lemma,
|
||||
}) : _lemma = lemma;
|
||||
|
||||
String get lemma {
|
||||
if (_lemma.text.isNotEmpty) {
|
||||
return _lemma.text;
|
||||
}
|
||||
ErrorHandler.logError(
|
||||
e: "Found lemma with empty text",
|
||||
data: {
|
||||
'lemma': _lemma,
|
||||
'part_of_speech': partOfSpeech,
|
||||
'lemma_lang': lemmaLang,
|
||||
'user_l1': userL1,
|
||||
},
|
||||
);
|
||||
return _lemma.form;
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'lemma': lemma,
|
||||
'part_of_speech': partOfSpeech,
|
||||
'lemma_lang': lemmaLang,
|
||||
'user_l1': userL1,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is LemmaDefinitionRequest &&
|
||||
runtimeType == other.runtimeType &&
|
||||
lemma == other.lemma &&
|
||||
partOfSpeech == other.partOfSpeech &&
|
||||
lemmaLang == other.lemmaLang &&
|
||||
userL1 == other.userL1;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
lemma.hashCode ^
|
||||
partOfSpeech.hashCode ^
|
||||
lemmaLang.hashCode ^
|
||||
userL1.hashCode;
|
||||
}
|
||||
|
||||
class LemmaDefinitionResponse {
|
||||
final List<String> emoji;
|
||||
final String meaning;
|
||||
|
||||
LemmaDefinitionResponse({
|
||||
required this.emoji,
|
||||
required this.meaning,
|
||||
});
|
||||
|
||||
factory LemmaDefinitionResponse.fromJson(Map<String, dynamic> json) {
|
||||
return LemmaDefinitionResponse(
|
||||
emoji: (json['emoji'] as List<dynamic>).map((e) => e as String).toList(),
|
||||
meaning: json['meaning'] as String,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'emoji': emoji,
|
||||
'meaning': meaning,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is LemmaDefinitionResponse &&
|
||||
runtimeType == other.runtimeType &&
|
||||
emoji.length == other.emoji.length &&
|
||||
emoji.every((element) => other.emoji.contains(element)) &&
|
||||
meaning == other.meaning;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
emoji.fold(0, (prev, element) => prev ^ element.hashCode) ^
|
||||
meaning.hashCode;
|
||||
}
|
||||
|
||||
class LemmaDictionaryRepo {
|
||||
// In-memory cache with timestamps
|
||||
static final Map<LemmaDefinitionRequest, LemmaDefinitionResponse> _cache = {};
|
||||
static final Map<LemmaDefinitionRequest, DateTime> _cacheTimestamps = {};
|
||||
|
||||
static const Duration _cacheDuration = Duration(days: 2);
|
||||
|
||||
static Future<LemmaDefinitionResponse> get(
|
||||
LemmaDefinitionRequest request,
|
||||
) async {
|
||||
_clearExpiredEntries();
|
||||
|
||||
// Check the cache first
|
||||
if (_cache.containsKey(request)) {
|
||||
return _cache[request]!;
|
||||
}
|
||||
|
||||
final Requests req = Requests(
|
||||
choreoApiKey: Environment.choreoApiKey,
|
||||
accessToken: MatrixState.pangeaController.userController.accessToken,
|
||||
);
|
||||
|
||||
final requestBody = request.toJson();
|
||||
final Response res = await req.post(
|
||||
url: PApiUrls.lemmaDictionary,
|
||||
body: requestBody,
|
||||
);
|
||||
|
||||
final decodedBody = jsonDecode(utf8.decode(res.bodyBytes));
|
||||
final response = LemmaDefinitionResponse.fromJson(decodedBody);
|
||||
|
||||
// Store the response and timestamp in the cache
|
||||
_cache[request] = response;
|
||||
_cacheTimestamps[request] = DateTime.now();
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/// From the cache, get a random set of cached definitions that are not for a specific lemma
|
||||
static List<String> getDistractorDefinitions(
|
||||
String lemma,
|
||||
int count,
|
||||
) {
|
||||
_clearExpiredEntries();
|
||||
|
||||
final List<String> definitions = [];
|
||||
for (final entry in _cache.entries) {
|
||||
if (entry.key.lemma != lemma) {
|
||||
definitions.add(entry.value.meaning);
|
||||
}
|
||||
}
|
||||
|
||||
definitions.shuffle();
|
||||
|
||||
return definitions.take(count).toList();
|
||||
}
|
||||
|
||||
static void _clearExpiredEntries() {
|
||||
final now = DateTime.now();
|
||||
final expiredKeys = _cacheTimestamps.entries
|
||||
.where((entry) => now.difference(entry.value) > _cacheDuration)
|
||||
.map((entry) => entry.key)
|
||||
.toList();
|
||||
|
||||
for (final key in expiredKeys) {
|
||||
_cache.remove(key);
|
||||
_cacheTimestamps.remove(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,107 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:fluffychat/pangea/models/content_feedback.dart';
|
||||
import 'package:fluffychat/pangea/network/urls.dart';
|
||||
import 'package:fluffychat/pangea/repo/lemma_info/lemma_info_request.dart';
|
||||
import 'package:fluffychat/pangea/repo/lemma_info/lemma_info_response.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:http/http.dart';
|
||||
|
||||
import '../../config/environment.dart';
|
||||
import '../../network/requests.dart';
|
||||
|
||||
class LemmaInfoRepo {
|
||||
// In-memory cache with timestamps
|
||||
static final Map<LemmaInfoRequest, LemmaInfoResponse> _cache = {};
|
||||
static final Map<LemmaInfoRequest, DateTime> _cacheTimestamps = {};
|
||||
|
||||
static const Duration _cacheDuration = Duration(days: 2);
|
||||
|
||||
static Future<LemmaInfoResponse> get(
|
||||
LemmaInfoRequest request, [
|
||||
String? feedback,
|
||||
]) async {
|
||||
_clearExpiredEntries();
|
||||
|
||||
if (_cache.containsKey(request)) {
|
||||
final cached = _cache[request]!;
|
||||
|
||||
if (feedback == null) {
|
||||
// in this case, we just return the cached response
|
||||
return cached;
|
||||
} else {
|
||||
// we're adding this within the service to avoid needing to have the widgets
|
||||
// save state including the bad response
|
||||
request.feedback = ContentFeedback(
|
||||
cached,
|
||||
feedback,
|
||||
);
|
||||
}
|
||||
} else if (feedback != null) {
|
||||
// the cache should have the request in order for the user to provide feedback
|
||||
// this would be a strange situation and indicate some error in our logic
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(
|
||||
m: 'Feedback provided for a non-cached request',
|
||||
data: request.toJson(),
|
||||
);
|
||||
} else {
|
||||
debugPrint('No cached response for lemma ${request.lemma}');
|
||||
}
|
||||
|
||||
final Requests req = Requests(
|
||||
choreoApiKey: Environment.choreoApiKey,
|
||||
accessToken: MatrixState.pangeaController.userController.accessToken,
|
||||
);
|
||||
|
||||
final requestBody = request.toJson();
|
||||
final Response res = await req.post(
|
||||
url: PApiUrls.lemmaDictionary,
|
||||
body: requestBody,
|
||||
);
|
||||
|
||||
final decodedBody = jsonDecode(utf8.decode(res.bodyBytes));
|
||||
final response = LemmaInfoResponse.fromJson(decodedBody);
|
||||
|
||||
// Store the response and timestamp in the cache
|
||||
_cache[request] = response;
|
||||
_cacheTimestamps[request] = DateTime.now();
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/// From the cache, get a random set of cached definitions that are not for a specific lemma
|
||||
static List<String> getDistractorDefinitions(
|
||||
String lemma,
|
||||
int count,
|
||||
) {
|
||||
_clearExpiredEntries();
|
||||
|
||||
final Set<String> definitions = {};
|
||||
for (final entry in _cache.entries) {
|
||||
if (entry.key.lemma != lemma) {
|
||||
definitions.add(entry.value.meaning);
|
||||
}
|
||||
}
|
||||
|
||||
definitions.toList().shuffle();
|
||||
|
||||
return definitions.take(count).toList();
|
||||
}
|
||||
|
||||
static void _clearExpiredEntries() {
|
||||
final now = DateTime.now();
|
||||
final expiredKeys = _cacheTimestamps.entries
|
||||
.where((entry) => now.difference(entry.value) > _cacheDuration)
|
||||
.map((entry) => entry.key)
|
||||
.toList();
|
||||
|
||||
for (final key in expiredKeys) {
|
||||
_cache.remove(key);
|
||||
_cacheTimestamps.remove(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,43 @@
|
||||
import 'package:fluffychat/pangea/models/content_feedback.dart';
|
||||
import 'package:fluffychat/pangea/repo/lemma_info/lemma_info_response.dart';
|
||||
|
||||
class LemmaInfoRequest {
|
||||
final String lemma;
|
||||
final String partOfSpeech;
|
||||
final String lemmaLang;
|
||||
final String userL1;
|
||||
|
||||
ContentFeedback<LemmaInfoResponse>? feedback;
|
||||
|
||||
LemmaInfoRequest({
|
||||
required String partOfSpeech,
|
||||
required String lemmaLang,
|
||||
required this.userL1,
|
||||
required this.lemma,
|
||||
this.feedback,
|
||||
}) : partOfSpeech = partOfSpeech.toLowerCase(),
|
||||
lemmaLang = lemmaLang.toLowerCase();
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'lemma': lemma,
|
||||
'part_of_speech': partOfSpeech,
|
||||
'lemma_lang': lemmaLang,
|
||||
'user_l1': userL1,
|
||||
'feedback': feedback?.toJson(),
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is LemmaInfoRequest &&
|
||||
runtimeType == other.runtimeType &&
|
||||
lemma == other.lemma &&
|
||||
partOfSpeech == other.partOfSpeech &&
|
||||
feedback == other.feedback;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
lemma.hashCode ^ partOfSpeech.hashCode ^ feedback.hashCode;
|
||||
}
|
||||
@ -0,0 +1,40 @@
|
||||
import 'package:fluffychat/pangea/models/content_feedback.dart';
|
||||
|
||||
class LemmaInfoResponse implements JsonSerializable {
|
||||
final List<String> emoji;
|
||||
final String meaning;
|
||||
|
||||
LemmaInfoResponse({
|
||||
required this.emoji,
|
||||
required this.meaning,
|
||||
});
|
||||
|
||||
factory LemmaInfoResponse.fromJson(Map<String, dynamic> json) {
|
||||
return LemmaInfoResponse(
|
||||
emoji: (json['emoji'] as List<dynamic>).map((e) => e as String).toList(),
|
||||
meaning: json['meaning'] as String,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'emoji': emoji,
|
||||
'meaning': meaning,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is LemmaInfoResponse &&
|
||||
runtimeType == other.runtimeType &&
|
||||
emoji.length == other.emoji.length &&
|
||||
emoji.every((element) => other.emoji.contains(element)) &&
|
||||
meaning == other.meaning;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
emoji.fold(0, (prev, element) => prev ^ element.hashCode) ^
|
||||
meaning.hashCode;
|
||||
}
|
||||
@ -1,62 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/constants/language_constants.dart';
|
||||
import 'package:fluffychat/pangea/models/pangea_token_model.dart';
|
||||
import 'package:fluffychat/pangea/repo/lemma_definition_repo.dart';
|
||||
import 'package:fluffychat/pangea/widgets/igc/card_error_widget.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
class ContextualTranslationWidget extends StatelessWidget {
|
||||
final PangeaToken token;
|
||||
final String langCode;
|
||||
|
||||
const ContextualTranslationWidget({
|
||||
super.key,
|
||||
required this.token,
|
||||
required this.langCode,
|
||||
});
|
||||
|
||||
Future<String> _fetchDefinition() async {
|
||||
final LemmaDefinitionRequest lemmaDefReq = LemmaDefinitionRequest(
|
||||
lemma: token.lemma,
|
||||
partOfSpeech: token.pos,
|
||||
|
||||
/// This assumes that the user's L2 is the language of the lemma
|
||||
lemmaLang: langCode,
|
||||
userL1:
|
||||
MatrixState.pangeaController.languageController.userL1?.langCode ??
|
||||
LanguageKeys.defaultLanguage,
|
||||
);
|
||||
|
||||
final res = await LemmaDictionaryRepo.get(lemmaDefReq);
|
||||
return res.meaning;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FutureBuilder(
|
||||
future: _fetchDefinition(),
|
||||
builder: (context, snapshot) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: snapshot.connectionState != ConnectionState.done
|
||||
? const CircularProgressIndicator()
|
||||
: snapshot.hasError
|
||||
? CardErrorWidget(
|
||||
error: L10n.of(context).oopsSomethingWentWrong,
|
||||
padding: 0,
|
||||
maxWidth: 500,
|
||||
)
|
||||
: Text(
|
||||
snapshot.data ?? "...",
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,71 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/constants/language_constants.dart';
|
||||
import 'package:fluffychat/pangea/models/pangea_token_model.dart';
|
||||
import 'package:fluffychat/pangea/repo/lemma_definition_repo.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
class LemmaDefinitionWidget extends StatefulWidget {
|
||||
final PangeaToken token;
|
||||
final String tokenLang;
|
||||
final VoidCallback onPressed;
|
||||
|
||||
const LemmaDefinitionWidget({
|
||||
super.key,
|
||||
required this.token,
|
||||
required this.tokenLang,
|
||||
required this.onPressed,
|
||||
});
|
||||
|
||||
@override
|
||||
LemmaDefinitionWidgetState createState() => LemmaDefinitionWidgetState();
|
||||
}
|
||||
|
||||
class LemmaDefinitionWidgetState extends State<LemmaDefinitionWidget> {
|
||||
late Future<String> _definition;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_definition = _fetchDefinition();
|
||||
}
|
||||
|
||||
Future<String> _fetchDefinition() async {
|
||||
if (widget.token.shouldDoPosActivity) {
|
||||
return '?';
|
||||
} else {
|
||||
final res = await LemmaDictionaryRepo.get(
|
||||
LemmaDefinitionRequest(
|
||||
lemma: widget.token.lemma,
|
||||
partOfSpeech: widget.token.pos,
|
||||
lemmaLang: widget.tokenLang,
|
||||
userL1: MatrixState
|
||||
.pangeaController.languageController.userL1?.langCode ??
|
||||
LanguageKeys.defaultLanguage,
|
||||
),
|
||||
);
|
||||
return res.meaning;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FutureBuilder<String>(
|
||||
future: _definition,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
return const CircularProgressIndicator();
|
||||
} else if (snapshot.hasError) {
|
||||
// TODO better error widget
|
||||
return Text('Error: ${snapshot.error}');
|
||||
} else {
|
||||
return ActionChip(
|
||||
avatar: const Icon(Icons.book),
|
||||
label: Text(snapshot.data ?? 'No definition found'),
|
||||
onPressed: widget.onPressed,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,97 @@
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:fluffychat/pangea/choreographer/widgets/text_loading_shimmer.dart';
|
||||
import 'package:fluffychat/pangea/constants/language_constants.dart';
|
||||
import 'package:fluffychat/pangea/repo/lemma_info/lemma_info_repo.dart';
|
||||
import 'package:fluffychat/pangea/repo/lemma_info/lemma_info_request.dart';
|
||||
import 'package:fluffychat/utils/feedback_dialog.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class LemmaMeaningWidget extends StatefulWidget {
|
||||
final String lemma;
|
||||
final String pos;
|
||||
final String langCode;
|
||||
|
||||
const LemmaMeaningWidget({
|
||||
super.key,
|
||||
required this.lemma,
|
||||
required this.pos,
|
||||
required this.langCode,
|
||||
});
|
||||
|
||||
@override
|
||||
_LemmaMeaningWidgetState createState() => _LemmaMeaningWidgetState();
|
||||
}
|
||||
|
||||
class _LemmaMeaningWidgetState extends State<LemmaMeaningWidget> {
|
||||
late Future<String> _definitionFuture;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_definitionFuture = _fetchDefinition();
|
||||
}
|
||||
|
||||
Future<String> _fetchDefinition([String? feedback]) async {
|
||||
final LemmaInfoRequest lemmaDefReq = LemmaInfoRequest(
|
||||
lemma: widget.lemma,
|
||||
partOfSpeech: widget.pos,
|
||||
|
||||
/// This assumes that the user's L2 is the language of the lemma
|
||||
lemmaLang: widget.langCode,
|
||||
userL1:
|
||||
MatrixState.pangeaController.languageController.userL1?.langCode ??
|
||||
LanguageKeys.defaultLanguage,
|
||||
);
|
||||
|
||||
final res = await LemmaInfoRepo.get(lemmaDefReq, feedback);
|
||||
return res.meaning;
|
||||
}
|
||||
|
||||
void _showFeedbackDialog(String offendingContentString) async {
|
||||
// setState(() {
|
||||
// _definitionFuture = _fetchDefinition(offendingContentString);
|
||||
// });
|
||||
await showFeedbackDialog(
|
||||
context,
|
||||
Text(offendingContentString),
|
||||
(feedback) async {
|
||||
setState(() {
|
||||
_definitionFuture = _fetchDefinition(feedback);
|
||||
});
|
||||
return;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FutureBuilder(
|
||||
future: _definitionFuture,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState != ConnectionState.done) {
|
||||
return const TextLoadingShimmer();
|
||||
}
|
||||
|
||||
if (snapshot.hasError) {
|
||||
debugger(when: kDebugMode);
|
||||
return Text(
|
||||
snapshot.error.toString(),
|
||||
textAlign: TextAlign.center,
|
||||
);
|
||||
}
|
||||
|
||||
return GestureDetector(
|
||||
onLongPress: () => _showFeedbackDialog(snapshot.data as String),
|
||||
onDoubleTap: () => _showFeedbackDialog(snapshot.data as String),
|
||||
child: Text(
|
||||
snapshot.data as String,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,74 @@
|
||||
import 'package:fluffychat/pangea/controllers/message_analytics_controller.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/toolbar_content_loading_indicator.dart';
|
||||
import 'package:fluffychat/pangea/widgets/practice_activity/practice_activity_card.dart';
|
||||
import 'package:fluffychat/pangea/widgets/word_zoom/lemma_meaning_widget.dart';
|
||||
import 'package:fluffychat/pangea/widgets/word_zoom/word_zoom_widget.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class WordZoomCenterWidget extends StatelessWidget {
|
||||
final WordZoomSelection? selectionType;
|
||||
final String? selectedMorphFeature;
|
||||
final bool shouldDoActivity;
|
||||
final bool locked;
|
||||
final WordZoomWidgetState wordDetailsController;
|
||||
|
||||
const WordZoomCenterWidget({
|
||||
required this.selectionType,
|
||||
required this.selectedMorphFeature,
|
||||
required this.shouldDoActivity,
|
||||
required this.locked,
|
||||
required this.wordDetailsController,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (selectionType == null) {
|
||||
return const ToolbarContentLoadingIndicator();
|
||||
}
|
||||
|
||||
if (shouldDoActivity || locked) {
|
||||
return PracticeActivityCard(
|
||||
pangeaMessageEvent: wordDetailsController.widget.messageEvent,
|
||||
targetTokensAndActivityType: TargetTokensAndActivityType(
|
||||
tokens: [wordDetailsController.widget.token],
|
||||
activityType: selectionType!.activityType,
|
||||
),
|
||||
overlayController: wordDetailsController.widget.overlayController,
|
||||
morphFeature: selectedMorphFeature,
|
||||
wordDetailsController: wordDetailsController,
|
||||
);
|
||||
}
|
||||
|
||||
if (selectionType == WordZoomSelection.meaning) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: LemmaMeaningWidget(
|
||||
lemma: wordDetailsController.widget.token.lemma.text.isNotEmpty
|
||||
? wordDetailsController.widget.token.lemma.text
|
||||
: wordDetailsController.widget.token.lemma.form,
|
||||
pos: wordDetailsController.widget.token.pos,
|
||||
langCode: wordDetailsController
|
||||
.widget.messageEvent.messageDisplayLangCode,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
ActivityAnswerWidget(
|
||||
token: wordDetailsController.widget.token,
|
||||
selectionType: selectionType!,
|
||||
selectedMorphFeature: selectedMorphFeature,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,76 @@
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
import '../pangea/widgets/common/bot_face_svg.dart';
|
||||
|
||||
Future<dynamic> showFeedbackDialog(
|
||||
BuildContext context,
|
||||
Widget offendingContent,
|
||||
void Function(String) submitFeedback,
|
||||
) {
|
||||
final TextEditingController feedbackController = TextEditingController();
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text(
|
||||
L10n.of(context).reportContentIssueTitle,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
content: SingleChildScrollView(
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(maxWidth: 300),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const BotFace(
|
||||
width: 60,
|
||||
expression: BotExpression.addled,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(L10n.of(context).reportContentIssueDescription),
|
||||
const SizedBox(height: 10),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
border: Border.all(
|
||||
color: AppConfig.warning,
|
||||
),
|
||||
),
|
||||
child: offendingContent,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
TextField(
|
||||
controller: feedbackController,
|
||||
decoration: InputDecoration(
|
||||
labelText: L10n.of(context).feedback,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
maxLines: 4,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(); // Close the dialog
|
||||
},
|
||||
child: Text(L10n.of(context).cancel),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
// Call the additional callback function
|
||||
submitFeedback(feedbackController.text);
|
||||
Navigator.of(context).pop(); // Close the dialog
|
||||
},
|
||||
child: Text(L10n.of(context).submit),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
Loading…
Reference in New Issue