Merge branch 'main' into remove-copy-option

pull/1183/head
ggurdin 1 year ago committed by GitHub
commit ed6b197f63
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -28,6 +28,7 @@ class ITController {
String? sourceText;
List<ITStep> completedITSteps = [];
CurrentITStep? currentITStep;
CurrentITStep? nextITStep;
GoldRouteTracker goldRouteTracker = GoldRouteTracker.defaultTracker;
List<int> payLoadIds = [];
@ -42,6 +43,7 @@ class ITController {
sourceText = null;
completedITSteps = [];
currentITStep = null;
nextITStep = null;
goldRouteTracker = GoldRouteTracker.defaultTracker;
payLoadIds = [];
@ -130,6 +132,7 @@ class ITController {
);
}
if (nextITStep == null) {
currentITStep = null;
final ITResponseModel res = await _customInputTranslation(currentText);
@ -156,10 +159,48 @@ class ITController {
);
_addPayloadId(res);
} else {
currentITStep = nextITStep;
nextITStep = null;
}
if (isTranslationDone) {
choreographer.altTranslator.setTranslationFeedback();
choreographer.getLanguageHelp(true);
} else {
getNextTranslationData();
}
} catch (e, s) {
debugger(when: kDebugMode);
if (e is! http.Response) {
ErrorHandler.logError(e: e, s: s);
}
choreographer.errorService.setErrorAndLock(
ChoreoError(type: ChoreoErrorType.unknown, raw: e),
);
} finally {
choreographer.stopLoading();
}
}
Future<void>getNextTranslationData() async {
try {
if (completedITSteps.length < goldRouteTracker.continuances.length) {
final String currentText = choreographer.currentText;
final String nextText =
goldRouteTracker.continuances[completedITSteps.length].text;
final ITResponseModel res =
await _customInputTranslation(currentText + nextText);
nextITStep = CurrentITStep(
sourceText: sourceText!,
currentText: nextText,
responseModel: res,
storedGoldContinuances: goldRouteTracker.continuances,
);
} else {
nextITStep = null;
}
} catch (e, s) {
debugger(when: kDebugMode);

@ -1,9 +1,11 @@
import 'dart:developer';
import 'dart:math';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:matrix/matrix.dart';
import '../../utils/bot_style.dart';
import 'it_shimmer.dart';
@ -56,10 +58,12 @@ class Choice {
Choice({
this.color,
required this.text,
this.isGold = false,
});
final Color? color;
final String text;
final bool isGold;
}
class ChoiceItem extends StatelessWidget {
@ -86,6 +90,10 @@ class ChoiceItem extends StatelessWidget {
waitDuration: onLongPress != null
? const Duration(milliseconds: 500)
: const Duration(days: 1),
child: ChoiceAnimationWidget(
key: ValueKey(entry.value.text),
selected: entry.value.color != null,
isGold: entry.value.isGold,
child: Container(
margin: const EdgeInsets.all(2),
padding: EdgeInsets.zero,
@ -128,6 +136,7 @@ class ChoiceItem extends StatelessWidget {
),
),
),
),
);
} catch (e) {
debugger(when: kDebugMode);
@ -135,3 +144,110 @@ class ChoiceItem extends StatelessWidget {
}
}
}
class ChoiceAnimationWidget extends StatefulWidget {
final Widget child;
final bool selected;
final bool isGold;
const ChoiceAnimationWidget({
super.key,
required this.child,
required this.selected,
this.isGold = false,
});
@override
ChoiceAnimationWidgetState createState() => ChoiceAnimationWidgetState();
}
class ChoiceAnimationWidgetState extends State<ChoiceAnimationWidget>
with SingleTickerProviderStateMixin {
late final AnimationController _controller;
late final Animation<double> _animation;
bool animationPlayed = false;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 300),
vsync: this,
);
_animation = widget.isGold
? Tween<double>(begin: 1.0, end: 1.2).animate(_controller)
: TweenSequence<double>([
TweenSequenceItem<double>(
tween: Tween<double>(begin: 0, end: -8 * pi / 180),
weight: 1.0,
),
TweenSequenceItem<double>(
tween: Tween<double>(begin: -8 * pi / 180, end: 16 * pi / 180),
weight: 2.0,
),
TweenSequenceItem<double>(
tween: Tween<double>(begin: 16 * pi / 180, end: 0),
weight: 1.0,
),
]).animate(_controller);
if (widget.selected && !animationPlayed) {
_controller.forward();
animationPlayed = true;
setState(() {});
}
_controller.addStatusListener((status) {
if (status == AnimationStatus.completed) {
_controller.reverse();
} else if (status == AnimationStatus.dismissed) {
_controller.stop();
_controller.reset();
}
});
}
@override
void didUpdateWidget(ChoiceAnimationWidget oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.selected && !animationPlayed) {
_controller.forward();
animationPlayed = true;
setState(() {});
}
}
@override
Widget build(BuildContext context) {
return widget.isGold
? AnimatedBuilder(
key: UniqueKey(),
animation: _animation,
builder: (context, child) {
return Transform.scale(
scale: _animation.value,
child: child,
);
},
child: widget.child,
)
: AnimatedBuilder(
key: UniqueKey(),
animation: _animation,
builder: (context, child) {
return Transform.rotate(
angle: _animation.value,
child: child,
);
},
child: widget.child,
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
}

@ -280,7 +280,11 @@ class ITChoices extends StatelessWidget {
originalSpan: "dummy",
choices: controller.currentITStep!.continuances.map((e) {
try {
return Choice(text: e.text.trim(), color: e.color);
return Choice(
text: e.text.trim(),
color: e.color,
isGold: e.description == "best",
);
} catch (e) {
debugger(when: kDebugMode);
return Choice(text: "error", color: Colors.red);

@ -3,6 +3,7 @@ import 'dart:developer';
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
import 'package:fluffychat/pangea/enum/construct_type_enum.dart';
import 'package:fluffychat/pangea/matrix_event_wrappers/construct_analytics_event.dart';
import 'package:fluffychat/pangea/models/student_analytics_summary_model.dart';
import 'package:fluffychat/pangea/utils/error_handler.dart';
import 'package:flutter/foundation.dart';
@ -111,4 +112,48 @@ class MyAnalyticsController {
ErrorHandler.logError(e: err, s: s);
}
}
// used to aggregate ConstructEvents, from multiple senders (students) with the same lemma
List<AggregateConstructUses> aggregateConstructData(
List<ConstructEvent> constructs,
) {
final Map<String, List<ConstructEvent>> lemmasToConstructs = {};
for (final construct in constructs) {
lemmasToConstructs[construct.content.lemma] ??= [];
lemmasToConstructs[construct.content.lemma]!.add(construct);
}
final List<AggregateConstructUses> aggregatedConstructs = [];
for (final lemmaToConstructs in lemmasToConstructs.entries) {
final List<ConstructEvent> lemmaConstructs = lemmaToConstructs.value;
final AggregateConstructUses aggregatedData = AggregateConstructUses(
constructs: lemmaConstructs,
);
aggregatedConstructs.add(aggregatedData);
}
return aggregatedConstructs;
}
}
class AggregateConstructUses {
final List<ConstructEvent> _constructs;
AggregateConstructUses({required List<ConstructEvent> constructs})
: _constructs = constructs;
String get lemma {
assert(
_constructs.isNotEmpty &&
_constructs.every(
(construct) =>
construct.content.lemma == _constructs.first.content.lemma,
),
);
return _constructs.first.content.lemma;
}
List<OneConstructUse> get uses => _constructs
.map((construct) => construct.content.uses)
.expand((element) => element)
.toList();
}

@ -68,26 +68,33 @@ class ConstructUses {
}
enum ConstructUseType {
/// encountered match and accepted it
/// produced in chat by user, igc was run, and we've judged it to be a correct use
wa,
/// produced in chat by user, igc was run, and we've judged it to be a incorrect use
/// Note: if the IGC match is ignored, this is not counted as an incorrect use
ga,
/// used without assistance
wa,
/// produced in chat by user and igc was not run
unk,
/// selected correctly in IT flow
corIt,
/// encountered as it distractor and selected it
incIt,
/// encountered as IT distractor and correctly ignored it
ignIt,
/// encountered as it distractor and selected it
incIt,
/// encountered in igc match and ignored match
ignIGC,
/// encountered in igc match and ignored match
/// selected correctly in IGC flow
corIGC,
/// encountered as distractor in IGC flow and selected it
incIGC,
}
extension on ConstructUseType {
@ -107,6 +114,10 @@ extension on ConstructUseType {
return 'ignIGC';
case ConstructUseType.corIGC:
return 'corIGC';
case ConstructUseType.incIGC:
return 'incIGC';
case ConstructUseType.unk:
return 'unk';
}
}
@ -126,6 +137,10 @@ extension on ConstructUseType {
return Icons.close;
case ConstructUseType.corIGC:
return Icons.check;
case ConstructUseType.incIGC:
return Icons.close;
case ConstructUseType.unk:
return Icons.help;
}
}
}

@ -1,10 +1,10 @@
import 'dart:convert';
import 'dart:developer';
import 'package:fluffychat/pangea/models/constructs_analytics_model.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:fluffychat/pangea/models/constructs_analytics_model.dart';
import '../enum/vocab_proficiency_enum.dart';
class VocabHeadwords {
@ -176,6 +176,11 @@ class VocabTotals {
case ConstructUseType.corIGC:
corIt++;
break;
case ConstructUseType.incIGC:
incIt++;
break;
case ConstructUseType.unk:
break;
}
}
}

