diff --git a/lib/core/api_consts.dart b/lib/core/api_consts.dart index b0df11f2..020f37e7 100644 --- a/lib/core/api_consts.dart +++ b/lib/core/api_consts.dart @@ -230,8 +230,13 @@ class ApiConsts { static String getPatientBloodGroup = "services/PatientVarification.svc/REST/BloodDonation_GetBloodGroupDetails"; static String getPatientBloodAgreement = "Services/PatientVarification.svc/REST/CheckUserAgreementForBloodDonation"; static String getPatientBloodTypeNew = "Services/Patients.svc/REST/HIS_GetPatientBloodType_New"; - static String getAiOverViewLabOrders = "Services/Patients.svc/REST/HMGAI_Lab_Analyze_Orders_API"; - static String getAiOverViewLabOrder = "Services/Patients.svc/REST/HMGAI_Lab_Analyzer_API"; + // static String getAiOverViewLabOrders = "Services/Patients.svc/REST/HMGAI_Lab_Analyze_Orders_API"; + // static String getAiOverViewLabOrder = "Services/Patients.svc/REST/HMGAI_Lab_Analyzer_API"; + + static String getAiAnalysisURL = "https://api.labpredict.com"; + static String getAiRadiologyAnalysis = "$getAiAnalysisURL/radiology"; + static String getAiOverViewLabOrdersNew = "$getAiAnalysisURL/analyze_lab_orders"; + static String getAiOverViewLabOrderNew = "$getAiAnalysisURL/analyze"; static String googleCloudStorageARTranslationFileURL = "https://storage.googleapis.com/hmg-patientapp-translations/ar-SA.json"; static String googleCloudStorageENTranslationFileURL = "https://storage.googleapis.com/hmg-patientapp-translations/en-US.json"; diff --git a/lib/features/lab/lab_repo.dart b/lib/features/lab/lab_repo.dart index 10839b12..08946e97 100644 --- a/lib/features/lab/lab_repo.dart +++ b/lib/features/lab/lab_repo.dart @@ -319,16 +319,24 @@ class LabRepoImp implements LabRepo { GenericApiModel? apiResponse; Failure? failure; await apiClient.post( - ApiConsts.getAiOverViewLabOrder, + ApiConsts.getAiOverViewLabOrderNew, body: labOrder, + isAllowAny: true, + isExternal: true, + apiHeaders: { + 'Accept-Language': 'en-US,en;q=0.7', + 'Content-Type': 'application/json', + 'X-API-KEY': '9d9d2ac4c1fc302299dbb0fbb1b7fea1', + 'accept': 'application/json', + }, onFailure: (error, statusCode, {messageStatus, failureType}) { failure = failureType; }, onSuccess: (response, statusCode, {messageStatus, errorMessage}) { try { var res; - if (response["analysis_result"] != null) { - res = response["analysis_result"]; + if (response != null) { + res = response; } apiResponse = GenericApiModel( messageStatus: messageStatus, @@ -355,7 +363,15 @@ class LabRepoImp implements LabRepo { GenericApiModel? apiResponse; Failure? failure; await apiClient.post( - ApiConsts.getAiOverViewLabOrders, + ApiConsts.getAiOverViewLabOrdersNew, + isAllowAny: true, + isExternal: true, + apiHeaders: { + 'Accept-Language': 'en-US,en;q=0.7', + 'Content-Type': 'application/json', + 'X-API-KEY': '9d9d2ac4c1fc302299dbb0fbb1b7fea1', + 'accept': 'application/json', + }, body: {"lab_results": labOrder}, onFailure: (error, statusCode, {messageStatus, failureType}) { failure = failureType; @@ -363,8 +379,8 @@ class LabRepoImp implements LabRepo { onSuccess: (response, statusCode, {messageStatus, errorMessage}) { try { var res; - if (response["lab_analysis_result"] != null) { - res = response["lab_analysis_result"]; + if (response != null) { + res = response; } apiResponse = GenericApiModel( messageStatus: messageStatus, 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 new file mode 100644 index 00000000..e838f187 --- /dev/null +++ b/lib/features/radiology/models/resp_models/radiology_ai_analysis_response_model.dart @@ -0,0 +1,22 @@ +import 'dart:convert'; + +class RadiologyAiAnalysisResponse { + String? summary; + + RadiologyAiAnalysisResponse({ + this.summary, + }); + + factory RadiologyAiAnalysisResponse.fromRawJson(String str) => RadiologyAiAnalysisResponse.fromJson(json.decode(str)); + + String toRawJson() => json.encode(toJson()); + + factory RadiologyAiAnalysisResponse.fromJson(Map json) => RadiologyAiAnalysisResponse( + summary: json["summary"], + ); + + Map toJson() => { + "summary": summary, + }; +} + diff --git a/lib/features/radiology/radiology_repo.dart b/lib/features/radiology/radiology_repo.dart index 3ad4e54f..a6ea293e 100644 --- a/lib/features/radiology/radiology_repo.dart +++ b/lib/features/radiology/radiology_repo.dart @@ -6,6 +6,7 @@ import 'package:hmg_patient_app_new/core/exceptions/api_failure.dart'; import 'package:hmg_patient_app_new/core/utils/utils.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/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/services/logger_service.dart'; abstract class RadiologyRepo { @@ -17,6 +18,8 @@ abstract class RadiologyRepo { {required PatientRadiologyResponseModel patientRadiologyResponseModel, required AuthenticatedUser authenticatedUser}); Future>>> getPatientRadiologyOrderByAppointment({required num appointmentNo, required num projectID}); + + Future>> getRadiologyAiAnalysis({required String reportData}); } class RadiologyRepoImp implements RadiologyRepo { @@ -218,4 +221,49 @@ class RadiologyRepoImp implements RadiologyRepo { return Left(UnknownFailure(e.toString())); } } + + @override + Future>> getRadiologyAiAnalysis({required String reportData}) async { + Map payload = { + "text": reportData, + }; + + try { + GenericApiModel? apiResponse; + Failure? failure; + + await apiClient.post( + ApiConsts.getAiRadiologyAnalysis, + body: payload, + isExternal: true, + isAllowAny: true, + apiHeaders: { + 'Accept-Language': 'en-US,en;q=0.7', + 'Content-Type': 'application/json', + 'X-API-KEY': '9d9d2ac4c1fc302299dbb0fbb1b7fea1', + 'accept': 'application/json', + }, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + apiResponse = GenericApiModel( + messageStatus: 1, + statusCode: statusCode, + errorMessage: null, + data: RadiologyAiAnalysisResponse.fromJson(response), + ); + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, + ); + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } } diff --git a/lib/features/radiology/radiology_view_model.dart b/lib/features/radiology/radiology_view_model.dart index fa43a96d..c8385870 100644 --- a/lib/features/radiology/radiology_view_model.dart +++ b/lib/features/radiology/radiology_view_model.dart @@ -1,12 +1,16 @@ 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_result_page.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/widgets/loader/bottomsheet_loader.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; import 'models/resp_models/patient_radiology_response_model.dart'; @@ -14,6 +18,7 @@ import 'models/resp_models/patient_radiology_response_model.dart'; class RadiologyViewModel extends ChangeNotifier { bool isRadiologyOrdersLoading = false; bool isRadiologyPDFReportLoading = false; + bool isRadiologyAIAnalysisNeedsToBeShown = true; RadiologyRepo radiologyRepo; ErrorHandlerService errorHandlerService; @@ -25,6 +30,8 @@ class RadiologyViewModel extends ChangeNotifier { String radiologyImageURL = ""; String patientRadiologyReportPDFBase64 = ""; + RadiologyAiAnalysisResponse? radiologyAiAnalysisResponse; + late List _radiologySuggestionsList = []; List get radiologySuggestions => _radiologySuggestionsList; @@ -211,4 +218,46 @@ class RadiologyViewModel extends ChangeNotifier { } notifyListeners(); } + + void setIsRadiologyAIAnalysisNeedsToBeShown(bool value) { + isRadiologyAIAnalysisNeedsToBeShown = value; + notifyListeners(); + } + + void closeRadiologyAIAnalysis() { + radiologyAiAnalysisResponse = null; + notifyListeners(); + } + + Future getRadiologyAiAnalysis({required String reportData, required String loadingText}) async { + LoaderBottomSheet.showLoader( + loadingText: loadingText, + showCloseButton: true, + onCloseTap: () { + setIsRadiologyAIAnalysisNeedsToBeShown(false); + }, + ); + + final result = await radiologyRepo.getRadiologyAiAnalysis(reportData: reportData); + + result.fold( + (failure) async { + LoaderBottomSheet.hideLoader(); + if (isRadiologyAIAnalysisNeedsToBeShown) { + await errorHandlerService.handleError(failure: failure); + } + }, + (apiResponse) { + LoaderBottomSheet.hideLoader(); + if (apiResponse.messageStatus == 1) { + if (isRadiologyAIAnalysisNeedsToBeShown) { + radiologyAiAnalysisResponse = apiResponse.data; + notifyListeners(); + } + } + }, + ); + + LoaderBottomSheet.hideLoader(); + } } diff --git a/lib/presentation/radiology/radiology_ai_analysis_widget.dart b/lib/presentation/radiology/radiology_ai_analysis_widget.dart new file mode 100644 index 00000000..97d35115 --- /dev/null +++ b/lib/presentation/radiology/radiology_ai_analysis_widget.dart @@ -0,0 +1,65 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.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/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; + +class RadiologyAiAnalysisWidget extends StatelessWidget { + final RadiologyAiAnalysisResponse data; + + const RadiologyAiAnalysisWidget({super.key, required this.data}); + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration(borderRadius: BorderRadius.circular(24.r), gradient: AppColors.aiLinearGradient), + padding: EdgeInsets.all(1.5.r), // Border thickness + child: Container( + decoration: BoxDecoration( + color: AppColors.whiteColor, + borderRadius: BorderRadius.circular(22.r), + ), + padding: EdgeInsets.all(20.r), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + LocaleKeys.aiAnalysis.tr().toText18(weight: FontWeight.w700, color: AppColors.blackColor), + Container( + padding: EdgeInsets.all(8.r), + decoration: BoxDecoration(gradient: AppColors.aiLinearGradient, borderRadius: BorderRadius.circular(8.r)), + child: SvgPicture.asset(AppAssets.aiOverView, colorFilter: const ColorFilter.mode(Colors.white, BlendMode.srcIn), height: 18.r, width: 18.r), + ), + ], + ), + SizedBox(height: 16.h), + if (data.summary != null && data.summary!.isNotEmpty) ...[ + data.summary!.toText14(color: AppColors.textColorLight, height: 1.5, weight: FontWeight.w400), + SizedBox(height: 20.h), + ], + 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), + ), + ], + ), + SizedBox(height: 8.h), + ], + ), + ), + ); + } +} diff --git a/lib/presentation/radiology/radiology_result_page.dart b/lib/presentation/radiology/radiology_result_page.dart index bb305c32..911cea01 100644 --- a/lib/presentation/radiology/radiology_result_page.dart +++ b/lib/presentation/radiology/radiology_result_page.dart @@ -7,11 +7,15 @@ 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/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; +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'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; @@ -23,9 +27,9 @@ import 'package:url_launcher/url_launcher.dart'; import 'dart:ui' as ui; class RadiologyResultPage extends StatefulWidget { - RadiologyResultPage({super.key, required this.patientRadiologyResponseModel}); + const RadiologyResultPage({super.key, required this.patientRadiologyResponseModel}); - PatientRadiologyResponseModel patientRadiologyResponseModel; + final PatientRadiologyResponseModel patientRadiologyResponseModel; @override State createState() => _RadiologyResultPageState(); @@ -33,6 +37,8 @@ class RadiologyResultPage extends StatefulWidget { class _RadiologyResultPageState extends State { late RadiologyViewModel radiologyViewModel; + final GlobalKey _aiAnalysisKey = GlobalKey(); + RadiologyAiAnalysisResponse? _previousAiData; @override void initState() { @@ -44,11 +50,36 @@ class _RadiologyResultPageState extends State { @override Widget build(BuildContext context) { - radiologyViewModel = Provider.of(context); + radiologyViewModel = Provider.of(context, listen: false); AppState _appState = getIt.get(); return Scaffold( backgroundColor: AppColors.bgScaffoldColor, - body: Column( + 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: [ Expanded( child: CollapsingListView( @@ -146,79 +177,102 @@ class _RadiologyResultPageState extends State { ).paddingSymmetrical(16.h, 0.h), ), SizedBox(height: 24.h), + if (radVM.radiologyAiAnalysisResponse != null) + RadiologyAiAnalysisWidget( + key: _aiAnalysisKey, + data: radVM.radiologyAiAnalysisResponse!, + ), ], ), ), ), ), ), - Container( - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.whiteColor, - borderRadius: 24.h, - hasShadow: true, - ), - child: Column( - children: [ - SizedBox(height: 24.h), - widget.patientRadiologyResponseModel.dIAPACSURL != "" - ? CustomButton( - text: LocaleKeys.openRad.tr(context: context), - onPressed: () async { - if (radiologyViewModel.radiologyImageURL.isNotEmpty) { - Uri uri = Uri.parse(radiologyViewModel.radiologyImageURL); - launchUrl(uri, mode: LaunchMode.platformDefault, webOnlyWindowName: ""); - } else { - Utils.showToast("Radiology image not available"); - } - }, - backgroundColor: AppColors.primaryRedColor, - borderColor: AppColors.primaryRedColor, - textColor: Colors.white, - fontSize: 16, - isBold: true, - borderRadius: 12, - padding: EdgeInsets.fromLTRB(10, 0, 10, 0), - height: 56.h, - icon: AppAssets.imageIcon, - iconColor: Colors.white, - iconSize: 20.h, - ).paddingSymmetrical(24.h, 0.h) - : SizedBox.shrink(), - Container( - height: 56.h, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12.r), - gradient: LinearGradient( - begin: Alignment.centerLeft, - end: Alignment.centerRight, - stops: [0.236, 1.0], // 53.6% and 100% - colors: [ - Color(0xFF8A38F5), // Transparent - Color(0xFFE20BBB), // Solid #F8F8F8 - ], - ), + radVM.radiologyAiAnalysisResponse == null + ? Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.h, + hasShadow: true, ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, + child: Column( children: [ - Padding( - padding: EdgeInsets.only(right: 4.w, left: 4.w), - child: Utils.buildSvgWithAssets(icon: AppAssets.aiOverView, width: 16.h, height: 16.h, iconColor: Colors.white), - ), - LocaleKeys.generateAiAnalysisRadResult.tr(context: context).toText16(isBold: true, color: Colors.white) + SizedBox(height: 24.h), + widget.patientRadiologyResponseModel.dIAPACSURL != "" + ? CustomButton( + text: LocaleKeys.openRad.tr(context: context), + onPressed: () async { + if (radiologyViewModel.radiologyImageURL.isNotEmpty) { + Uri uri = Uri.parse(radiologyViewModel.radiologyImageURL); + launchUrl(uri, mode: LaunchMode.platformDefault, webOnlyWindowName: ""); + } else { + Utils.showToast("Radiology image not available"); + } + }, + backgroundColor: AppColors.primaryRedColor, + borderColor: AppColors.primaryRedColor, + textColor: Colors.white, + fontSize: 16, + isBold: true, + borderRadius: 12, + padding: EdgeInsets.fromLTRB(10, 0, 10, 0), + height: 56.h, + icon: AppAssets.imageIcon, + iconColor: Colors.white, + iconSize: 20.h, + ).paddingSymmetrical(24.h, 0.h) + : SizedBox.shrink(), + Container( + height: 56.h, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12.r), + gradient: LinearGradient( + begin: Alignment.centerLeft, + end: Alignment.centerRight, + stops: [0.236, 1.0], // 53.6% and 100% + colors: [ + Color(0xFF8A38F5), // Transparent + Color(0xFFE20BBB), // Solid #F8F8F8 + ], + ), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + padding: EdgeInsets.only(right: 4.w, left: 4.w), + child: Utils.buildSvgWithAssets(icon: AppAssets.aiOverView, width: 16.h, height: 16.h, iconColor: Colors.white), + ), + LocaleKeys.generateAiAnalysisRadResult.tr(context: context).toText16(isBold: true, color: Colors.white) + ], + ), + ).paddingSymmetrical(24.h, 12.h).onPress(() async { + final _dialogService = getIt.get(); + await _dialogService.showCommonBottomSheetWithoutH( + message: LocaleKeys.aiDisclaimer.tr(), + label: LocaleKeys.consent.tr(), + okLabel: LocaleKeys.acceptLbl.tr(), + cancelLabel: LocaleKeys.rejectView.tr(), + onOkPressed: () { + context.pop(); + radiologyViewModel.getRadiologyAiAnalysis( + reportData: widget.patientRadiologyResponseModel.reportData ?? "", + loadingText: LocaleKeys.loadingAIAnalysis.tr(context: context), + ); + }, + onCancelPressed: () { + context.pop(); + }, + ); + }), ], ), - ).paddingSymmetrical(24.h, 12.h).onPress(() { - // Need to implement the Radiology AI analysis result generation functionality here - Utils.showToast("Radiology AI analysis result generation functionality is not implemented yet"); - }), - ], - ), - ), + ) + : SizedBox.shrink(), ], - ), + ); + }), ); } } diff --git a/lib/presentation/symptoms_checker/widgets/selected_organs_section.dart b/lib/presentation/symptoms_checker/widgets/selected_organs_section.dart index 13688e0d..2918173f 100644 --- a/lib/presentation/symptoms_checker/widgets/selected_organs_section.dart +++ b/lib/presentation/symptoms_checker/widgets/selected_organs_section.dart @@ -78,7 +78,7 @@ class _SelectedOrgansSectionState extends State { style: TextStyle( color: AppColors.whiteColor, fontSize: 12.f, - isBold: true, + // isBold: true, ), ), ), diff --git a/lib/widgets/datepicker_widget.dart b/lib/widgets/datepicker_widget.dart index bbbd2a0a..56f86baf 100644 --- a/lib/widgets/datepicker_widget.dart +++ b/lib/widgets/datepicker_widget.dart @@ -102,7 +102,7 @@ class DatePickerWidget extends StatelessWidget { style: TextStyle( fontSize: 14.f, height: 21 / 14, - isBold: true, + // isBold: true, color: (selectedValue != null && selectedValue!.isNotEmpty) ? const Color(0xff2E3039) : const Color(0xffB0B0B0), letterSpacing: -0.2, ),