display, interactivity, saving/fetching of record, and dummy generation all done
parent
1dcd988be0
commit
91d7600c5d
@ -0,0 +1,101 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:fluffychat/pangea/constants/pangea_event_types.dart';
|
||||
import 'package:fluffychat/pangea/enum/construct_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/practice_activity_event.dart';
|
||||
import 'package:fluffychat/pangea/models/practice_activities.dart/multiple_choice_activity_model.dart';
|
||||
import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
/// Represents an item in the completion cache.
|
||||
class _RequestCacheItem {
|
||||
PracticeActivityRequest req;
|
||||
|
||||
Future<PracticeActivityEvent?> practiceActivityEvent;
|
||||
|
||||
_RequestCacheItem({
|
||||
required this.req,
|
||||
required this.practiceActivityEvent,
|
||||
});
|
||||
}
|
||||
|
||||
/// Controller for handling activity completions.
|
||||
class PracticeGenerationController {
|
||||
static final Map<int, _RequestCacheItem> _cache = {};
|
||||
Timer? _cacheClearTimer;
|
||||
|
||||
PracticeGenerationController() {
|
||||
_initializeCacheClearing();
|
||||
}
|
||||
|
||||
void _initializeCacheClearing() {
|
||||
const duration = Duration(minutes: 2);
|
||||
_cacheClearTimer = Timer.periodic(duration, (Timer t) => _clearCache());
|
||||
}
|
||||
|
||||
void _clearCache() {
|
||||
_cache.clear();
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
_cacheClearTimer?.cancel();
|
||||
}
|
||||
|
||||
Future<PracticeActivityEvent?> _sendAndPackageEvent(
|
||||
PracticeActivityModel model,
|
||||
PangeaMessageEvent pangeaMessageEvent,
|
||||
) async {
|
||||
final Event? activityEvent = await pangeaMessageEvent.room.sendPangeaEvent(
|
||||
content: model.toJson(),
|
||||
parentEventId: pangeaMessageEvent.eventId,
|
||||
type: PangeaEventTypes.pangeaActivityRes,
|
||||
);
|
||||
|
||||
if (activityEvent == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return PracticeActivityEvent(
|
||||
event: activityEvent,
|
||||
timeline: pangeaMessageEvent.timeline,
|
||||
);
|
||||
}
|
||||
|
||||
Future<PracticeActivityEvent?> getPracticeActivity(
|
||||
PracticeActivityRequest req,
|
||||
PangeaMessageEvent event,
|
||||
) async {
|
||||
final int cacheKey = req.hashCode;
|
||||
|
||||
if (_cache.containsKey(cacheKey)) {
|
||||
return _cache[cacheKey]!.practiceActivityEvent;
|
||||
} else {
|
||||
//TODO - send request to server/bot, either via API or via event of type pangeaActivityReq
|
||||
// for now, just make and send the event from the client
|
||||
final Future<PracticeActivityEvent?> eventFuture =
|
||||
_sendAndPackageEvent(dummyModel(event), event);
|
||||
|
||||
_cache[cacheKey] =
|
||||
_RequestCacheItem(req: req, practiceActivityEvent: eventFuture);
|
||||
|
||||
return _cache[cacheKey]!.practiceActivityEvent;
|
||||
}
|
||||
}
|
||||
|
||||
PracticeActivityModel dummyModel(PangeaMessageEvent event) =>
|
||||
PracticeActivityModel(
|
||||
tgtConstructs: [
|
||||
ConstructIdentifier(lemma: "be", type: ConstructType.vocab.string),
|
||||
],
|
||||
activityType: ActivityType.multipleChoice,
|
||||
langCode: event.messageDisplayLangCode,
|
||||
msgId: event.eventId,
|
||||
multipleChoice: MultipleChoice(
|
||||
question: "What is a synonym for 'happy'?",
|
||||
choices: ["sad", "angry", "joyful", "tired"],
|
||||
correctAnswer: "joyful",
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,94 @@
|
||||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:fluffychat/pangea/constants/pangea_event_types.dart';
|
||||
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/practice_activity_event.dart';
|
||||
import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_record_model.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
/// Represents an item in the completion cache.
|
||||
class _RecordCacheItem {
|
||||
PracticeActivityRecordModel data;
|
||||
|
||||
Future<Event?> recordEvent;
|
||||
|
||||
_RecordCacheItem({required this.data, required this.recordEvent});
|
||||
}
|
||||
|
||||
/// Controller for handling activity completions.
|
||||
class PracticeActivityRecordController {
|
||||
static final Map<int, _RecordCacheItem> _cache = {};
|
||||
late final PangeaController _pangeaController;
|
||||
Timer? _cacheClearTimer;
|
||||
|
||||
PracticeActivityRecordController(this._pangeaController) {
|
||||
_initializeCacheClearing();
|
||||
}
|
||||
|
||||
void _initializeCacheClearing() {
|
||||
const duration = Duration(minutes: 2);
|
||||
_cacheClearTimer = Timer.periodic(duration, (Timer t) => _clearCache());
|
||||
}
|
||||
|
||||
void _clearCache() {
|
||||
_cache.clear();
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
_cacheClearTimer?.cancel();
|
||||
}
|
||||
|
||||
/// Sends a practice activity record to the server and returns the corresponding event.
|
||||
///
|
||||
/// The [recordModel] parameter is the model representing the practice activity record.
|
||||
/// The [practiceActivityEvent] parameter is the event associated with the practice activity.
|
||||
/// Note that the system will send a new event if the model has changed in any way ie it is
|
||||
/// a new completion of the practice activity. However, it will cache previous sends to ensure
|
||||
/// that opening and closing of the widget does not result in multiple sends of the same data.
|
||||
/// It allows checks the data to make sure that it contains responses to the practice activity
|
||||
/// and does not represent a blank record with no actual completion to be saved.
|
||||
///
|
||||
/// Returns a [Future] that completes with the corresponding [Event] object.
|
||||
Future<Event?> send(
|
||||
PracticeActivityRecordModel recordModel,
|
||||
PracticeActivityEvent practiceActivityEvent,
|
||||
) async {
|
||||
final int cacheKey = recordModel.hashCode;
|
||||
|
||||
if (recordModel.responses.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (_cache.containsKey(cacheKey)) {
|
||||
return _cache[cacheKey]!.recordEvent;
|
||||
} else {
|
||||
final Future<Event?> eventFuture = practiceActivityEvent.event.room
|
||||
.sendPangeaEvent(
|
||||
content: recordModel.toJson(),
|
||||
parentEventId: practiceActivityEvent.event.eventId,
|
||||
type: PangeaEventTypes.activityRecord,
|
||||
)
|
||||
.catchError((e) {
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(
|
||||
e: e,
|
||||
s: StackTrace.current,
|
||||
data: {
|
||||
'recordModel': recordModel.toJson(),
|
||||
'practiceActivityEvent': practiceActivityEvent.event.toJson(),
|
||||
},
|
||||
);
|
||||
return null;
|
||||
});
|
||||
|
||||
_cache[cacheKey] =
|
||||
_RecordCacheItem(data: recordModel, recordEvent: eventFuture);
|
||||
|
||||
return _cache[cacheKey]!.recordEvent;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,9 +0,0 @@
|
||||
// relates to a pangea representation event
|
||||
// the matrix even fits the form of a regular matrix audio event
|
||||
// but with something to distinguish it as a pangea audio event
|
||||
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
class PangeaAudioEvent {
|
||||
Event? _event;
|
||||
}
|
||||
@ -0,0 +1,24 @@
|
||||
import 'package:fluffychat/pangea/extensions/pangea_event_extension.dart';
|
||||
import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_record_model.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import '../constants/pangea_event_types.dart';
|
||||
|
||||
class PracticeActivityRecordEvent {
|
||||
Event event;
|
||||
|
||||
PracticeActivityRecordModel? _content;
|
||||
|
||||
PracticeActivityRecordEvent({required this.event}) {
|
||||
if (event.type != PangeaEventTypes.activityRecord) {
|
||||
throw Exception(
|
||||
"${event.type} should not be used to make a PracticeActivityRecordEvent",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
PracticeActivityRecordModel? get record {
|
||||
_content ??= event.getPangeaContent<PracticeActivityRecordModel>();
|
||||
return _content!;
|
||||
}
|
||||
}
|
||||
@ -1,29 +1,67 @@
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:fluffychat/pangea/extensions/pangea_event_extension.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/practice_acitivity_record_event.dart';
|
||||
import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import '../constants/pangea_event_types.dart';
|
||||
|
||||
class PracticeActivityEvent {
|
||||
Event event;
|
||||
Timeline? timeline;
|
||||
PracticeActivityModel? _content;
|
||||
|
||||
PracticeActivityEvent({required this.event}) {
|
||||
if (event.type != PangeaEventTypes.activityResponse) {
|
||||
PracticeActivityEvent({
|
||||
required this.event,
|
||||
required this.timeline,
|
||||
content,
|
||||
}) {
|
||||
if (content != null) {
|
||||
if (!kDebugMode) {
|
||||
throw Exception(
|
||||
"content should not be set on product, just a dev placeholder",
|
||||
);
|
||||
} else {
|
||||
_content = content;
|
||||
}
|
||||
}
|
||||
if (event.type != PangeaEventTypes.pangeaActivityRes) {
|
||||
throw Exception(
|
||||
"${event.type} should not be used to make a PracticeActivityEvent",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
PracticeActivityModel? get practiceActivity {
|
||||
try {
|
||||
_content ??= event.getPangeaContent<PracticeActivityModel>();
|
||||
return _content!;
|
||||
} catch (err, s) {
|
||||
ErrorHandler.logError(e: err, s: s);
|
||||
return null;
|
||||
PracticeActivityModel get practiceActivity {
|
||||
_content ??= event.getPangeaContent<PracticeActivityModel>();
|
||||
return _content!;
|
||||
}
|
||||
|
||||
//in aggregatedEvents for the event, find all practiceActivityRecordEvents whose sender matches the client's userId
|
||||
List<PracticeActivityRecordEvent> get allRecords {
|
||||
if (timeline == null) {
|
||||
debugger(when: kDebugMode);
|
||||
return [];
|
||||
}
|
||||
final List<Event> records = event
|
||||
.aggregatedEvents(timeline!, PangeaEventTypes.activityRecord)
|
||||
.toList();
|
||||
|
||||
return records
|
||||
.map((event) => PracticeActivityRecordEvent(event: event))
|
||||
.toList();
|
||||
}
|
||||
|
||||
List<PracticeActivityRecordEvent> get userRecords => allRecords
|
||||
.where(
|
||||
(recordEvent) =>
|
||||
recordEvent.event.senderId == recordEvent.event.room.client.userID,
|
||||
)
|
||||
.toList();
|
||||
|
||||
/// Checks if there are any user records in the list for this activity,
|
||||
/// and, if so, then the activity is complete
|
||||
bool get isComplete => userRecords.isNotEmpty;
|
||||
}
|
||||
|
||||
@ -0,0 +1,127 @@
|
||||
// record the options that the user selected
|
||||
// note that this is not the same as the correct answer
|
||||
// the user might have selected multiple options before
|
||||
// finding the answer
|
||||
import 'dart:developer';
|
||||
import 'dart:typed_data';
|
||||
|
||||
class PracticeActivityRecordModel {
|
||||
final String? question;
|
||||
late List<ActivityResponse> responses;
|
||||
|
||||
PracticeActivityRecordModel({
|
||||
required this.question,
|
||||
List<ActivityResponse>? responses,
|
||||
}) {
|
||||
if (responses == null) {
|
||||
this.responses = List<ActivityResponse>.empty(growable: true);
|
||||
} else {
|
||||
this.responses = responses;
|
||||
}
|
||||
}
|
||||
|
||||
factory PracticeActivityRecordModel.fromJson(
|
||||
Map<String, dynamic> json,
|
||||
) {
|
||||
return PracticeActivityRecordModel(
|
||||
question: json['question'] as String,
|
||||
responses: (json['responses'] as List)
|
||||
.map((e) => ActivityResponse.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'question': question,
|
||||
'responses': responses.map((e) => e.toJson()).toList(),
|
||||
};
|
||||
}
|
||||
|
||||
void addResponse({
|
||||
String? text,
|
||||
Uint8List? audioBytes,
|
||||
Uint8List? imageBytes,
|
||||
}) {
|
||||
try {
|
||||
responses.add(
|
||||
ActivityResponse(
|
||||
text: text,
|
||||
audioBytes: audioBytes,
|
||||
imageBytes: imageBytes,
|
||||
timestamp: DateTime.now(),
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
debugger();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other is PracticeActivityRecordModel &&
|
||||
other.question == question &&
|
||||
other.responses.length == responses.length &&
|
||||
List.generate(
|
||||
responses.length,
|
||||
(index) => responses[index] == other.responses[index],
|
||||
).every((element) => element);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => question.hashCode ^ responses.hashCode;
|
||||
}
|
||||
|
||||
class ActivityResponse {
|
||||
// the user's response
|
||||
// has nullable string, nullable audio bytes, nullable image bytes, and timestamp
|
||||
final String? text;
|
||||
final Uint8List? audioBytes;
|
||||
final Uint8List? imageBytes;
|
||||
final DateTime timestamp;
|
||||
|
||||
ActivityResponse({
|
||||
this.text,
|
||||
this.audioBytes,
|
||||
this.imageBytes,
|
||||
required this.timestamp,
|
||||
});
|
||||
|
||||
factory ActivityResponse.fromJson(Map<String, dynamic> json) {
|
||||
return ActivityResponse(
|
||||
text: json['text'] as String?,
|
||||
audioBytes: json['audio'] as Uint8List?,
|
||||
imageBytes: json['image'] as Uint8List?,
|
||||
timestamp: DateTime.parse(json['timestamp'] as String),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'text': text,
|
||||
'audio': audioBytes,
|
||||
'image': imageBytes,
|
||||
'timestamp': timestamp.toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other is ActivityResponse &&
|
||||
other.text == text &&
|
||||
other.audioBytes == audioBytes &&
|
||||
other.imageBytes == imageBytes &&
|
||||
other.timestamp == timestamp;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
text.hashCode ^
|
||||
audioBytes.hashCode ^
|
||||
imageBytes.hashCode ^
|
||||
timestamp.hashCode;
|
||||
}
|
||||
@ -0,0 +1,96 @@
|
||||
import 'package:fluffychat/pangea/enum/message_mode_enum.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_toolbar.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class MessageButtons extends StatelessWidget {
|
||||
final ToolbarDisplayController? toolbarController;
|
||||
|
||||
const MessageButtons({
|
||||
super.key,
|
||||
this.toolbarController,
|
||||
});
|
||||
|
||||
void showActivity(BuildContext context) {
|
||||
toolbarController?.showToolbar(
|
||||
context,
|
||||
mode: MessageMode.practiceActivity,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (toolbarController == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 8.0),
|
||||
child: Row(
|
||||
children: [
|
||||
HoverIconButton(
|
||||
icon: MessageMode.practiceActivity.icon,
|
||||
onTap: () => showActivity(context),
|
||||
primaryColor: Theme.of(context).colorScheme.primary,
|
||||
tooltip: MessageMode.practiceActivity.tooltip(context),
|
||||
),
|
||||
|
||||
// Additional buttons can be added here in the future
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class HoverIconButton extends StatefulWidget {
|
||||
final IconData icon;
|
||||
final VoidCallback onTap;
|
||||
final Color primaryColor;
|
||||
final String tooltip;
|
||||
|
||||
const HoverIconButton({
|
||||
super.key,
|
||||
required this.icon,
|
||||
required this.onTap,
|
||||
required this.primaryColor,
|
||||
required this.tooltip,
|
||||
});
|
||||
|
||||
@override
|
||||
_HoverIconButtonState createState() => _HoverIconButtonState();
|
||||
}
|
||||
|
||||
class _HoverIconButtonState extends State<HoverIconButton> {
|
||||
bool _isHovered = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Tooltip(
|
||||
message: widget.tooltip,
|
||||
child: InkWell(
|
||||
onTap: widget.onTap,
|
||||
onHover: (hovering) {
|
||||
setState(() => _isHovered = hovering);
|
||||
},
|
||||
borderRadius: BorderRadius.circular(100),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: _isHovered ? widget.primaryColor : null,
|
||||
borderRadius: BorderRadius.circular(100),
|
||||
border: Border.all(
|
||||
width: 1,
|
||||
color: widget.primaryColor,
|
||||
),
|
||||
),
|
||||
padding: const EdgeInsets.all(2),
|
||||
child: Icon(
|
||||
widget.icon,
|
||||
size: 18,
|
||||
// when hovered, use themeData to get background color, otherwise use primary
|
||||
color: _isHovered
|
||||
? Theme.of(context).scaffoldBackgroundColor
|
||||
: widget.primaryColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,60 @@
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/practice_activity_event.dart';
|
||||
import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
class GeneratePracticeActivityButton extends StatelessWidget {
|
||||
final PangeaMessageEvent pangeaMessageEvent;
|
||||
final Function(PracticeActivityEvent?) onActivityGenerated;
|
||||
|
||||
const GeneratePracticeActivityButton({
|
||||
super.key,
|
||||
required this.pangeaMessageEvent,
|
||||
required this.onActivityGenerated,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ElevatedButton(
|
||||
onPressed: () async {
|
||||
final String? l2Code = MatrixState.pangeaController.languageController
|
||||
.activeL1Model(roomID: pangeaMessageEvent.room.id)
|
||||
?.langCode;
|
||||
|
||||
if (l2Code == null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(L10n.of(context)!.noLanguagesSet),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final PracticeActivityEvent? practiceActivityEvent = await MatrixState
|
||||
.pangeaController.practiceGenerationController
|
||||
.getPracticeActivity(
|
||||
PracticeActivityRequest(
|
||||
candidateMessages: [
|
||||
CandidateMessage(
|
||||
msgId: pangeaMessageEvent.eventId,
|
||||
roomId: pangeaMessageEvent.room.id,
|
||||
text:
|
||||
pangeaMessageEvent.representationByLanguage(l2Code)?.text ??
|
||||
pangeaMessageEvent.body,
|
||||
),
|
||||
],
|
||||
userIds: pangeaMessageEvent.room.client.userID != null
|
||||
? [pangeaMessageEvent.room.client.userID!]
|
||||
: null,
|
||||
),
|
||||
pangeaMessageEvent,
|
||||
);
|
||||
|
||||
onActivityGenerated(practiceActivityEvent);
|
||||
},
|
||||
child: Text(L10n.of(context)!.practice),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,141 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/practice_acitivity_record_event.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/practice_activity_event.dart';
|
||||
import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart';
|
||||
import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_record_model.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/widgets/practice_activity_card/multiple_choice_activity.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
class PracticeActivityContent extends StatefulWidget {
|
||||
final PracticeActivityEvent practiceEvent;
|
||||
final PangeaMessageEvent pangeaMessageEvent;
|
||||
|
||||
const PracticeActivityContent({
|
||||
super.key,
|
||||
required this.practiceEvent,
|
||||
required this.pangeaMessageEvent,
|
||||
});
|
||||
|
||||
@override
|
||||
MessagePracticeActivityContentState createState() =>
|
||||
MessagePracticeActivityContentState();
|
||||
}
|
||||
|
||||
class MessagePracticeActivityContentState
|
||||
extends State<PracticeActivityContent> {
|
||||
int? selectedChoiceIndex;
|
||||
PracticeActivityRecordModel? recordModel;
|
||||
bool recordSubmittedThisSession = false;
|
||||
bool recordSubmittedPreviousSession = false;
|
||||
|
||||
PracticeActivityEvent get practiceEvent => widget.practiceEvent;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final PracticeActivityRecordEvent? recordEvent =
|
||||
widget.practiceEvent.userRecords.firstOrNull;
|
||||
if (recordEvent?.record == null) {
|
||||
recordModel = PracticeActivityRecordModel(
|
||||
question:
|
||||
widget.practiceEvent.practiceActivity.multipleChoice!.question,
|
||||
);
|
||||
} else {
|
||||
recordModel = recordEvent!.record;
|
||||
recordSubmittedPreviousSession = true;
|
||||
recordSubmittedThisSession = true;
|
||||
}
|
||||
}
|
||||
|
||||
void updateChoice(int index) {
|
||||
setState(() {
|
||||
selectedChoiceIndex = index;
|
||||
recordModel!.addResponse(
|
||||
text: widget
|
||||
.practiceEvent.practiceActivity.multipleChoice!.choices[index],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Widget get activityWidget {
|
||||
switch (widget.practiceEvent.practiceActivity.activityType) {
|
||||
case ActivityType.multipleChoice:
|
||||
return MultipleChoiceActivity(
|
||||
card: this,
|
||||
updateChoice: updateChoice,
|
||||
isActive:
|
||||
!recordSubmittedPreviousSession && !recordSubmittedThisSession,
|
||||
);
|
||||
default:
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
}
|
||||
|
||||
void sendRecord() {
|
||||
MatrixState.pangeaController.activityRecordController
|
||||
.send(
|
||||
recordModel!,
|
||||
widget.practiceEvent,
|
||||
)
|
||||
.catchError((error) {
|
||||
ErrorHandler.logError(
|
||||
e: error,
|
||||
s: StackTrace.current,
|
||||
data: {
|
||||
'recordModel': recordModel?.toJson(),
|
||||
'practiceEvent': widget.practiceEvent.event.toJson(),
|
||||
},
|
||||
);
|
||||
return null;
|
||||
});
|
||||
|
||||
setState(() {
|
||||
recordSubmittedThisSession = true;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
debugPrint(
|
||||
"MessagePracticeActivityContentState.build with selectedChoiceIndex: $selectedChoiceIndex",
|
||||
);
|
||||
return Column(
|
||||
children: [
|
||||
activityWidget,
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Opacity(
|
||||
opacity: selectedChoiceIndex != null &&
|
||||
!recordSubmittedThisSession &&
|
||||
!recordSubmittedPreviousSession
|
||||
? 1.0
|
||||
: 0.5,
|
||||
child: TextButton(
|
||||
onPressed: () {
|
||||
if (recordSubmittedThisSession ||
|
||||
recordSubmittedPreviousSession) {
|
||||
return;
|
||||
}
|
||||
selectedChoiceIndex != null ? sendRecord() : null;
|
||||
},
|
||||
style: ButtonStyle(
|
||||
backgroundColor: WidgetStateProperty.all<Color>(
|
||||
AppConfig.primaryColor,
|
||||
),
|
||||
),
|
||||
child: Text(L10n.of(context)!.submit),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue