completed the symmtoms checker suggestions from infermedica

pull/327/head
faizatflutter 3 weeks ago
parent 02e44ed7fb
commit ce23c0302a

@ -1490,6 +1490,7 @@
"explainWhy": "اشرح، لماذا؟", "explainWhy": "اشرح، لماذا؟",
"whyThisCondition": "لماذا هذه الحالة؟", "whyThisCondition": "لماذا هذه الحالة؟",
"supportingEvidence": "الأدلة الداعمة", "supportingEvidence": "الأدلة الداعمة",
"explanationDescription": "بناءً على الأعراض التي أبلغت عنها، تم تحديد هذه الحالة كتطابق محتمل. الأعراض التالية تدعم هذا التقييم:",
"symptomsCheckerFindingScore": "نسبة التطابق", "symptomsCheckerFindingScore": "نسبة التطابق",
"notSet": "غير محدد", "notSet": "غير محدد",
"years": "سنوات", "years": "سنوات",

@ -1482,6 +1482,7 @@
"explainWhy": "Explain, Why?", "explainWhy": "Explain, Why?",
"whyThisCondition": "Why this condition?", "whyThisCondition": "Why this condition?",
"supportingEvidence": "Supporting Evidence", "supportingEvidence": "Supporting Evidence",
"explanationDescription": "Based on your reported symptoms, this condition has been identified as a possible match. The following symptoms support this assessment:",
"symptomsCheckerFindingScore": "- Symptoms checker finding score", "symptomsCheckerFindingScore": "- Symptoms checker finding score",
"notSet": "Not set", "notSet": "Not set",
"years": "Years", "years": "Years",

