/// 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.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; /// 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 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?, needsPrepare: json['needsPrepare'] as bool? ?? false, ); } IncomingQuestion copyWith({ String? text, String? spokenWord, String? audioTitle, 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, needsPrepare: needsPrepare ?? this.needsPrepare, ); } }