radiology ai Analysis

pull/246/head
aamir-csol 1 day ago
parent 7d86ca0511
commit 4cd5132627

@ -2,9 +2,19 @@ import 'dart:convert';
class RadiologyAiAnalysisResponse { class RadiologyAiAnalysisResponse {
String? summary; String? summary;
String? labResults;
String? aiPredictions;
String? riskFactors;
String? prevention;
int? languageId;
RadiologyAiAnalysisResponse({ RadiologyAiAnalysisResponse({
this.summary, this.summary,
this.labResults,
this.aiPredictions,
this.riskFactors,
this.prevention,
this.languageId,
}); });
factory RadiologyAiAnalysisResponse.fromRawJson(String str) => RadiologyAiAnalysisResponse.fromJson(json.decode(str)); factory RadiologyAiAnalysisResponse.fromRawJson(String str) => RadiologyAiAnalysisResponse.fromJson(json.decode(str));
@ -12,11 +22,20 @@ class RadiologyAiAnalysisResponse {
String toRawJson() => json.encode(toJson()); String toRawJson() => json.encode(toJson());
factory RadiologyAiAnalysisResponse.fromJson(Map<String, dynamic> json) => RadiologyAiAnalysisResponse( factory RadiologyAiAnalysisResponse.fromJson(Map<String, dynamic> 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<String, dynamic> toJson() => { Map<String, dynamic> toJson() => {
"summary": summary, "summary": summary,
}; "lab_results": labResults,
"ai_predictions": aiPredictions,
"riskFactors": riskFactors,
"prevention": prevention,
"language_id": languageId,
};
} }

@ -1,12 +1,10 @@
import 'package:easy_localization/easy_localization.dart'; 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_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/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/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/features/radiology/radiology_repo.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/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/presentation/radiology/radiology_result_page.dart';
import 'package:hmg_patient_app_new/services/error_handler_service.dart'; import 'package:hmg_patient_app_new/services/error_handler_service.dart';
import 'package:hmg_patient_app_new/services/navigation_service.dart'; import 'package:hmg_patient_app_new/services/navigation_service.dart';
@ -216,12 +214,11 @@ class RadiologyViewModel extends ChangeNotifier {
void setIsRadiologyAIAnalysisNeedsToBeShown(bool value) { void setIsRadiologyAIAnalysisNeedsToBeShown(bool value) {
isRadiologyAIAnalysisNeedsToBeShown = value; isRadiologyAIAnalysisNeedsToBeShown = value;
notifyListeners();
} }
void closeRadiologyAIAnalysis() { void closeRadiologyAIAnalysis() {
radiologyAiAnalysisResponse = null; radiologyAiAnalysisResponse = null;
notifyListeners();
} }
Future<void> getRadiologyAiAnalysis({required String reportData, required String loadingText, required int langId}) async { Future<void> getRadiologyAiAnalysis({required String reportData, required String loadingText, required int langId}) async {
@ -229,7 +226,6 @@ class RadiologyViewModel extends ChangeNotifier {
loadingText: loadingText, loadingText: loadingText,
showCloseButton: true, showCloseButton: true,
onCloseTap: () { onCloseTap: () {
setIsRadiologyAIAnalysisNeedsToBeShown(false);
}, },
); );
@ -248,6 +244,10 @@ class RadiologyViewModel extends ChangeNotifier {
if (isRadiologyAIAnalysisNeedsToBeShown) { if (isRadiologyAIAnalysisNeedsToBeShown) {
radiologyAiAnalysisResponse = apiResponse.data; radiologyAiAnalysisResponse = apiResponse.data;
notifyListeners(); notifyListeners();
// Navigate to detailed page
navigationService.push(
CustomPageRoute(direction: AxisDirection.down, page: RadiologyAiAnalysisDetailedPage()),
);
} }
} }
}, },

@ -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<RadiologyAiAnalysisDetailedPage> createState() => _RadiologyAiAnalysisDetailedPageState();
}
class _RadiologyAiAnalysisDetailedPageState extends State<RadiologyAiAnalysisDetailedPage> {
late RadiologyViewModel radiologyProvider;
List<String> _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<RadiologyViewModel>(context, listen: false);
return CollapsingListView(
isClose: true,
title: LocaleKeys.aiAnalysis.tr(context: context),
child: Consumer<RadiologyViewModel>(
builder: (context, model, child) {
if (model.radiologyAiAnalysisResponse == null) {
return Utils.getNoDataWidget(context);
}
final analysis = model.radiologyAiAnalysisResponse!;
List<ExpandableListItem> 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),
),
],
),
);
}
}