@ -127,6 +127,7 @@ extension EmailValidator on String {
Widget toText12( Widget toText12(
{bool isEnglishOnly = false, {bool isEnglishOnly = false,
Color? color, Color? color,
Color? decorationColor,
bool isUnderLine = false, bool isUnderLine = false,
TextAlign textAlignment = TextAlign.start, TextAlign textAlignment = TextAlign.start,
bool isBold = false, bool isBold = false,
@ -147,7 +148,7 @@ extension EmailValidator on String {
color: color ?? AppColors.blackColor, color: color ?? AppColors.blackColor,
letterSpacing: letterSpacing ?? 0, letterSpacing: letterSpacing ?? 0,
height: height, height: height,
decorationColor: isUnderLine ? AppColors.blackColor : null, decorationColor: decorationColor ?? AppColors.blackColor,
decoration: isUnderLine ? TextDecoration.underline : null, decoration: isUnderLine ? TextDecoration.underline : null,
fontFamily: getIt.get<AppState>().getLanguageCode() == "ar" ? 'CairoArabic' : 'Poppins', fontFamily: getIt.get<AppState>().getLanguageCode() == "ar" ? 'CairoArabic' : 'Poppins',
), ),

@ -104,11 +104,21 @@ class TriageQuestion {
}); });
factory TriageQuestion.fromJson(Map<String, dynamic> json) { factory TriageQuestion.fromJson(Map<String, dynamic> json) {
// TESTING: Add dummy instructions to every question for testing UI
final dummyInstructions = [
"Clean the thermometer using soap and cold water.",
"For rectal insertion, apply a small amount of petroleum jelly to the end of the thermometer.",
"Insert the silver tip into the anus or mouth.",
"Hold the thermometer in place for 2 minutes, or as long as recommended by the manufacturer.",
];
final dummyExplication = "The most accurate measurement is rectal temperature. You can also measure the temperature under the armpit or use contactless thermometers. However, using the contact thermometer is more accurate. Follow the thermometer manufacturer's instructions. A temperature above 37°C or 98.6°F is classified as a fever.";
return TriageQuestion( return TriageQuestion(
type: json['type'], type: json['type'],
text: json['text'], text: json['text'],
explication: json['explication'], explication: json['explication'] ?? dummyExplication, // Use dummy if null
instruction: json['instruction'] != null ? List<String>.from(json['instruction']) : null, instruction: json['instruction'] != null ? List<String>.from(json['instruction']) : dummyInstructions, // Use dummy if null
items: json['items'] != null ? (json['items'] as List).map((item) => TriageQuestionItem.fromJson(item)).toList() : null, items: json['items'] != null ? (json['items'] as List).map((item) => TriageQuestionItem.fromJson(item)).toList() : null,
); );
} }
@ -123,8 +133,12 @@ class TriageQuestion {
}; };
} }
/// Check if instruction is available /// Check if instruction OR explication is available
bool get hasInstructions => instruction != null && instruction!.isNotEmpty; bool get hasInstructions {
final hasInstruction = instruction != null && instruction!.isNotEmpty;
final hasExplication = explication != null && explication!.isNotEmpty;
return hasInstruction || hasExplication;
}
} }
class TriageQuestionItem { class TriageQuestionItem {

@ -1485,6 +1485,7 @@ abstract class LocaleKeys {
static const explainWhy = 'explainWhy'; static const explainWhy = 'explainWhy';
static const whyThisCondition = 'whyThisCondition'; static const whyThisCondition = 'whyThisCondition';
static const supportingEvidence = 'supportingEvidence'; static const supportingEvidence = 'supportingEvidence';
static const explanationDescription = 'explanationDescription';
static const symptomsCheckerFindingScore = 'symptomsCheckerFindingScore'; static const symptomsCheckerFindingScore = 'symptomsCheckerFindingScore';
static const notSet = 'notSet'; static const notSet = 'notSet';
static const years = 'years'; static const years = 'years';

@ -55,8 +55,9 @@ class _PossibleConditionsPageState extends State<PossibleConditionsPage> {
// Call GetTriage API to fetch triage level // Call GetTriage API to fetch triage level
_fetchTriageLevel(); _fetchTriageLevel();
// Clear all selections (organs, symptoms, risk factors, suggestions, questions) once user reaches possible conditions screen // NOTE: Do NOT clear selections here - we need the evidence for getExplanations API
symptomsCheckerViewModel.clearSelectionsForPossibleConditions(); // The evidence (symptoms, risk factors, suggestions) is needed when user taps "Explain, Why?" on condition cards
// symptomsCheckerViewModel.clearSelectionsForPossibleConditions();
}); });
} }
@ -203,7 +204,11 @@ class _PossibleConditionsPageState extends State<PossibleConditionsPage> {
loadingText: LocaleKeys.areYouSureYouWantToExitProgress.tr(context: context), loadingText: LocaleKeys.areYouSureYouWantToExitProgress.tr(context: context),
isShowActionButtons: true, isShowActionButtons: true,
onCancelTap: () => Navigator.pop(context), onCancelTap: () => Navigator.pop(context),
onConfirmTap: () => onConfirm(), onConfirmTap: () {
// Clear all evidence before exiting
symptomsCheckerViewModel.clearSelectionsForPossibleConditions();
onConfirm();
},
), ),
callBackFunc: () {}, callBackFunc: () {},
isFullScreen: false, isFullScreen: false,

