Merge branch 'main' into 5208-exiting-practice

This commit is contained in:
Ava Shilling 2026-01-16 12:19:56 -05:00
commit b21173e482
9 changed files with 100 additions and 36 deletions

View file

@ -29,8 +29,10 @@ class StateMessage extends StatelessWidget {
// event.calcLocalizedBodyFallback(
// MatrixLocals(L10n.of(context)),
// ),
event.type == EventTypes.RoomMember &&
event.roomMemberChangeType == RoomMemberChangeType.leave
(event.type == EventTypes.RoomMember) &&
(event.roomMemberChangeType ==
RoomMemberChangeType.leave) &&
(event.stateKey == event.room.client.userID)
? L10n.of(context).youLeftTheChat
: event.calcLocalizedBodyFallback(
MatrixLocals(L10n.of(context)),

View file

@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
import 'package:diacritic/diacritic.dart';
import 'package:go_router/go_router.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pangea/analytics_data/analytics_data_service.dart';
@ -248,10 +249,11 @@ Widget _buildVocabPracticeButton(BuildContext context) {
label: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (!hasEnoughVocab) ...[
const Icon(Icons.lock_outline, size: 18),
const SizedBox(width: 4),
],
Icon(
hasEnoughVocab ? Symbols.fitness_center : Icons.lock_outline,
size: 18,
),
const SizedBox(width: 4),
Text(L10n.of(context).practiceVocab),
],
),

View file

@ -24,16 +24,19 @@ class LemmaUsageDots extends StatelessWidget {
});
/// Find lemma uses for the given exercise type, to create dot list
List<bool> sortedUses(LearningSkillsEnum category) {
final List<bool> useList = [];
List<Color> sortedUses(LearningSkillsEnum category) {
final List<Color> useList = [];
for (final OneConstructUse use in construct.cappedUses) {
if (use.xp == 0) {
continue;
}
// If the use type matches the given category, save to list
// Usage with positive XP is saved as true, else false
if (category == use.useType.skillsEnumType) {
useList.add(use.xp > 0);
useList.add(
switch (use.xp) {
> 0 => AppConfig.success,
< 0 => Colors.red,
_ => Colors.grey[400]!,
},
);
}
}
return useList;
@ -42,13 +45,13 @@ class LemmaUsageDots extends StatelessWidget {
@override
Widget build(BuildContext context) {
final List<Widget> dots = [];
for (final bool use in sortedUses(category)) {
for (final Color color in sortedUses(category)) {
dots.add(
Container(
width: 15.0,
height: 15.0,
decoration: BoxDecoration(
color: use ? AppConfig.success : Colors.red,
color: color,
shape: BoxShape.circle,
),
),

View file

@ -175,7 +175,9 @@ class LearningProgressIndicators extends StatelessWidget {
builder: (context, hovered) {
return Container(
decoration: BoxDecoration(
color: hovered && canSelect
color: (hovered && canSelect) ||
(selected ==
ProgressIndicatorEnum.level)
? Theme.of(context)
.colorScheme
.primary

View file

@ -11,4 +11,5 @@ class ChoreoConstants {
static const int msBeforeIGCStart = 10000;
static const int maxLength = 1000;
static const String inputTransformTargetKey = 'input_text_field';
static const int defaultErrorBackoffSeconds = 5;
}

View file

@ -2,6 +2,8 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:async/async.dart';
import 'package:fluffychat/pangea/choreographer/assistance_state_enum.dart';
import 'package:fluffychat/pangea/choreographer/choreo_constants.dart';
import 'package:fluffychat/pangea/choreographer/choreo_mode_enum.dart';
@ -45,6 +47,11 @@ class Choreographer extends ChangeNotifier {
String? _lastChecked;
ChoreoModeEnum _choreoMode = ChoreoModeEnum.igc;
DateTime? _lastIgcError;
DateTime? _lastTokensError;
int _igcErrorBackoff = ChoreoConstants.defaultErrorBackoffSeconds;
int _tokenErrorBackoff = ChoreoConstants.defaultErrorBackoffSeconds;
StreamSubscription? _languageSub;
StreamSubscription? _settingsUpdateSub;
StreamSubscription? _acceptedContinuanceSub;
@ -68,6 +75,12 @@ class Choreographer extends ChangeNotifier {
openMatches: [],
);
bool _backoffRequest(DateTime? error, int backoffSeconds) {
if (error == null) return false;
final secondsSinceError = DateTime.now().difference(error).inSeconds;
return secondsSinceError <= backoffSeconds;
}
void _initialize() {
textController = PangeaTextController(choreographer: this);
textController.addListener(_onChange);
@ -82,7 +95,14 @@ class Choreographer extends ChangeNotifier {
itController.editing.addListener(_onSubmitSourceTextEdits);
igcController = IgcController(
(e) => errorService.setErrorAndLock(ChoreoError(raw: e)),
(e) {
errorService.setErrorAndLock(ChoreoError(raw: e));
_lastIgcError = DateTime.now();
_igcErrorBackoff *= 2;
},
() {
_igcErrorBackoff = ChoreoConstants.defaultErrorBackoffSeconds;
},
);
_languageSub ??= MatrixState
@ -233,7 +253,8 @@ class Choreographer extends ChangeNotifier {
!ToolSetting.interactiveTranslator.enabled) ||
(!ToolSetting.autoIGC.enabled &&
!manual &&
_choreoMode != ChoreoModeEnum.it)) {
_choreoMode != ChoreoModeEnum.it) ||
_backoffRequest(_lastIgcError, _igcErrorBackoff)) {
return;
}
@ -275,7 +296,9 @@ class Choreographer extends ChangeNotifier {
MatrixState.pangeaController.userController.userL2?.langCode;
final l1LangCode =
MatrixState.pangeaController.userController.userL1?.langCode;
if (l1LangCode != null && l2LangCode != null) {
if (l1LangCode != null &&
l2LangCode != null &&
!_backoffRequest(_lastTokensError, _tokenErrorBackoff)) {
final res = await TokensRepo.get(
MatrixState.pangeaController.userController.accessToken,
TokensRequestModel(
@ -283,7 +306,21 @@ class Choreographer extends ChangeNotifier {
senderL1: l1LangCode,
senderL2: l2LangCode,
),
).timeout(
const Duration(seconds: 10),
onTimeout: () {
return Result.error("Token request timed out");
},
);
if (res.isError) {
_lastTokensError = DateTime.now();
_tokenErrorBackoff *= 2;
} else {
// reset backoff on success
_tokenErrorBackoff = ChoreoConstants.defaultErrorBackoffSeconds;
}
tokensResp = res.isValue ? res.result : null;
}

View file

@ -18,7 +18,8 @@ import 'package:fluffychat/widgets/matrix.dart';
class IgcController {
final Function(Object) onError;
IgcController(this.onError);
final VoidCallback onFetch;
IgcController(this.onError, this.onFetch);
bool _isFetching = false;
String? _currentText;
@ -321,6 +322,8 @@ class IgcController {
onError(res.asError!);
clear();
return;
} else {
onFetch();
}
if (!_isFetching) return;

View file

@ -2,6 +2,7 @@ import 'dart:convert';
import 'package:async/async.dart';
import 'package:http/http.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:fluffychat/pangea/common/config/environment.dart';
import 'package:fluffychat/pangea/common/network/requests.dart';
@ -62,7 +63,18 @@ class TokensRepo {
final Map<String, dynamic> json =
jsonDecode(utf8.decode(res.bodyBytes).toString());
return TokensResponseModel.fromJson(json);
final tokens = TokensResponseModel.fromJson(json);
if (tokens.tokens.any((t) => t.pos == 'other')) {
ErrorHandler.logError(
e: Exception('Received token with pos "other"'),
data: {
"request": request.toJson(),
"response": json,
},
level: SentryLevel.warning,
);
}
return tokens;
}
static Future<Result<TokensResponseModel>> _getResult(

View file

@ -67,26 +67,28 @@ class VocabPracticeView extends StatelessWidget {
],
),
),
body: MaxWidthBody(
withScrolling: false,
body: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 24.0,
),
showBorder: false,
child: ValueListenableBuilder(
valueListenable: controller.sessionState,
builder: (context, state, __) {
return switch (state) {
AsyncError<VocabPracticeSessionModel>(:final error) =>
ErrorIndicator(message: error.toString()),
AsyncLoaded<VocabPracticeSessionModel>(:final value) =>
value.isComplete
? CompletedActivitySessionView(state.value, controller)
: _VocabActivityView(controller),
_ => loading,
};
},
child: MaxWidthBody(
withScrolling: false,
showBorder: false,
child: ValueListenableBuilder(
valueListenable: controller.sessionState,
builder: (context, state, __) {
return switch (state) {
AsyncError<VocabPracticeSessionModel>(:final error) =>
ErrorIndicator(message: error.toString()),
AsyncLoaded<VocabPracticeSessionModel>(:final value) =>
value.isComplete
? CompletedActivitySessionView(state.value, controller)
: _VocabActivityView(controller),
_ => loading,
};
},
),
),
),
);