cyberhybridhub/lib/models/incoming_question.dart
2026-06-05 15:10:03 +00:00

111 lines
3.3 KiB
Dart

/// Question pushed from the API over the SignalR questions hub.
class IncomingQuestion {
const IncomingQuestion({
required this.id,
required this.text,
required this.sentAt,
this.unansweredCount = 1,
this.sourceId = defaultSourceId,
this.presentation = defaultPresentation,
this.spokenWord,
this.audioTitle,
this.canonicalWord,
this.needsPrepare = false,
});
static const String defaultSourceId = 'market_history';
static const String defaultPresentation = 'market_glyph_slider';
final String id;
final String text;
final DateTime sentAt;
/// Unanswered questions in the database for this user (from API).
final int unansweredCount;
/// Question source (`market_history`, `spelling`, …).
final String sourceId;
/// UI presentation key (`market_glyph_slider`, `spelling_word`, …).
final String presentation;
/// Word read aloud after server-side selection at tile load.
final String? spokenWord;
/// Question id referenced as the audio utterance title.
final String? audioTitle;
/// Proper spelling used to grade the typed answer.
final String? canonicalWord;
/// True when the server still needs tile-load word selection.
final bool needsPrepare;
/// Spelling pipeline source (word quiz and follow-up prompts).
bool get isSpelling => sourceId == 'spelling';
/// Typed spelling tile (hear word, type answer).
bool get usesSpellingWordTile {
if (presentation == 'spelling_word' || needsPrepare) {
return true;
}
if (!isSpelling) {
return false;
}
if (spokenWord != null && spokenWord!.isNotEmpty) {
return true;
}
return !isSpellingFollowUp;
}
/// Spelling branch prompt to continue (+1) or pause (-1); uses swipe UI.
bool get isSpellingFollowUp {
if (!isSpelling || presentation == 'spelling_word') {
return false;
}
final String lower = text.toLowerCase();
return lower.contains('enter +1') ||
lower.contains('another word') ||
lower.contains('to pause');
}
bool get isMarketHistory => sourceId == 'market_history';
factory IncomingQuestion.fromJson(Map<String, dynamic> json) {
return IncomingQuestion(
id: json['id'] as String,
text: json['text'] as String,
sentAt: DateTime.parse(json['sentAt'] as String).toUtc(),
unansweredCount: (json['unansweredCount'] as num?)?.toInt() ?? 1,
sourceId: json['sourceId'] as String? ?? defaultSourceId,
presentation: json['presentation'] as String? ?? defaultPresentation,
spokenWord: json['spokenWord'] as String?,
audioTitle: json['audioTitle'] as String?,
canonicalWord: json['canonicalWord'] as String?,
needsPrepare: json['needsPrepare'] as bool? ?? false,
);
}
IncomingQuestion copyWith({
String? text,
String? spokenWord,
String? audioTitle,
String? canonicalWord,
int? unansweredCount,
bool? needsPrepare,
}) {
return IncomingQuestion(
id: id,
text: text ?? this.text,
sentAt: sentAt,
unansweredCount: unansweredCount ?? this.unansweredCount,
sourceId: sourceId,
presentation: presentation,
spokenWord: spokenWord ?? this.spokenWord,
audioTitle: audioTitle ?? this.audioTitle,
canonicalWord: canonicalWord ?? this.canonicalWord,
needsPrepare: needsPrepare ?? this.needsPrepare,
);
}
}