@ -104,7 +104,6 @@ class AnalyticsListTileState extends State<AnalyticsListTile> {
)
: null,
selected: widget.selected,
enabled: widget.enabled,
onTap: () {
(room?.isSpace ?? false) && widget.allowNavigateOnSelect
? context.go(

@ -5,6 +5,7 @@ import 'package:fluffychat/pangea/extensions/client_extension.dart';
import 'package:fluffychat/pangea/pages/analytics/base_analytics_view.dart';
import 'package:fluffychat/pangea/pages/analytics/student_analytics/student_analytics.dart';
import 'package:flutter/material.dart';
import 'package:future_loading_dialog/future_loading_dialog.dart';
import 'package:matrix/matrix.dart';
import '../../../widgets/matrix.dart';
@ -101,18 +102,40 @@ class BaseAnalyticsController extends State<BaseAnalyticsPage> {
}
}
void toggleSelection(AnalyticsSelected selectedParam) {
Future<void> toggleSelection(AnalyticsSelected selectedParam) async {
final bool joinSelectedRoom =
selectedParam.type == AnalyticsEntryType.room &&
!enableSelection(
selectedParam,
);
if (joinSelectedRoom) {
await showFutureLoadingDialog(
context: context,
future: () async {
final waitForRoom = Matrix.of(context).client.waitForRoomInSync(
selectedParam.id,
join: true,
);
await Matrix.of(context).client.joinRoom(selectedParam.id);
await waitForRoom;
},
);
}
setState(() {
debugPrint("selectedParam.id is ${selectedParam.id}");
currentLemma = null;
selected = isSelected(selectedParam.id) ? null : selectedParam;
});
pangeaController.analytics.setConstructs(
constructType: ConstructType.grammar,
defaultSelected: widget.defaultSelected,
selected: selected,
removeIT: true,
);
Future.delayed(Duration.zero, () => setState(() {}));
}

@ -145,6 +145,7 @@ class BaseAnalyticsView extends StatelessWidget {
) *
72,
child: TabBarView(
physics: const NeverScrollableScrollPhysics(),
children: [
Column(
crossAxisAlignment:

@ -1,11 +1,10 @@
import 'dart:async';
import 'package:fluffychat/pangea/enum/time_span.dart';
import 'package:fluffychat/pangea/pages/analytics/class_list/class_list_view.dart';
import 'package:flutter/material.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/pangea/enum/time_span.dart';
import 'package:fluffychat/pangea/pages/analytics/class_list/class_list_view.dart';
import '../../../../widgets/matrix.dart';
import '../../../constants/pangea_event_types.dart';
import '../../../controllers/pangea_controller.dart';
@ -42,7 +41,11 @@ class AnalyticsClassListController extends State<AnalyticsClassList> {
if (!(refreshTimer[newState.room.id]?.isActive ?? false)) {
refreshTimer[newState.room.id] = Timer(
const Duration(seconds: 3),
() => updateClassAnalytics(context, newState.room),
() {
if (newState.room.isSpace) {
updateClassAnalytics(context, newState.room);
}
},
);
}
}

@ -3,9 +3,9 @@ import 'dart:async';
import 'package:collection/collection.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pangea/constants/pangea_event_types.dart';
import 'package:fluffychat/pangea/controllers/my_analytics_controller.dart';
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
import 'package:fluffychat/pangea/enum/construct_type_enum.dart';
import 'package:fluffychat/pangea/matrix_event_wrappers/construct_analytics_event.dart';
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_representation_event.dart';
import 'package:fluffychat/pangea/models/constructs_analytics_model.dart';
@ -169,7 +169,7 @@ class ConstructListViewState extends State<ConstructListView> {
int get lemmaIndex =>
constructs?.indexWhere(
(element) => element.content.lemma == widget.controller.currentLemma,
(element) => element.lemma == widget.controller.currentLemma,
) ??
-1;
@ -217,7 +217,7 @@ class ConstructListViewState extends State<ConstructListView> {
setState(() => fetchingUses = true);
try {
final List<OneConstructUse> uses = currentConstruct!.content.uses;
final List<OneConstructUse> uses = currentConstruct!.uses;
_msgEvents.clear();
for (final OneConstructUse use in uses) {
@ -236,16 +236,24 @@ class ConstructListViewState extends State<ConstructListView> {
ErrorHandler.logError(
e: err,
s: s,
m: "Failed to fetch uses for current construct ${currentConstruct?.content.lemma}",
m: "Failed to fetch uses for current construct ${currentConstruct?.lemma}",
);
}
}
List<ConstructEvent>? get constructs =>
widget.pangeaController.analytics.constructs;
List<AggregateConstructUses>? get constructs =>
widget.pangeaController.analytics.constructs != null
? widget.pangeaController.myAnalytics
.aggregateConstructData(
widget.pangeaController.analytics.constructs!,
)
.sorted(
(a, b) => b.uses.length.compareTo(a.uses.length),
)
: null;
ConstructEvent? get currentConstruct => constructs?.firstWhereOrNull(
(element) => element.content.lemma == widget.controller.currentLemma,
AggregateConstructUses? get currentConstruct => constructs?.firstWhereOrNull(
(element) => element.lemma == widget.controller.currentLemma,
);
// given the current lemma and list of message events, return a list of
@ -280,6 +288,13 @@ class ConstructListViewState extends State<ConstructListView> {
return allMsgErrorSteps;
}
Future<void> showConstructMessagesDialog() async {
await showDialog<ConstructMessagesDialog>(
context: context,
builder: (c) => ConstructMessagesDialog(controller: this),
);
}
@override
Widget build(BuildContext context) {
if (!widget.init || fetchingUses) {
@ -294,57 +309,91 @@ class ConstructListViewState extends State<ConstructListView> {
);
}
final msgEventMatches = getMessageEventMatches();
return widget.controller.currentLemma == null
? Expanded(
return Expanded(
child: ListView.builder(
itemCount: constructs!.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(
constructs![index].content.lemma,
constructs![index].lemma,
),
subtitle: Text(
'${L10n.of(context)!.total} ${constructs![index].content.uses.length}',
'${L10n.of(context)!.total} ${constructs![index].uses.length}',
),
onTap: () {
final String lemma = constructs![index].content.lemma;
onTap: () async {
final String lemma = constructs![index].lemma;
widget.controller.setCurrentLemma(lemma);
fetchUses();
fetchUses().then((_) => showConstructMessagesDialog());
},
);
},
),
)
: Expanded(
child: Column(
);
}
}
class ConstructMessagesDialog extends StatelessWidget {
final ConstructListViewState controller;
const ConstructMessagesDialog({
super.key,
required this.controller,
});
@override
Widget build(BuildContext context) {
if (controller.widget.controller.currentLemma == null) {
return const AlertDialog(content: CircularProgressIndicator.adaptive());
}
final msgEventMatches = controller.getMessageEventMatches();
return AlertDialog(
title: Center(child: Text(controller.widget.controller.currentLemma!)),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (constructs![lemmaIndex].content.uses.length >
_msgEvents.length)
if (controller.constructs![controller.lemmaIndex].uses.length >
controller._msgEvents.length)
Center(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(L10n.of(context)!.roomDataMissing),
),
),
Expanded(
child: ListView.separated(
separatorBuilder: (context, index) =>
SingleChildScrollView(
child: Column(
children: [
...msgEventMatches.mapIndexed(
(index, event) => Column(
children: [
ConstructMessage(
msgEvent: event.msgEvent,
lemma: controller.widget.controller.currentLemma!,
errorMessage: event.lemmaMatch,
),
if (index < msgEventMatches.length - 1)
const Divider(height: 1),
itemCount: msgEventMatches.length,
itemBuilder: (context, index) {
return ConstructMessage(
msgEvent: msgEventMatches[index].msgEvent,
lemma: widget.controller.currentLemma!,
errorMessage: msgEventMatches[index].lemmaMatch,
);
},
],
),
),
],
),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.of(context, rootNavigator: false).pop(),
child: Text(
L10n.of(context)!.close.toUpperCase(),
style: TextStyle(
color:
Theme.of(context).textTheme.bodyMedium?.color?.withAlpha(150),
),
),
),
],
);
}
}

@ -198,6 +198,7 @@ class WordMatchContent extends StatelessWidget {
(e) => Choice(
text: e.value,
color: e.selected ? e.type.color : null,
isGold: e.type.name == 'bestCorrection',
),
)
.toList(),

Loading…
Cancel
Save