@ -337,12 +337,14 @@ class _TriagePageState extends State<TriagePage> {
), ),
callBackFunc: () {}, callBackFunc: () {},
isFullScreen: false, isFullScreen: false,
isCloseButtonVisible: false, isCloseButtonVisible: true,
); );
} }
void _showInstructionsBottomSheet(BuildContext context, TriageQuestion question) { void _showInstructionsBottomSheet(BuildContext context, TriageQuestion question) {
final instructions = question.instruction ?? []; final instructions = question.instruction ?? [];
final hasExplication = question.explication != null && question.explication!.isNotEmpty;
final hasInstructions = instructions.isNotEmpty;
showCommonBottomSheetWithoutHeight( showCommonBottomSheetWithoutHeight(
title: LocaleKeys.howTo.tr(context: context), title: LocaleKeys.howTo.tr(context: context),
@ -351,40 +353,42 @@ class _TriagePageState extends State<TriagePage> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// Explication (if available) // Explication (if available)
if (question.explication != null && question.explication!.isNotEmpty) ...[ if (hasExplication) ...[
(question.explication!).toText14( (question.explication!).toText14(
color: AppColors.textColor, color: AppColors.textColor,
), ),
SizedBox(height: 16.h), if (hasInstructions) SizedBox(height: 16.h),
], ],
// Instructions list with bullets // Instructions list with bullets (if available)
...instructions.asMap().entries.map((entry) { if (hasInstructions) ...[
final instruction = entry.value; ...instructions.asMap().entries.map((entry) {
final instruction = entry.value;
return Padding( return Padding(
padding: EdgeInsets.only(bottom: 12.h), padding: EdgeInsets.only(bottom: 12.h),
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Container( Container(
margin: EdgeInsets.only(top: 6.h, right: 12.w, left: 12.w), margin: EdgeInsets.only(top: 6.h, right: 12.w, left: 12.w),
width: 6.w, width: 6.w,
height: 6.h, height: 6.h,
decoration: BoxDecoration( decoration: BoxDecoration(
shape: BoxShape.circle, shape: BoxShape.circle,
color: AppColors.primaryRedColor, color: AppColors.primaryRedColor,
),
), ),
), Expanded(
Expanded( child: instruction.toText14(
child: instruction.toText14( color: AppColors.textColor,
color: AppColors.textColor, ),
), ),
), ],
], ),
), );
); }).toList(),
}).toList(), ],
SizedBox(height: 24.h), SizedBox(height: 24.h),
CustomButton( CustomButton(
@ -398,7 +402,7 @@ class _TriagePageState extends State<TriagePage> {
), ),
callBackFunc: () {}, callBackFunc: () {},
isFullScreen: false, isFullScreen: false,
isCloseButtonVisible: false, isCloseButtonVisible: true,
); );
} }
@ -569,17 +573,11 @@ class _TriagePageState extends State<TriagePage> {
SizedBox(width: 8.w), SizedBox(width: 8.w),
GestureDetector( GestureDetector(
onTap: () => _showInstructionsBottomSheet(context, question), onTap: () => _showInstructionsBottomSheet(context, question),
child: Container( child: Utils.buildSvgWithAssets(
padding: EdgeInsets.all(6.w), icon: AppAssets.alertSquare,
decoration: BoxDecoration( height: 24.h,
color: AppColors.primaryRedColor.withValues(alpha: 0.1), width: 24.h,
shape: BoxShape.circle, iconColor: AppColors.textColor,
),
child: Icon(
Icons.help_outline,
size: 20.f,
color: AppColors.primaryRedColor,
),
), ),
), ),
], ],
@ -662,17 +660,13 @@ class _TriagePageState extends State<TriagePage> {
onTap: () => _showRationaleBottomSheet(context), onTap: () => _showRationaleBottomSheet(context),
child: Row( child: Row(
children: [ children: [
Utils.buildSvgWithAssets(
icon: AppAssets.alertSquare,
height: 24.h,
width: 24.h,
iconColor: AppColors.textColor,
),
SizedBox(width: 12.w),
Expanded( Expanded(
child: LocaleKeys.whyAmIBeingAskedThis child: LocaleKeys.whyAmIBeingAskedThis.tr(context: context).toText12(
.tr(context: context) decorationColor: AppColors.primaryRedColor,
.toText12(color: AppColors.greyInfoTextColor, fontWeight: FontWeight.w600), isUnderLine: true,
color: AppColors.primaryRedColor,
fontWeight: FontWeight.w600,
),
) )
], ],
), ),

@ -255,9 +255,11 @@ class ConditionCard extends StatelessWidget {
SizedBox(height: 8.h), SizedBox(height: 8.h),
GestureDetector( GestureDetector(
onTap: () => _showExplanationBottomSheet(context), onTap: () => _showExplanationBottomSheet(context),
child: LocaleKeys.explainWhy.tr(context: context).toText14( child: LocaleKeys.explainWhy.tr(context: context).toText12(
isBold: true, isBold: true,
color: AppColors.primaryRedColor, color: AppColors.primaryRedColor,
isUnderLine: true,
decorationColor: AppColors.primaryRedColor,
), ),
), ),
if (severityEnum != PossibleConditionsSeverityEnum.monitorOnly) if (severityEnum != PossibleConditionsSeverityEnum.monitorOnly)
@ -309,8 +311,17 @@ class ConditionCard extends StatelessWidget {
return; return;
} }
// Get all evidence // Get all evidence with proper sources
final evidence = viewModel.getAllEvidenceWithSources(); final allEvidenceWithSources = viewModel.getAllEvidenceWithSources();
// Convert to format expected by GetExplanations API (with choice_id)
final evidence = allEvidenceWithSources.map((item) {
return {
"id": item["id"] ?? "",
"choice_id": "present",
"source": item["source"] ?? "initial",
};
}).toList();
// Show loading // Show loading
LoaderBottomSheet.showLoader(); LoaderBottomSheet.showLoader();
@ -331,9 +342,8 @@ class ConditionCard extends StatelessWidget {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// Supporting Evidence Section // Human-understandable description
LocaleKeys.supportingEvidence.tr(context: context).toText16( LocaleKeys.explanationDescription.tr(context: context).toText14(
isBold: true,
color: AppColors.textColor, color: AppColors.textColor,
), ),
SizedBox(height: 12.h), SizedBox(height: 12.h),
@ -356,7 +366,7 @@ class ConditionCard extends StatelessWidget {
height: 6.h, height: 6.h,
decoration: BoxDecoration( decoration: BoxDecoration(
shape: BoxShape.circle, shape: BoxShape.circle,
color: AppColors.primaryRedColor, color: AppColors.textColor,
), ),
), ),
Expanded( Expanded(
@ -381,7 +391,7 @@ class ConditionCard extends StatelessWidget {
), ),
callBackFunc: () {}, callBackFunc: () {},
isFullScreen: false, isFullScreen: false,
isCloseButtonVisible: false, isCloseButtonVisible: true,
); );
}, },
onError: (error) { onError: (error) {

@ -2,19 +2,54 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/app_export.dart'; import 'package:hmg_patient_app_new/core/app_export.dart';
import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart';
import 'package:hmg_patient_app_new/extensions/widget_extensions.dart';
import 'package:hmg_patient_app_new/features/symptoms_checker/models/resp_models/triage_level_response_model.dart'; import 'package:hmg_patient_app_new/features/symptoms_checker/models/resp_models/triage_level_response_model.dart';
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/theme/colors.dart';
class TriageLevelCard extends StatelessWidget { class TriageLevelCard extends StatefulWidget {
final TriageLevelDataDetails triageLevelData; final TriageLevelDataDetails triageLevelData;
const TriageLevelCard({super.key, required this.triageLevelData}); const TriageLevelCard({super.key, required this.triageLevelData});
@override
State<TriageLevelCard> createState() => _TriageLevelCardState();
}
class _TriageLevelCardState extends State<TriageLevelCard> with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _borderAnimation;
@override
void initState() {
super.initState();
// Create animation controller for border pulse effect
_animationController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 2000),
);
// Create animation that goes from 1.0 to 0.3 and back (for opacity)
_borderAnimation = Tween<double>(begin: 1.0, end: 0.3).animate(
CurvedAnimation(
parent: _animationController,
curve: Curves.easeInOut,
),
);
// Start the animation and repeat
_animationController.repeat(reverse: true);
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
/// Get localized title based on triage level /// Get localized title based on triage level
String getTitle(BuildContext context) { String getTitle(BuildContext context) {
final triageLevel = triageLevelData.triageLevel ?? ''; final triageLevel = widget.triageLevelData.triageLevel ?? '';
switch (triageLevel) { switch (triageLevel) {
case 'emergency_ambulance': case 'emergency_ambulance':
@ -34,7 +69,7 @@ class TriageLevelCard extends StatelessWidget {
/// Get localized description based on triage level /// Get localized description based on triage level
String getDescription(BuildContext context) { String getDescription(BuildContext context) {
final triageLevel = triageLevelData.triageLevel ?? ''; final triageLevel = widget.triageLevelData.triageLevel ?? '';
switch (triageLevel) { switch (triageLevel) {
case 'emergency_ambulance': case 'emergency_ambulance':
@ -54,7 +89,7 @@ class TriageLevelCard extends StatelessWidget {
/// Get color based on triage level /// Get color based on triage level
Color getBackgroundColor() { Color getBackgroundColor() {
final triageLevel = triageLevelData.triageLevel ?? ''; final triageLevel = widget.triageLevelData.triageLevel ?? '';
switch (triageLevel) { switch (triageLevel) {
case 'emergency_ambulance': case 'emergency_ambulance':
@ -72,7 +107,7 @@ class TriageLevelCard extends StatelessWidget {
/// Get border color based on triage level /// Get border color based on triage level
Color getBorderColor() { Color getBorderColor() {
final triageLevel = triageLevelData.triageLevel ?? ''; final triageLevel = widget.triageLevelData.triageLevel ?? '';
switch (triageLevel) { switch (triageLevel) {
case 'emergency_ambulance': case 'emergency_ambulance':
@ -90,7 +125,7 @@ class TriageLevelCard extends StatelessWidget {
/// Get icon based on triage level /// Get icon based on triage level
IconData getIcon() { IconData getIcon() {
final triageLevel = triageLevelData.triageLevel ?? ''; final triageLevel = widget.triageLevelData.triageLevel ?? '';
switch (triageLevel) { switch (triageLevel) {
case 'emergency_ambulance': case 'emergency_ambulance':
@ -110,7 +145,7 @@ class TriageLevelCard extends StatelessWidget {
/// Get root cause text (localized) /// Get root cause text (localized)
String? getRootCauseText(BuildContext context) { String? getRootCauseText(BuildContext context) {
final rootCause = triageLevelData.rootCause ?? ''; final rootCause = widget.triageLevelData.rootCause ?? '';
switch (rootCause) { switch (rootCause) {
case 'emergency_evidence_present': case 'emergency_evidence_present':
@ -138,13 +173,33 @@ class TriageLevelCard extends StatelessWidget {
final title = getTitle(context); final title = getTitle(context);
final description = getDescription(context); final description = getDescription(context);
final rootCauseText = getRootCauseText(context); final rootCauseText = getRootCauseText(context);
final seriousSymptoms = triageLevelData.serious ?? []; final seriousSymptoms = widget.triageLevelData.serious ?? [];
return Container( return AnimatedBuilder(
width: double.infinity, animation: _borderAnimation,
margin: EdgeInsets.only(left: 24.w, right: 24.w, top: 16.h, bottom: 8.h), builder: (context, child) {
padding: EdgeInsets.all(20.w), return Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r, hasShadow: true), width: double.infinity,
margin: EdgeInsets.only(left: 24.w, right: 24.w, top: 16.h, bottom: 8.h),
padding: EdgeInsets.all(20.w),
decoration: BoxDecoration(
color: AppColors.whiteColor,
borderRadius: BorderRadius.circular(24.r),
border: Border.all(
color: borderColor.withValues(alpha: _borderAnimation.value),
width: 2.w,
),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.08),
blurRadius: 10.r,
offset: Offset(0, 2.h),
),
],
),
child: child,
);
},
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [

Loading…
Cancel
Save