From 4cd51326277ba741ce7a1d7549c8dc1e844144c3 Mon Sep 17 00:00:00 2001 From: aamir-csol Date: Tue, 14 Apr 2026 12:25:53 +0300 Subject: [PATCH] radiology ai Analysis --- .../radiology_ai_analysis_response_model.dart | 29 +- .../radiology/radiology_view_model.dart | 12 +- .../radiology_ai_analysis_detailed_page.dart | 255 ++++++++++++++++++ .../radiology_ai_analysis_widget.dart | 63 ++++- .../radiology/radiology_result_page.dart | 39 +-- 5 files changed, 349 insertions(+), 49 deletions(-) create mode 100644 lib/presentation/radiology/radiology_ai_analysis_detailed_page.dart diff --git a/lib/features/radiology/models/resp_models/radiology_ai_analysis_response_model.dart b/lib/features/radiology/models/resp_models/radiology_ai_analysis_response_model.dart index e838f187..3f523428 100644 --- a/lib/features/radiology/models/resp_models/radiology_ai_analysis_response_model.dart +++ b/lib/features/radiology/models/resp_models/radiology_ai_analysis_response_model.dart @@ -2,9 +2,19 @@ import 'dart:convert'; class RadiologyAiAnalysisResponse { String? summary; + String? labResults; + String? aiPredictions; + String? riskFactors; + String? prevention; + int? languageId; RadiologyAiAnalysisResponse({ this.summary, + this.labResults, + this.aiPredictions, + this.riskFactors, + this.prevention, + this.languageId, }); factory RadiologyAiAnalysisResponse.fromRawJson(String str) => RadiologyAiAnalysisResponse.fromJson(json.decode(str)); @@ -12,11 +22,20 @@ class RadiologyAiAnalysisResponse { String toRawJson() => json.encode(toJson()); factory RadiologyAiAnalysisResponse.fromJson(Map json) => RadiologyAiAnalysisResponse( - summary: json["summary"], - ); + summary: json["summary"], + labResults: json["lab_results"], + aiPredictions: json["ai_predictions"], + riskFactors: json["riskFactors"], + prevention: json["prevention"], + languageId: json["language_id"], + ); Map toJson() => { - "summary": summary, - }; + "summary": summary, + "lab_results": labResults, + "ai_predictions": aiPredictions, + "riskFactors": riskFactors, + "prevention": prevention, + "language_id": languageId, + }; } - diff --git a/lib/features/radiology/radiology_view_model.dart b/lib/features/radiology/radiology_view_model.dart index 7386a5a0..cf795a1d 100644 --- a/lib/features/radiology/radiology_view_model.dart +++ b/lib/features/radiology/radiology_view_model.dart @@ -1,12 +1,10 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:hmg_patient_app_new/core/app_state.dart'; -import 'package:hmg_patient_app_new/core/dependencies.dart'; -import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/features/authentication/models/resp_models/authenticated_user_resp_model.dart'; import 'package:hmg_patient_app_new/features/radiology/models/resp_models/radiology_ai_analysis_response_model.dart'; import 'package:hmg_patient_app_new/features/radiology/radiology_repo.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/presentation/radiology/radiology_ai_analysis_detailed_page.dart'; import 'package:hmg_patient_app_new/presentation/radiology/radiology_result_page.dart'; import 'package:hmg_patient_app_new/services/error_handler_service.dart'; import 'package:hmg_patient_app_new/services/navigation_service.dart'; @@ -216,12 +214,11 @@ class RadiologyViewModel extends ChangeNotifier { void setIsRadiologyAIAnalysisNeedsToBeShown(bool value) { isRadiologyAIAnalysisNeedsToBeShown = value; - notifyListeners(); } void closeRadiologyAIAnalysis() { radiologyAiAnalysisResponse = null; - notifyListeners(); + } Future getRadiologyAiAnalysis({required String reportData, required String loadingText, required int langId}) async { @@ -229,7 +226,6 @@ class RadiologyViewModel extends ChangeNotifier { loadingText: loadingText, showCloseButton: true, onCloseTap: () { - setIsRadiologyAIAnalysisNeedsToBeShown(false); }, ); @@ -248,6 +244,10 @@ class RadiologyViewModel extends ChangeNotifier { if (isRadiologyAIAnalysisNeedsToBeShown) { radiologyAiAnalysisResponse = apiResponse.data; notifyListeners(); + // Navigate to detailed page + navigationService.push( + CustomPageRoute(direction: AxisDirection.down, page: RadiologyAiAnalysisDetailedPage()), + ); } } }, diff --git a/lib/presentation/radiology/radiology_ai_analysis_detailed_page.dart b/lib/presentation/radiology/radiology_ai_analysis_detailed_page.dart new file mode 100644 index 00000000..78a3a097 --- /dev/null +++ b/lib/presentation/radiology/radiology_ai_analysis_detailed_page.dart @@ -0,0 +1,255 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.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/radiology/radiology_view_model.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/widgets/appbar/collapsing_list_view.dart'; +import 'package:hmg_patient_app_new/widgets/expandable_list_widget.dart'; +import 'package:provider/provider.dart'; + +class RadiologyAiAnalysisDetailedPage extends StatefulWidget { + const RadiologyAiAnalysisDetailedPage({super.key}); + + @override + State createState() => _RadiologyAiAnalysisDetailedPageState(); +} + +class _RadiologyAiAnalysisDetailedPageState extends State { + late RadiologyViewModel radiologyProvider; + + List _parseStringToList(String? text) { + if (text == null || text.isEmpty) return []; + // Split by newlines and filter out empty lines + return text.split('\n').where((line) => line.trim().isNotEmpty).map((line) => line.trim()).toList(); + } + + @override + Widget build(BuildContext context) { + radiologyProvider = Provider.of(context, listen: false); + return CollapsingListView( + isClose: true, + title: LocaleKeys.aiAnalysis.tr(context: context), + child: Consumer( + builder: (context, model, child) { + if (model.radiologyAiAnalysisResponse == null) { + return Utils.getNoDataWidget(context); + } + + final analysis = model.radiologyAiAnalysisResponse!; + List expandableItems = []; + + // Parse string fields into lists + final predictions = _parseStringToList(analysis.aiPredictions); + final preventions = _parseStringToList(analysis.prevention); + final riskFactorsText = _parseStringToList(analysis.riskFactors); + + // Summary Section + if (analysis.summary != null && analysis.summary!.isNotEmpty) { + expandableItems.add( + ExpandableListItem( + backgroundColor: Colors.transparent, + expandedBackgroundColor: Colors.transparent, + title: LocaleKeys.summary.tr(context: context).toText18(weight: FontWeight.w700, color: AppColors.blackColor), + initiallyExpanded: true, + children: [ + SizedBox(height: 10.h), + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 12.r, + side: BorderSide(width: 1, color: Color(0xFF0B85F7)), + ), + padding: EdgeInsets.all(16.r), + child: (analysis.summary ?? "").toText14(color: AppColors.textColorLight, height: 1.5, weight: FontWeight.w400), + ), + SizedBox(height: 8.h), + ], + ), + ); + } + + // Lab Results Section (Radiology Results) + if (analysis.labResults != null && analysis.labResults!.isNotEmpty) { + expandableItems.add( + ExpandableListItem( + backgroundColor: Colors.transparent, + expandedBackgroundColor: Colors.transparent, + title: LocaleKeys.labResults.tr(context: context).toText18(weight: FontWeight.w700, color: AppColors.blackColor), + children: [ + SizedBox(height: 10.h), + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 12.r, + side: BorderSide(width: 1, color: Color(0xFF0B85F7)), + ), + padding: EdgeInsets.all(16.r), + child: (analysis.labResults ?? "").toText14(color: AppColors.textColorLight, height: 1.5, weight: FontWeight.w400), + ), + SizedBox(height: 8.h), + ], + ), + ); + } + + // AI Predictions Section + if (predictions.isNotEmpty) { + expandableItems.add( + ExpandableListItem( + backgroundColor: Colors.transparent, + expandedBackgroundColor: Colors.transparent, + title: LocaleKeys.prediction.tr(context: context).toText18(weight: FontWeight.w700, color: AppColors.blackColor), + children: [ + SizedBox(height: 10.h), + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 12.r, + side: BorderSide(width: 1, color: Color(0xFFFFAF15)), + ), + padding: EdgeInsets.all(16.r), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: predictions.map((prediction) => _buildBulletItem(prediction)).toList(), + ), + ), + SizedBox(height: 8.h), + ], + ), + ); + } + + // Risk Factors Section + if (riskFactorsText.isNotEmpty) { + expandableItems.add( + ExpandableListItem( + backgroundColor: Colors.transparent, + expandedBackgroundColor: Colors.transparent, + title: LocaleKeys.riskFactors.tr(context: context).toText18(weight: FontWeight.w700, color: AppColors.blackColor), + children: [ + SizedBox(height: 10.h), + ...riskFactorsText.asMap().entries.map((entry) { + final riskText = entry.value; + return Column( + children: [ + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: Color(0xFFFBCB6E).withOpacity(0.10), + borderRadius: 12.r, + side: BorderSide(width: 1, color: Color(0xFFFBCB6E).withOpacity(0.10)), + ), + padding: EdgeInsets.all(16.r), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.guradIcon), + Padding( + padding: const EdgeInsets.all(8.0), + child: LocaleKeys.riskFactors.tr(context: context).toText16(weight: FontWeight.w700, color: AppColors.blackColor), + ), + ], + ), + SizedBox(height: 8.h), + riskText.toText14( + color: AppColors.textColorLight, + height: 1.5, + weight: FontWeight.w400, + ), + ], + ), + ), + SizedBox(height: 16.h), + ], + ); + }).toList(), + ], + ), + ); + } + + // Prevention Section + if (preventions.isNotEmpty) { + expandableItems.add( + ExpandableListItem( + backgroundColor: Colors.transparent, + expandedBackgroundColor: Colors.transparent, + title: LocaleKeys.prevention.tr(context: context).toText18(weight: FontWeight.w700, color: AppColors.blackColor), + children: [ + SizedBox(height: 10.h), + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 12.r, + side: BorderSide(width: 1, color: Color(0xFF18C273)), + ), + padding: EdgeInsets.all(16.r), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: preventions.map((prevention) => _buildBulletItem(prevention)).toList(), + ), + ), + SizedBox(height: 8.h), + ], + ), + ); + } + + return Column( + children: [ + if (expandableItems.isEmpty) + Utils.getNoDataWidget(context) + else + CustomExpandableList( + expansionMode: ExpansionMode.exactlyOne, + dividerColor: AppColors.dividerColor, + itemPadding: EdgeInsets.symmetric(vertical: 16.h, horizontal: 14.h), + items: expandableItems, + theme: ExpandableListTheme.custom( + defaultTrailingIcon: Utils.buildSvgWithAssets( + icon: AppAssets.arrow_down, + height: 22.h, + width: 22.w, + iconColor: AppColors.textColor, + ), + ), + ).paddingSymmetrical(16.w, 0.0), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon(Icons.info_outline, color: AppColors.greyTextColor, size: 20.r), + SizedBox(width: 8.w), + Expanded( + child: ("${LocaleKeys.disclaimer.tr()}:${LocaleKeys.thisAboveInfo.tr()}") + .toText12(color: AppColors.greyTextColor, fontWeight: FontWeight.w400), + ), + ], + ).paddingSymmetrical(16.w, 16.h) + ], + ); + }, + ), + ); + } + + Widget _buildBulletItem(String text) { + return Padding( + padding: EdgeInsets.only(bottom: 12.h), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: text.toText14(color: AppColors.textColorLight, height: 1.5, weight: FontWeight.w400), + ), + ], + ), + ); + } +} + diff --git a/lib/presentation/radiology/radiology_ai_analysis_widget.dart b/lib/presentation/radiology/radiology_ai_analysis_widget.dart index 97d35115..7eda4047 100644 --- a/lib/presentation/radiology/radiology_ai_analysis_widget.dart +++ b/lib/presentation/radiology/radiology_ai_analysis_widget.dart @@ -16,8 +16,18 @@ class RadiologyAiAnalysisWidget extends StatelessWidget { const RadiologyAiAnalysisWidget({super.key, required this.data}); + List _parseStringToList(String? text) { + if (text == null || text.isEmpty) return []; + // Split by newlines and filter out empty lines + return text.split('\n').where((line) => line.trim().isNotEmpty).map((line) => line.trim()).toList(); + } + @override Widget build(BuildContext context) { + // Parse the string fields into lists + final predictions = _parseStringToList(data.aiPredictions); + final preventions = _parseStringToList(data.prevention); + return Container( decoration: BoxDecoration(borderRadius: BorderRadius.circular(24.r), gradient: AppColors.aiLinearGradient), padding: EdgeInsets.all(1.5.r), // Border thickness @@ -43,7 +53,25 @@ class RadiologyAiAnalysisWidget extends StatelessWidget { ), SizedBox(height: 16.h), if (data.summary != null && data.summary!.isNotEmpty) ...[ + LocaleKeys.suggestion.tr().toText16(weight: FontWeight.w700, color: AppColors.blackColor), + SizedBox(height: 8.h), data.summary!.toText14(color: AppColors.textColorLight, height: 1.5, weight: FontWeight.w400), + SizedBox(height: 16.h), + Divider(color: AppColors.dividerColor, thickness: 1), + SizedBox(height: 16.h), + ], + if (predictions.isNotEmpty) ...[ + LocaleKeys.prediction.tr().toText16(weight: FontWeight.w700, color: AppColors.blackColor), + SizedBox(height: 12.h), + ...predictions.map((item) => _buildBulletItem(item)), + SizedBox(height: 16.h), + Divider(color: AppColors.dividerColor, thickness: 1), + SizedBox(height: 16.h), + ], + if (preventions.isNotEmpty) ...[ + LocaleKeys.prevention.tr().toText16(weight: FontWeight.w700, color: AppColors.blackColor), + SizedBox(height: 12.h), + ...preventions.map((item) => _buildBulletItem(item)), SizedBox(height: 20.h), ], Row( @@ -52,14 +80,45 @@ class RadiologyAiAnalysisWidget extends StatelessWidget { Icon(Icons.info_outline, color: AppColors.greyTextColor, size: 20.r), SizedBox(width: 8.w), Expanded( - child: ("${LocaleKeys.disclaimer.tr()}: ${LocaleKeys.thisAboveInfo.tr()}").toText12(color: AppColors.greyTextColor, fontWeight: FontWeight.w400), + child: ("${LocaleKeys.disclaimer.tr()}:${LocaleKeys.thisAboveInfo.tr()}").toText12(color: AppColors.greyTextColor, fontWeight: FontWeight.w400), ), ], ), - SizedBox(height: 8.h), + SizedBox(height: 16.h), + CustomButton( + height: 50.h, + text: LocaleKeys.close.tr(context: context), + backgroundColor: AppColors.primaryRedColor, + borderColor: AppColors.primaryRedColor, + onPressed: () { + getIt.get().closeRadiologyAIAnalysis(); + }, + ), ], ), ), ); } + + Widget _buildBulletItem(String text) { + return Padding( + padding: EdgeInsets.only(bottom: 12.h), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.only(top: 8.h, right: 12.w, left: 4.w), + child: Container( + width: 5.r, + height: 5.r, + decoration: BoxDecoration(color: AppColors.blackColor, shape: BoxShape.circle), + ), + ), + Expanded( + child: text.toText14(color: AppColors.textColorLight, height: 1.5, weight: FontWeight.w400), + ), + ], + ), + ); + } } diff --git a/lib/presentation/radiology/radiology_result_page.dart b/lib/presentation/radiology/radiology_result_page.dart index 445bbd45..5b7072e6 100644 --- a/lib/presentation/radiology/radiology_result_page.dart +++ b/lib/presentation/radiology/radiology_result_page.dart @@ -11,10 +11,8 @@ import 'package:hmg_patient_app_new/extensions/route_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/radiology/models/resp_models/patient_radiology_response_model.dart'; -import 'package:hmg_patient_app_new/features/radiology/models/resp_models/radiology_ai_analysis_response_model.dart'; import 'package:hmg_patient_app_new/features/radiology/radiology_view_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; -import 'package:hmg_patient_app_new/presentation/radiology/radiology_ai_analysis_widget.dart'; import 'package:hmg_patient_app_new/services/dialog_service.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; @@ -37,8 +35,6 @@ class RadiologyResultPage extends StatefulWidget { class _RadiologyResultPageState extends State { late RadiologyViewModel radiologyViewModel; - final GlobalKey _aiAnalysisKey = GlobalKey(); - RadiologyAiAnalysisResponse? _previousAiData; @override void initState() { @@ -52,7 +48,7 @@ class _RadiologyResultPageState extends State { void dispose() { // Clear AI analysis data when leaving the screen radiologyViewModel.closeRadiologyAIAnalysis(); - radiologyViewModel.setIsRadiologyAIAnalysisNeedsToBeShown(true); + // radiologyViewModel.setIsRadiologyAIAnalysisNeedsToBeShown(true); super.dispose(); } @@ -63,29 +59,6 @@ class _RadiologyResultPageState extends State { return Scaffold( backgroundColor: AppColors.bgScaffoldColor, body: Consumer(builder: (context, radVM, child) { - // Detect changes in radiologyAiAnalysisResponse - WidgetsBinding.instance.addPostFrameCallback((_) { - if (radVM.radiologyAiAnalysisResponse != null && _previousAiData == null) { - // AI data just became available — scroll to it - final ctx = _aiAnalysisKey.currentContext; - if (ctx != null) { - Scrollable.ensureVisible( - ctx, - duration: const Duration(milliseconds: 400), - curve: Curves.easeOut, - ); - } - } else if (radVM.radiologyAiAnalysisResponse == null && _previousAiData != null) { - // AI data just became null — scroll to top - final scrollable = Scrollable.maybeOf(context); - scrollable?.position.animateTo( - 0, - duration: const Duration(milliseconds: 400), - curve: Curves.easeOut, - ); - } - _previousAiData = radVM.radiologyAiAnalysisResponse; - }); return Column( children: [ @@ -185,11 +158,6 @@ class _RadiologyResultPageState extends State { ).paddingSymmetrical(16.h, 0.h), ), SizedBox(height: 24.h), - if (radVM.radiologyAiAnalysisResponse != null) - RadiologyAiAnalysisWidget( - key: _aiAnalysisKey, - data: radVM.radiologyAiAnalysisResponse!, - ), ], ), ), @@ -229,8 +197,7 @@ class _RadiologyResultPageState extends State { iconSize: 20.h, ).paddingSymmetrical(24.h, 0.h) : SizedBox.shrink(), - Utils.havePrivilege(116) ? radVM.radiologyAiAnalysisResponse == null - ? Container( + Utils.havePrivilege(116) ? Container( height: 56.h, decoration: BoxDecoration( borderRadius: BorderRadius.circular(12.r), @@ -274,7 +241,7 @@ class _RadiologyResultPageState extends State { context.pop(); }, ); - }) : SizedBox.shrink() : SizedBox(height: 24.h), + }) : SizedBox(height: 24.h), ], ), ), -- 2.30.2