@ -16,8 +16,18 @@ class RadiologyAiAnalysisWidget extends StatelessWidget {
const RadiologyAiAnalysisWidget({super.key, required this.data}); const RadiologyAiAnalysisWidget({super.key, required this.data});
List<String> _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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// Parse the string fields into lists
final predictions = _parseStringToList(data.aiPredictions);
final preventions = _parseStringToList(data.prevention);
return Container( return Container(
decoration: BoxDecoration(borderRadius: BorderRadius.circular(24.r), gradient: AppColors.aiLinearGradient), decoration: BoxDecoration(borderRadius: BorderRadius.circular(24.r), gradient: AppColors.aiLinearGradient),
padding: EdgeInsets.all(1.5.r), // Border thickness padding: EdgeInsets.all(1.5.r), // Border thickness
@ -43,7 +53,25 @@ class RadiologyAiAnalysisWidget extends StatelessWidget {
), ),
SizedBox(height: 16.h), SizedBox(height: 16.h),
if (data.summary != null && data.summary!.isNotEmpty) ...[ 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), 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), SizedBox(height: 20.h),
], ],
Row( Row(
@ -52,14 +80,45 @@ class RadiologyAiAnalysisWidget extends StatelessWidget {
Icon(Icons.info_outline, color: AppColors.greyTextColor, size: 20.r), Icon(Icons.info_outline, color: AppColors.greyTextColor, size: 20.r),
SizedBox(width: 8.w), SizedBox(width: 8.w),
Expanded( 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<RadiologyViewModel>().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),
),
],
),
);
}
} }

@ -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/string_extensions.dart';
import 'package:hmg_patient_app_new/extensions/widget_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/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/features/radiology/radiology_view_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/presentation/radiology/radiology_ai_analysis_widget.dart';
import 'package:hmg_patient_app_new/services/dialog_service.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/widgets/appbar/collapsing_list_view.dart';
import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/theme/colors.dart';
@ -37,8 +35,6 @@ class RadiologyResultPage extends StatefulWidget {
class _RadiologyResultPageState extends State<RadiologyResultPage> { class _RadiologyResultPageState extends State<RadiologyResultPage> {
late RadiologyViewModel radiologyViewModel; late RadiologyViewModel radiologyViewModel;
final GlobalKey _aiAnalysisKey = GlobalKey();
RadiologyAiAnalysisResponse? _previousAiData;
@override @override
void initState() { void initState() {
@ -52,7 +48,7 @@ class _RadiologyResultPageState extends State<RadiologyResultPage> {
void dispose() { void dispose() {
// Clear AI analysis data when leaving the screen // Clear AI analysis data when leaving the screen
radiologyViewModel.closeRadiologyAIAnalysis(); radiologyViewModel.closeRadiologyAIAnalysis();
radiologyViewModel.setIsRadiologyAIAnalysisNeedsToBeShown(true); // radiologyViewModel.setIsRadiologyAIAnalysisNeedsToBeShown(true);
super.dispose(); super.dispose();
} }
@ -63,29 +59,6 @@ class _RadiologyResultPageState extends State<RadiologyResultPage> {
return Scaffold( return Scaffold(
backgroundColor: AppColors.bgScaffoldColor, backgroundColor: AppColors.bgScaffoldColor,
body: Consumer<RadiologyViewModel>(builder: (context, radVM, child) { body: Consumer<RadiologyViewModel>(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( return Column(
children: [ children: [
@ -185,11 +158,6 @@ class _RadiologyResultPageState extends State<RadiologyResultPage> {
).paddingSymmetrical(16.h, 0.h), ).paddingSymmetrical(16.h, 0.h),
), ),
SizedBox(height: 24.h), SizedBox(height: 24.h),
if (radVM.radiologyAiAnalysisResponse != null)
RadiologyAiAnalysisWidget(
key: _aiAnalysisKey,
data: radVM.radiologyAiAnalysisResponse!,
),
], ],
), ),
), ),
@ -229,8 +197,7 @@ class _RadiologyResultPageState extends State<RadiologyResultPage> {
iconSize: 20.h, iconSize: 20.h,
).paddingSymmetrical(24.h, 0.h) ).paddingSymmetrical(24.h, 0.h)
: SizedBox.shrink(), : SizedBox.shrink(),
Utils.havePrivilege(116) ? radVM.radiologyAiAnalysisResponse == null Utils.havePrivilege(116) ? Container(
? Container(
height: 56.h, height: 56.h,
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12.r), borderRadius: BorderRadius.circular(12.r),
@ -274,7 +241,7 @@ class _RadiologyResultPageState extends State<RadiologyResultPage> {
context.pop(); context.pop();
}, },
); );
}) : SizedBox.shrink() : SizedBox(height: 24.h), }) : SizedBox(height: 24.h),
], ],
), ),
), ),

Loading…
Cancel
Save