only rebuild choreo-widgets when related data updates

This commit is contained in:
ggurdin 2025-11-04 14:39:16 -05:00
parent ae29fbd11a
commit 4bbb81e20c
No known key found for this signature in database
GPG key ID: A01CB41737CBB478
17 changed files with 288 additions and 342 deletions

View file

@ -353,7 +353,9 @@ class ChatView extends StatelessWidget {
child: Stack(
children: [
ChatEventList(controller: controller),
ChatViewBackground(controller.choreographer),
ChatViewBackground(
controller.choreographer.itController.open,
),
],
),
// Pangea#

View file

@ -446,7 +446,7 @@ class InputBar extends StatelessWidget {
return;
}
final match = choreographer.getMatchByOffset(
final match = choreographer.igcController.getMatchByOffset(
selection.baseOffset,
);
if (match == null) return;

View file

@ -3,27 +3,24 @@ import 'package:flutter/material.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/pangea/activity_sessions/activity_room_extension.dart';
import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart';
import 'package:fluffychat/pangea/instructions/instructions_inline_tooltip.dart';
class ActivityRoleTooltip extends StatelessWidget {
final Choreographer choreographer;
final Room room;
final ValueNotifier<bool> hide;
const ActivityRoleTooltip({
required this.choreographer,
required this.room,
required this.hide,
super.key,
});
Room get room => choreographer.chatController.room;
@override
Widget build(BuildContext context) {
return ListenableBuilder(
listenable: choreographer,
builder: (context, _) {
if (!room.showActivityChatUI ||
room.ownRole?.goal == null ||
choreographer.itController.open.value) {
return ValueListenableBuilder(
valueListenable: hide,
builder: (context, hide, _) {
if (!room.showActivityChatUI || room.ownRole?.goal == null || hide) {
return const SizedBox();
}

View file

@ -23,7 +23,8 @@ class ChatInputBar extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
children: [
ActivityRoleTooltip(
choreographer: controller.choreographer,
room: controller.room,
hide: controller.choreographer.itController.open,
),
ITBar(choreographer: controller.choreographer),
if (!controller.obscureText) ReplyDisplay(controller),

View file

@ -2,18 +2,16 @@ import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart';
class ChatViewBackground extends StatelessWidget {
final Choreographer choreographer;
const ChatViewBackground(this.choreographer, {super.key});
final ValueNotifier<bool> visible;
const ChatViewBackground(this.visible, {super.key});
@override
Widget build(BuildContext context) {
return ListenableBuilder(
listenable: choreographer,
builder: (context, _) {
return choreographer.itController.open.value
return ValueListenableBuilder(
valueListenable: visible,
builder: (context, value, _) {
return value
? Positioned(
left: 0,
right: 0,

View file

@ -239,7 +239,9 @@ class PangeaChatInputRow extends StatelessWidget {
foregroundColor: theme.onBubbleColor,
child: const Icon(Icons.mic_none_outlined),
)
: ChoreographerSendButton(controller: controller),
: ChoreographerSendButton(
choreographer: controller.choreographer,
),
),
],
),

View file

@ -15,13 +15,13 @@ import 'package:fluffychat/pangea/choreographer/enums/choreo_mode.dart';
import 'package:fluffychat/pangea/choreographer/enums/edit_type.dart';
import 'package:fluffychat/pangea/choreographer/enums/pangea_match_status.dart';
import 'package:fluffychat/pangea/choreographer/models/choreo_record.dart';
import 'package:fluffychat/pangea/choreographer/models/completed_it_step.dart';
import 'package:fluffychat/pangea/choreographer/models/pangea_match_state.dart';
import 'package:fluffychat/pangea/common/controllers/pangea_controller.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/events/models/representation_content_model.dart';
import 'package:fluffychat/pangea/events/models/tokens_event_content_model.dart';
import 'package:fluffychat/pangea/events/repo/token_api_models.dart';
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
import 'package:fluffychat/pangea/learning_settings/constants/language_constants.dart';
import 'package:fluffychat/pangea/subscription/controllers/subscription_controller.dart';
import 'package:fluffychat/pangea/toolbar/controllers/tts_controller.dart';
@ -39,14 +39,14 @@ class Choreographer extends ChangeNotifier {
late PangeaTextController textController;
late ITController itController;
late IgcController igc;
late IgcController igcController;
late ErrorService errorService;
ChoreoRecord? _choreoRecord;
final ValueNotifier<bool> _isFetching = ValueNotifier(false);
int _timesClicked = 0;
int _timesClicked = 0;
Timer? _debounceTimer;
String? _lastChecked;
ChoreoMode _choreoMode = ChoreoMode.igc;
@ -65,14 +65,19 @@ class Choreographer extends ChangeNotifier {
void _initialize() {
textController = PangeaTextController(choreographer: this);
itController = ITController(this);
igc = IgcController(this);
textController.addListener(_onChange);
errorService = ErrorService();
errorService.addListener(notifyListeners);
textController.addListener(_onChange);
itController = ITController(
(e) => errorService.setErrorAndLock(ChoreoError(raw: e)),
);
itController.open.addListener(_onCloseIT);
igcController = IgcController(
(e) => errorService.setErrorAndLock(ChoreoError(raw: e)),
);
_languageStream =
pangeaController.userController.languageStream.stream.listen((update) {
@ -95,7 +100,7 @@ class Choreographer extends ChangeNotifier {
_choreoRecord = null;
itController.clear();
itController.clearSourceText();
igc.clear();
igcController.clear();
_resetDebounceTimer();
}
@ -124,7 +129,7 @@ class Choreographer extends ChangeNotifier {
// if user is doing IT, call closeIT here to
// ensure source text is replaced when needed
if (itController.open.value && _timesClicked > 1) {
closeIT();
itController.closeIT();
}
}
}
@ -161,7 +166,71 @@ class Choreographer extends ChangeNotifier {
}
Future<void> requestLanguageAssistance() =>
_getLanguageAssistance(manual: true);
_startWritingAssistance(manual: true);
/// Handles any changes to the text input
void _onChange() {
if (_lastChecked != null && _lastChecked == textController.text) {
return;
}
_lastChecked = textController.text;
if (textController.editType == EditType.it) {
return;
}
if (textController.editType == EditType.igc ||
textController.editType == EditType.itDismissed) {
textController.editType = EditType.keyboard;
return;
}
// Close any open IGC overlays
MatrixState.pAnyState.closeOverlay();
if (errorService.isError) return;
igcController.clear();
if (textController.editType == EditType.keyboard) {
itController.clearSourceText();
}
_resetDebounceTimer();
_debounceTimer ??= Timer(
const Duration(milliseconds: ChoreoConstants.msBeforeIGCStart),
() => _startWritingAssistance(),
);
//Note: we don't set the keyboard type on each keyboard stroke so this is how we default to
//a change being from the keyboard unless explicitly set to one of the other
//types when that action happens (e.g. an it/igc choice is selected)
textController.editType = EditType.keyboard;
}
Future<void> _startWritingAssistance({
bool manual = false,
}) async {
if (errorService.isError || isRunningIT) return;
final SubscriptionStatus canSendStatus =
pangeaController.subscriptionController.subscriptionStatus;
if (canSendStatus != SubscriptionStatus.subscribed ||
l2Lang == null ||
l1Lang == null ||
(!igcEnabled && !itEnabled) ||
(!isAutoIGCEnabled && !manual && _choreoMode != ChoreoMode.it)) {
return;
}
_resetDebounceTimer();
_initChoreoRecord();
_startLoading();
await igcController.getIGCTextData(
textController.text,
chatController.room.getPreviousMessages(),
);
_acceptNormalizationMatches();
_stopLoading();
}
Future<void> send([int recurrence = 0]) async {
// if isFetching, already called to getLanguageHelp and hasn't completed yet
@ -188,7 +257,7 @@ class Choreographer extends ChangeNotifier {
return;
}
if (igc.canShowFirstMatch) {
if (igcController.canShowFirstMatch) {
throw OpenMatchesException();
} else if (isRunningIT) {
// If the user is in the middle of IT, don't send the message.
@ -213,10 +282,10 @@ class Choreographer extends ChangeNotifier {
return;
}
if (!igc.hasIGCTextData && !itController.dismissed) {
await _getLanguageAssistance();
if (!igcController.hasIGCTextData && !itController.dismissed) {
await _startWritingAssistance();
// it's possible for this not to be true, i.e. if IGC has an error
if (igc.hasIGCTextData) {
if (igcController.hasIGCTextData) {
await send(recurrence + 1);
}
} else {
@ -224,72 +293,6 @@ class Choreographer extends ChangeNotifier {
}
}
/// Handles any changes to the text input
void _onChange() {
if (_lastChecked != null && _lastChecked == textController.text) {
return;
}
_lastChecked = textController.text;
if (textController.editType == EditType.it) {
return;
}
if (textController.editType == EditType.igc ||
textController.editType == EditType.itDismissed) {
textController.editType = EditType.keyboard;
return;
}
// Close any open IGC/IT overlays
MatrixState.pAnyState.closeOverlay();
if (errorService.isError) return;
igc.clear();
if (textController.editType == EditType.keyboard) {
itController.clearSourceText();
}
_resetDebounceTimer();
_debounceTimer ??= Timer(
const Duration(milliseconds: ChoreoConstants.msBeforeIGCStart),
() => _getLanguageAssistance(),
);
//Note: we don't set the keyboard type on each keyboard stroke so this is how we default to
//a change being from the keyboard unless explicitly set to one of the other
//types when that action happens (e.g. an it/igc choice is selected)
textController.editType = EditType.keyboard;
}
/// Fetches the language help for the current text, including grammar correction, language detection,
/// tokens, and translations. Includes logic to exit the flow if the user is not subscribed, if the tools are not enabled, or
/// or if autoIGC is not enabled and the user has not manually requested it.
/// [onlyTokensAndLanguageDetection] will
Future<void> _getLanguageAssistance({
bool manual = false,
}) async {
if (errorService.isError) return;
final SubscriptionStatus canSendStatus =
pangeaController.subscriptionController.subscriptionStatus;
if (canSendStatus != SubscriptionStatus.subscribed ||
l2Lang == null ||
l1Lang == null ||
(!igcEnabled && !itEnabled) ||
(!isAutoIGCEnabled && !manual && _choreoMode != ChoreoMode.it)) {
return;
}
_resetDebounceTimer();
_initChoreoRecord();
_startLoading();
await igc.getIGCTextData();
_stopLoading();
}
Future<void> _sendWithIGC() async {
if (chatController.sendController.text.trim().isEmpty) {
return;
@ -369,12 +372,14 @@ class Choreographer extends ChangeNotifier {
if (!itMatch.updatedMatch.isITStart) {
throw Exception("Attempted to open IT with a non-IT start match");
}
chatController.inputFocus.unfocus();
chatController.inputFocus.unfocus();
setChoreoMode(ChoreoMode.it);
itController.openIT(textController.text);
final sourceText = currentText;
textController.setSystemText("", EditType.it);
igc.clear();
itController.openIT(sourceText);
igcController.clear();
_initChoreoRecord();
itMatch.setStatus(PangeaMatchStatus.accepted);
@ -382,18 +387,26 @@ class Choreographer extends ChangeNotifier {
"",
match: itMatch.updatedMatch,
);
notifyListeners();
}
void closeIT() {
itController.closeIT();
void _onCloseIT() {
if (itController.open.value) return;
if (currentText.isEmpty && itController.sourceText.value != null) {
textController.setSystemText(
itController.sourceText.value!,
EditType.itDismissed,
);
}
setChoreoMode(ChoreoMode.igc);
errorService.resetError();
notifyListeners();
}
Continuance onSelectContinuance(int index) {
final continuance = itController.onSelectContinuance(index);
notifyListeners();
return continuance;
void onSubmitEdits(String text) {
textController.setSystemText("", EditType.it);
itController.onSubmitEdits(text);
}
void onAcceptContinuance(int index) {
@ -402,9 +415,6 @@ class Choreographer extends ChangeNotifier {
textController.text + step.continuances[step.chosen].text,
EditType.it,
);
textController.selection = TextSelection.collapsed(
offset: textController.text.length,
);
_initChoreoRecord();
_choreoRecord!.addRecord(textController.text, step: step);
@ -412,45 +422,22 @@ class Choreographer extends ChangeNotifier {
notifyListeners();
}
void setEditingSourceText(bool value) {
itController.setEditing(value);
notifyListeners();
}
void submitSourceTextEdits(String text) {
textController.setSystemText("", EditType.it);
itController.onSubmitEdits();
notifyListeners();
}
PangeaMatchState? getMatchByOffset(int offset) =>
igc.getMatchByOffset(offset);
void clearMatches(Object error) {
MatrixState.pAnyState.closeAllOverlays();
igc.clearMatches();
igcController.clearMatches();
errorService.setError(ChoreoError(raw: error));
}
Future<void> fetchSpanDetails({
required PangeaMatchState match,
bool force = false,
}) =>
igc.fetchSpanDetails(
match: match,
force: force,
);
void onAcceptReplacement({
required PangeaMatchState match,
}) {
final updatedMatch = igc.acceptReplacement(
final updatedMatch = igcController.acceptReplacement(
match,
PangeaMatchStatus.accepted,
);
textController.setSystemText(
igc.currentText!,
igcController.currentText!,
EditType.igc,
);
@ -461,19 +448,18 @@ class Choreographer extends ChangeNotifier {
match: updatedMatch,
);
}
MatrixState.pAnyState.closeOverlay();
notifyListeners();
}
void onUndoReplacement(PangeaMatchState match) {
igc.undoReplacement(match);
igcController.undoReplacement(match);
_choreoRecord?.choreoSteps.removeWhere(
(step) => step.acceptedOrIgnoredMatch == match.updatedMatch,
);
textController.setSystemText(
igc.currentText!,
igcController.currentText!,
EditType.igc,
);
MatrixState.pAnyState.closeOverlay();
@ -481,7 +467,7 @@ class Choreographer extends ChangeNotifier {
}
void onIgnoreMatch({required PangeaMatchState match}) {
final updatedMatch = igc.ignoreReplacement(match);
final updatedMatch = igcController.ignoreReplacement(match);
if (!updatedMatch.match.isNormalizationError()) {
_initChoreoRecord();
_choreoRecord!.addRecord(
@ -493,31 +479,42 @@ class Choreographer extends ChangeNotifier {
notifyListeners();
}
void acceptNormalizationMatches() {
final normalizationsMatches = igc.openNormalizationMatches;
void _acceptNormalizationMatches() {
final normalizationsMatches = igcController.openNormalizationMatches;
if (normalizationsMatches?.isEmpty ?? true) return;
_initChoreoRecord();
for (final match in normalizationsMatches!) {
match.selectChoice(
match.updatedMatch.match.choices!.indexWhere(
(c) => c.isBestCorrection,
),
);
try {
for (final match in normalizationsMatches!) {
match.selectChoice(
match.updatedMatch.match.choices!.indexWhere(
(c) => c.isBestCorrection,
),
);
final updatedMatch = igcController.acceptReplacement(
match,
PangeaMatchStatus.automatic,
);
final updatedMatch = igc.acceptReplacement(
match,
PangeaMatchStatus.automatic,
);
textController.setSystemText(
igc.currentText!,
EditType.igc,
);
_choreoRecord!.addRecord(
currentText,
match: updatedMatch,
textController.setSystemText(
igcController.currentText!,
EditType.igc,
);
_choreoRecord!.addRecord(
currentText,
match: updatedMatch,
);
}
} catch (e, s) {
ErrorHandler.logError(
e: e,
s: s,
data: {
"currentText": currentText,
"l1LangCode": l1LangCode,
"l2LangCode": l2LangCode,
"choreoRecord": _choreoRecord?.toJson(),
},
);
}
notifyListeners();

View file

@ -10,13 +10,13 @@ extension ChoregrapherUserSettingsExtension on Choreographer {
itController.currentITStep.value?.isFinal != true;
}
String? get currentIGCText => igc.currentText;
PangeaMatchState? get openMatch => igc.openMatch;
PangeaMatchState? get firstOpenMatch => igc.firstOpenMatch;
List<PangeaMatchState>? get openIGCMatches => igc.openMatches;
List<PangeaMatchState>? get closedIGCMatches => igc.closedMatches;
bool get canShowFirstIGCMatch => igc.canShowFirstMatch;
bool get hasIGCTextData => igc.hasIGCTextData;
String? get currentIGCText => igcController.currentText;
PangeaMatchState? get openMatch => igcController.openMatch;
PangeaMatchState? get firstOpenMatch => igcController.firstOpenMatch;
List<PangeaMatchState>? get openIGCMatches => igcController.openMatches;
List<PangeaMatchState>? get closedIGCMatches => igcController.closedMatches;
bool get canShowFirstIGCMatch => igcController.canShowFirstMatch;
bool get hasIGCTextData => igcController.hasIGCTextData;
AssistanceState get assistanceState {
final isSubscribed = pangeaController.subscriptionController.isSubscribed;
@ -29,12 +29,12 @@ extension ChoregrapherUserSettingsExtension on Choreographer {
return AssistanceState.error;
}
if (igc.hasOpenMatches || isRunningIT) {
if (igcController.hasOpenMatches || isRunningIT) {
return AssistanceState.fetched;
}
if (isFetching.value) return AssistanceState.fetching;
if (!igc.hasIGCTextData) return AssistanceState.notFetched;
if (!igcController.hasIGCTextData) return AssistanceState.notFetched;
return AssistanceState.complete;
}
@ -57,13 +57,13 @@ extension ChoregrapherUserSettingsExtension on Choreographer {
if (isFetching.value) return false;
// they're supposed to run IGC but haven't yet, don't let them send
if (!igc.hasIGCTextData) {
if (!igcController.hasIGCTextData) {
return itController.dismissed;
}
// if they have relevant matches, don't let them send
final hasITMatches = igc.hasOpenITMatches;
final hasIGCMatches = igc.hasOpenIGCMatches;
final hasITMatches = igcController.hasOpenITMatches;
final hasIGCMatches = igcController.hasOpenIGCMatches;
if ((itEnabled && hasITMatches) || (igcEnabled && hasIGCMatches)) {
return false;
}

View file

@ -1,15 +1,7 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:async/async.dart';
import 'package:matrix/matrix.dart' hide Result;
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart';
import 'package:fluffychat/pangea/choreographer/controllers/error_service.dart';
import 'package:fluffychat/pangea/choreographer/controllers/extensions/choregrapher_user_settings_extension.dart';
import 'package:fluffychat/pangea/choreographer/enums/choreo_mode.dart';
import 'package:fluffychat/pangea/choreographer/enums/pangea_match_status.dart';
import 'package:fluffychat/pangea/choreographer/models/igc_text_data_model.dart';
import 'package:fluffychat/pangea/choreographer/models/pangea_match_model.dart';
@ -18,16 +10,16 @@ import 'package:fluffychat/pangea/choreographer/repo/igc_repo.dart';
import 'package:fluffychat/pangea/choreographer/repo/igc_request_model.dart';
import 'package:fluffychat/pangea/choreographer/repo/span_data_repo.dart';
import 'package:fluffychat/pangea/choreographer/repo/span_data_request.dart';
import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart';
import 'package:fluffychat/widgets/future_loading_dialog.dart';
import 'package:fluffychat/widgets/matrix.dart';
import '../../common/utils/error_handler.dart';
class IgcController {
final Choreographer _choreographer;
final Function(Object) onError;
bool _isFetching = false;
IGCTextData? _igcTextData;
IgcController(this._choreographer);
IgcController(this.onError);
String? get currentText => _igcTextData?.currentText;
bool get hasOpenMatches => _igcTextData?.hasOpenMatches == true;
@ -42,12 +34,10 @@ class IgcController {
_igcTextData?.openNormalizationMatches;
bool get canShowFirstMatch => _igcTextData?.firstOpenMatch != null;
bool get hasIGCTextData {
if (_igcTextData == null) return false;
return _igcTextData!.currentText == _choreographer.currentText;
}
bool get hasIGCTextData => _igcTextData != null;
void clear() {
_isFetching = false;
_igcTextData = null;
MatrixState.pAnyState.closeAllOverlays();
}
@ -64,7 +54,8 @@ class IgcController {
if (_igcTextData == null) {
throw "acceptReplacement called with null igcTextData";
}
return _igcTextData!.acceptReplacement(match, status);
final updateMatch = _igcTextData!.acceptReplacement(match, status);
return updateMatch;
}
PangeaMatch ignoreReplacement(PangeaMatchState match) {
@ -82,24 +73,25 @@ class IgcController {
_igcTextData!.undoReplacement(match);
}
Future<void> getIGCTextData() async {
if (_choreographer.currentText.isEmpty) return clear();
debugPrint('getIGCTextData called with ${_choreographer.currentText}');
Future<void> getIGCTextData(
String text,
List<PreviousMessage> prevMessages,
) async {
if (text.isEmpty) return clear();
if (_isFetching) return;
_isFetching = true;
final IGCRequestModel reqBody = IGCRequestModel(
fullText: _choreographer.currentText,
userId: _choreographer.pangeaController.userController.userId!,
userL1: _choreographer.l1LangCode!,
userL2: _choreographer.l2LangCode!,
enableIGC: _choreographer.igcEnabled &&
_choreographer.choreoMode != ChoreoMode.it,
enableIT: _choreographer.itEnabled &&
_choreographer.choreoMode != ChoreoMode.it,
prevMessages: _prevMessages(),
fullText: text,
userId: MatrixState.pangeaController.userController.userId!,
userL1: MatrixState.pangeaController.languageController.activeL1Code()!,
userL2: MatrixState.pangeaController.languageController.activeL2Code()!,
enableIGC: true,
enableIT: true,
prevMessages: prevMessages,
);
final res = await IgcRepo.get(
_choreographer.pangeaController.userController.accessToken,
MatrixState.pangeaController.userController.accessToken,
reqBody,
).timeout(
(const Duration(seconds: 10)),
@ -111,37 +103,18 @@ class IgcController {
);
if (res.isError) {
_choreographer.errorService.setErrorAndLock(
ChoreoError(raw: res.asError),
);
onError(res.asError!);
clear();
return;
}
// this will happen when the user changes the input while igc is fetching results
if (res.result!.originalInput.trim() != _choreographer.currentText.trim()) {
return;
}
if (!_isFetching) return;
final response = res.result!;
_igcTextData = IGCTextData(
originalInput: response.originalInput,
matches: response.matches,
);
try {
_choreographer.acceptNormalizationMatches();
} catch (e, s) {
ErrorHandler.logError(
e: e,
s: s,
level: SentryLevel.warning,
data: {
"igcResponse": response.toJson(),
},
);
}
_isFetching = false;
if (_igcTextData != null) {
for (final match in _igcTextData!.openMatches) {
fetchSpanDetails(match: match).catchError((e) {});
@ -159,12 +132,12 @@ class IgcController {
}
final response = await SpanDataRepo.get(
_choreographer.pangeaController.userController.accessToken,
MatrixState.pangeaController.userController.accessToken,
request: SpanDetailsRequest(
userL1: _choreographer.l1LangCode!,
userL2: _choreographer.l2LangCode!,
enableIGC: _choreographer.igcEnabled,
enableIT: _choreographer.itEnabled,
userL1: MatrixState.pangeaController.languageController.activeL1Code()!,
userL2: MatrixState.pangeaController.languageController.activeL2Code()!,
enableIGC: true,
enableIT: true,
span: span,
),
).timeout(
@ -182,39 +155,4 @@ class IgcController {
_igcTextData?.setSpanData(match, response.result!.span);
}
List<PreviousMessage> _prevMessages({int numMessages = 5}) {
final List<Event> events = _choreographer.chatController.visibleEvents
.where(
(e) =>
e.type == EventTypes.Message &&
(e.messageType == MessageTypes.Text ||
e.messageType == MessageTypes.Audio),
)
.toList();
final List<PreviousMessage> messages = [];
for (final Event event in events) {
final String? content = event.messageType == MessageTypes.Text
? event.content.toString()
: PangeaMessageEvent(
event: event,
timeline: _choreographer.chatController.timeline!,
ownMessage: event.senderId ==
_choreographer.pangeaController.matrixState.client.userID,
).getSpeechToTextLocal()?.transcript.text.trim(); // trim whitespace
if (content == null) continue;
messages.add(
PreviousMessage(
content: content,
sender: event.senderId,
timestamp: event.originServerTs,
),
);
if (messages.length >= numMessages) {
return messages;
}
}
return messages;
}
}

View file

@ -5,9 +5,6 @@ import 'package:flutter/foundation.dart';
import 'package:async/async.dart';
import 'package:fluffychat/pangea/choreographer/controllers/error_service.dart';
import 'package:fluffychat/pangea/choreographer/enums/choreo_mode.dart';
import 'package:fluffychat/pangea/choreographer/enums/edit_type.dart';
import 'package:fluffychat/pangea/choreographer/models/gold_route_tracker.dart';
import 'package:fluffychat/pangea/choreographer/models/it_step.dart';
import 'package:fluffychat/pangea/choreographer/repo/it_repo.dart';
@ -16,21 +13,20 @@ import 'package:fluffychat/widgets/future_loading_dialog.dart';
import 'package:fluffychat/widgets/matrix.dart';
import '../models/completed_it_step.dart';
import '../repo/it_request_model.dart';
import 'choreographer.dart';
class ITController {
final Choreographer _choreographer;
final ValueNotifier<String?> _sourceText = ValueNotifier(null);
final Function(Object) onError;
final ValueNotifier<ITStep?> _currentITStep = ValueNotifier(null);
final Queue<Completer<ITStep>> _queue = Queue();
GoldRouteTracker? _goldRouteTracker;
final ValueNotifier<String?> _sourceText = ValueNotifier(null);
final ValueNotifier<ITStep?> _currentITStep = ValueNotifier(null);
final ValueNotifier<bool> _open = ValueNotifier(false);
final ValueNotifier<bool> _editing = ValueNotifier(false);
bool _dismissed = false;
ITController(this._choreographer);
ITController(this.onError);
ValueNotifier<bool> get open => _open;
ValueNotifier<bool> get editing => _editing;
@ -47,8 +43,6 @@ class ITController {
MatrixState.pangeaController.languageController.activeL1Code()!,
targetLangCode:
MatrixState.pangeaController.languageController.activeL2Code()!,
userId: _choreographer.chatController.room.client.userID!,
roomId: _choreographer.chatController.room.id,
goldTranslation: _goldRouteTracker?.fullTranslation,
goldContinuances: _goldRouteTracker?.continuances,
);
@ -72,8 +66,6 @@ class ITController {
_queue.clear();
_currentITStep.value = null;
_goldRouteTracker = null;
_choreographer.setChoreoMode(ChoreoMode.igc);
}
void clearSourceText() {
@ -81,38 +73,30 @@ class ITController {
}
void dispose() {
_open.dispose();
_currentITStep.dispose();
_editing.dispose();
_sourceText.dispose();
}
void openIT(String sourceText) {
_sourceText.value = sourceText;
void openIT(String text) {
_sourceText.value = text;
_open.value = true;
continueIT();
}
void closeIT() {
// if the user hasn't gone through any IT steps, reset the text
if (_choreographer.currentText.isEmpty && _sourceText.value != null) {
_choreographer.textController.setSystemText(
_sourceText.value!,
EditType.itDismissed,
);
}
clear(dismissed: true);
}
void closeIT() => clear(dismissed: true);
void setEditing(bool value) {
_editing.value = value;
}
void onSubmitEdits() {
void onSubmitEdits(String text) {
_editing.value = false;
_queue.clear();
_currentITStep.value = null;
_goldRouteTracker = null;
_sourceText.value = text;
continueIT();
}
@ -163,13 +147,13 @@ class ITController {
if (_currentITStep.value == null) {
await _initTranslationData();
} else if (_queue.isEmpty) {
_choreographer.closeIT();
closeIT();
} else {
final nextStepCompleter = _queue.removeFirst();
_currentITStep.value = await nextStepCompleter.future;
}
} catch (e) {
_choreographer.errorService.setErrorAndLock(ChoreoError(raw: e));
onError(e);
} finally {
_continuing = false;
}
@ -179,9 +163,7 @@ class ITController {
final res = await _safeRequest("");
if (_sourceText.value == null || !_open.value) return;
if (res.isError || res.result?.goldContinuances == null) {
_choreographer.errorService.setErrorAndLock(
ChoreoError(raw: res.asError),
);
onError(res.asError!);
return;
}

View file

@ -8,8 +8,6 @@ class ITRequestModel {
final String customInput;
final String sourceLangCode;
final String targetLangCode;
final String userId;
final String roomId;
final String? goldTranslation;
final List<Continuance>? goldContinuances;
@ -19,8 +17,6 @@ class ITRequestModel {
required this.customInput,
required this.sourceLangCode,
required this.targetLangCode,
required this.userId,
required this.roomId,
required this.goldTranslation,
required this.goldContinuances,
});
@ -30,8 +26,6 @@ class ITRequestModel {
customInput: json['custom_input'],
sourceLangCode: json[ModelKey.srcLang],
targetLangCode: json[ModelKey.tgtLang],
userId: json['user_id'],
roomId: json['room_id'],
goldTranslation: json['gold_translation'],
goldContinuances: json['gold_continuances'] != null
? (json['gold_continuances'])
@ -45,8 +39,6 @@ class ITRequestModel {
'custom_input': customInput,
ModelKey.srcLang: sourceLangCode,
ModelKey.tgtLang: targetLangCode,
'user_id': userId,
'room_id': roomId,
'gold_translation': goldTranslation,
'gold_continuances': goldContinuances != null
? List.from(goldContinuances!.map((e) => e.toJson()))
@ -62,8 +54,6 @@ class ITRequestModel {
other.customInput == customInput &&
other.sourceLangCode == sourceLangCode &&
other.targetLangCode == targetLangCode &&
other.userId == userId &&
other.roomId == roomId &&
other.goldTranslation == goldTranslation &&
listEquals(other.goldContinuances, goldContinuances);
}
@ -74,8 +64,6 @@ class ITRequestModel {
customInput.hashCode ^
sourceLangCode.hashCode ^
targetLangCode.hashCode ^
userId.hashCode ^
roomId.hashCode ^
goldTranslation.hashCode ^
Object.hashAll(goldContinuances ?? []);
}

View file

@ -72,7 +72,7 @@ class SpanCardState extends State<SpanCard> {
try {
setState(() => _loadingChoices = true);
await widget.choreographer.fetchSpanDetails(
await widget.choreographer.igcController.fetchSpanDetails(
match: widget.match,
);
} catch (e) {
@ -83,7 +83,9 @@ class SpanCardState extends State<SpanCard> {
'No choices available for span ${widget.match.updatedMatch.match.message}',
);
}
setState(() => _loadingChoices = false);
if (mounted) {
setState(() => _loadingChoices = false);
}
}
}
@ -95,7 +97,7 @@ class SpanCardState extends State<SpanCard> {
try {
_feedbackModel.setState(FeedbackLoading<String>());
await widget.choreographer.fetchSpanDetails(
await widget.choreographer.igcController.fetchSpanDetails(
match: widget.match,
force: true,
);
@ -157,7 +159,7 @@ class SpanCardState extends State<SpanCard> {
void _showFirstMatch() {
if (widget.choreographer.canShowFirstIGCMatch) {
OverlayUtil.showIGCMatch(
widget.choreographer.igc.firstOpenMatch!,
widget.choreographer.igcController.firstOpenMatch!,
widget.choreographer,
context,
);

View file

@ -133,7 +133,8 @@ class ITBarState extends State<ITBar> with SingleTickerProviderStateMixin {
MatrixState.pAnyState.closeOverlay("it_feedback_card");
Continuance continuance;
try {
continuance = widget.choreographer.onSelectContinuance(index);
continuance =
widget.choreographer.itController.onSelectContinuance(index);
} catch (e, s) {
ErrorHandler.logError(
e: e,
@ -143,7 +144,7 @@ class ITBarState extends State<ITBar> with SingleTickerProviderStateMixin {
"index": index,
},
);
widget.choreographer.closeIT();
widget.choreographer.itController.closeIT();
return;
}
@ -174,7 +175,7 @@ class ITBarState extends State<ITBar> with SingleTickerProviderStateMixin {
"index": index,
},
);
widget.choreographer.closeIT();
widget.choreographer.itController.closeIT();
}
});
}
@ -217,12 +218,14 @@ class ITBarState extends State<ITBar> with SingleTickerProviderStateMixin {
spacing: 12.0,
children: [
_ITBarHeader(
onClose: widget.choreographer.closeIT,
onClose: widget.choreographer.itController.closeIT,
setEditing: widget.choreographer.itController.setEditing,
editing: widget.choreographer.itController.editing,
sourceTextController: _sourceTextController,
sourceText: _sourceText,
onSubmitEdits: widget.choreographer.submitSourceTextEdits,
onSubmitEdits: (_) => widget.choreographer.onSubmitEdits(
_sourceTextController.text,
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
@ -241,7 +244,8 @@ class ITBarState extends State<ITBar> with SingleTickerProviderStateMixin {
),
),
IconButton(
onPressed: widget.choreographer.closeIT,
onPressed:
widget.choreographer.itController.closeIT,
icon: const Icon(
Icons.close,
size: 20,

View file

@ -7,33 +7,31 @@ import 'package:fluffychat/pangea/choreographer/controllers/extensions/choreogra
import 'package:fluffychat/pangea/choreographer/enums/assistance_state_enum.dart';
import 'package:fluffychat/pangea/choreographer/widgets/igc/paywall_card.dart';
import 'package:fluffychat/pangea/common/utils/overlay.dart';
import '../../../pages/chat/chat.dart';
class ChoreographerSendButton extends StatelessWidget {
final Choreographer choreographer;
const ChoreographerSendButton({
super.key,
required this.controller,
required this.choreographer,
});
final ChatController controller;
Future<void> _onPressed(BuildContext context) async {
controller.choreographer.onClickSend();
choreographer.onClickSend();
try {
await controller.choreographer.send();
await choreographer.send();
} on ShowPaywallException {
PaywallCard.show(
context,
controller.choreographer.inputTransformTargetKey,
choreographer.inputTransformTargetKey,
);
} on OpenMatchesException {
if (controller.choreographer.firstOpenMatch != null) {
if (controller.choreographer.firstOpenMatch!.updatedMatch.isITStart) {
controller.choreographer
.openIT(controller.choreographer.firstOpenMatch!);
if (choreographer.firstOpenMatch != null) {
if (choreographer.firstOpenMatch!.updatedMatch.isITStart) {
choreographer.openIT(choreographer.firstOpenMatch!);
} else {
OverlayUtil.showIGCMatch(
controller.choreographer.firstOpenMatch!,
controller.choreographer,
choreographer.firstOpenMatch!,
choreographer,
context,
);
}
@ -45,8 +43,8 @@ class ChoreographerSendButton extends StatelessWidget {
Widget build(BuildContext context) {
return ListenableBuilder(
listenable: Listenable.merge([
controller.choreographer.textController,
controller.choreographer.isFetching,
choreographer.textController,
choreographer.isFetching,
]),
builder: (context, _) {
return Container(
@ -54,9 +52,8 @@ class ChoreographerSendButton extends StatelessWidget {
alignment: Alignment.center,
child: IconButton(
icon: const Icon(Icons.send_outlined),
color: controller.choreographer.assistanceState
.sendButtonColor(context),
onPressed: controller.choreographer.isFetching.value
color: choreographer.assistanceState.sendButtonColor(context),
onPressed: choreographer.isFetching.value
? null
: () => _onPressed(context),
tooltip: L10n.of(context).send,

View file

@ -63,7 +63,8 @@ class StartIGCButtonState extends State<StartIGCButton>
void _showFirstMatch() {
if (widget.controller.choreographer.canShowFirstIGCMatch) {
final match = widget.controller.choreographer.igc.firstOpenMatch;
final match =
widget.controller.choreographer.igcController.firstOpenMatch;
if (match == null) return;
if (match.updatedMatch.isITStart) {
widget.controller.choreographer.openIT(match);

View file

@ -22,6 +22,7 @@ import 'package:fluffychat/pangea/bot/utils/bot_room_extension.dart';
import 'package:fluffychat/pangea/chat/constants/default_power_level.dart';
import 'package:fluffychat/pangea/chat_settings/constants/bot_mode.dart';
import 'package:fluffychat/pangea/chat_settings/constants/pangea_room_types.dart';
import 'package:fluffychat/pangea/choreographer/repo/igc_request_model.dart';
import 'package:fluffychat/pangea/common/constants/model_keys.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/constructs/construct_identifier.dart';

View file

@ -385,4 +385,40 @@ extension EventsRoomExtension on Room {
return allPangeaMessages;
}
List<PreviousMessage> getPreviousMessages({int numMessages = 5}) {
if (timeline == null) return [];
final events = timeline!.events
.where(
(e) =>
e.type == EventTypes.Message &&
(e.messageType == MessageTypes.Text ||
e.messageType == MessageTypes.Audio),
)
.toList();
final List<PreviousMessage> messages = [];
for (final Event event in events) {
final String? content = event.messageType == MessageTypes.Text
? event.content.toString()
: PangeaMessageEvent(
event: event,
timeline: timeline!,
ownMessage: event.senderId == client.userID,
).getSpeechToTextLocal()?.transcript.text.trim();
if (content == null) continue;
messages.add(
PreviousMessage(
content: content,
sender: event.senderId,
timestamp: event.originServerTs,
),
);
if (messages.length >= numMessages) {
return messages;
}
}
return messages;
}
}