Upgrade to version 1.4.1.1 with .NET 8.0 support

- Updated application version from 1.3.2.1 to 1.4.1.1.
- Dockerfile now uses .NET 8.0 for runtime and SDK.
- Added new API endpoints in HospitalController for sections by hospital ID.
- Modified UniversityController to retrieve hospitals instead of sections.
- Enhanced DocuMed.Api.csproj with new using directives for commands and queries.
- Updated AccountService and UserService to handle hospital IDs and sections.
- Introduced HospitalId and ProfileType properties in various DTOs.
- Updated RestApiWrapper to include new REST API interfaces for hospitals and AI.
- Enhanced MedicalHistoryTemplate components with AI interaction features.
- Modified CSS files to include new styles and animations.
- Introduced new classes and interfaces for AI interactions, including AiController and MetisMessage.
- Updated ProfilePage to support hospital selection and related logic.
- Revised API endpoints for hospitals and universities for improved data management.
master
Amir Hossein Khademi 2025-04-29 10:58:16 +03:30
parent ab56bf3c20
commit 3fb22c8c48
36 changed files with 590 additions and 72 deletions

View File

@ -1 +1 @@
1.3.2.1
1.4.1.1

View File

@ -1,9 +1,9 @@
FROM mcr.microsoft.com/dotnet/aspnet:7.0 AS base
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
ENV ASPNETCORE_URLS=http://0.0.0.0:8010
WORKDIR /app
EXPOSE 8010
FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY ["DocuMed.Api/DocuMed.Api.csproj", "DocuMed.Api/"]
RUN dotnet restore "DocuMed.Api/DocuMed.Api.csproj"

View File

@ -0,0 +1,26 @@
namespace DocuMed.Api.Controllers;
public class AiController : ICarterModule
{
public void AddRoutes(IEndpointRouteBuilder app)
{
var group = app.NewVersionedApi("Ai").MapGroup("api/ai");
group.MapPost("chat", ChatAsync)
.WithDisplayName("AiChatBot")
.HasApiVersion(1.0);
}
private async Task<IResult> ChatAsync([FromBody] MetisMessage message, [FromServices] IRestApiWrapper apiWrapper, CancellationToken cancellationToken)
{
var messageRequest = new MetisMessageRequest
{
message = new MetisMessage
{
content = message.content
}
};
var response = await apiWrapper.MetisRestApi.SendMessage("7324c5a0-5cad-4239-a8d9-38d99d490493", messageRequest, "tpsg-epC8BoLfa7uSL4ogjlocFLKiW7Un66e");
return TypedResults.Ok(response.Content);
}
}

View File

@ -1,8 +1,4 @@
using DocuMed.Domain.CommandQueries.Commands;
using DocuMed.Domain.Entities.MedicalHistory;
using MediatR;
namespace DocuMed.Api.Controllers;
namespace DocuMed.Api.Controllers;
public class HospitalController : ICarterModule
{
@ -12,6 +8,9 @@ public class HospitalController : ICarterModule
.MapGroup("api/hospital")
.RequireAuthorization(builder => builder.AddAuthenticationSchemes("Bearer").RequireAuthenticatedUser());
group.MapGet("{id}/section", GetSectionsAsync)
.WithDisplayName("Get All Sections")
.HasApiVersion(1.0);
group.MapGet("", GetAllAsync)
.WithDisplayName("GetAll")
@ -31,37 +30,31 @@ public class HospitalController : ICarterModule
.HasApiVersion(1.0);
}
// GET:Get All Entity
private async Task<IResult> GetAllAsync([FromQuery] int page, IMedicalHistoryRepository repository, CancellationToken cancellationToken)
// GET:Get All Sections
public virtual async Task<IResult> GetSectionsAsync(Guid id, IRepositoryWrapper repositoryWrapper, ICurrentUserService currentUserService, CancellationToken cancellationToken)
{
return TypedResults.Ok(await repository.GetMedicalHistoriesAsync(page, cancellationToken));
return TypedResults.Ok(await repositoryWrapper.SetRepository<Section>().TableNoTracking
.Where(s => s.HospitalId == id)
.Select(SectionMapper.ProjectToSDto).ToListAsync(cancellationToken));
}
// GET:Get All Entity
private async Task<IResult> GetAllAsync([FromQuery] int page, IMediator mediator, CancellationToken cancellationToken)
=> TypedResults.Ok(await mediator.Send(new GetHospitalsQuery(page), cancellationToken));
// GET:Get An Entity By Id
private async Task<IResult> GetAsync(Guid id, IMedicalHistoryRepository repository, CancellationToken cancellationToken)
{
return TypedResults.Ok(await repository.GetMedicalHistoryAsync(id, cancellationToken));
}
private async Task<IResult> GetAsync(Guid id, IMediator mediator, CancellationToken cancellationToken)
=> TypedResults.Ok(await mediator.Send(new GetHospitalQuery(id), cancellationToken));
// POST:Add New Entity
private async Task<IResult> Post([FromBody] CreateHospitalCommand dto, IMediator service, ICurrentUserService currentUserService, CancellationToken cancellationToken)
{
return TypedResults.Ok(await service.Send(dto,cancellationToken));
}
=> TypedResults.Ok(await service.Send(dto, cancellationToken));
// PUT:Update Entity
private async Task<IResult> Put([FromBody] UpdateHospitalCommand dto, IMediator service, ICurrentUserService currentUserService, CancellationToken cancellationToken)
{
return TypedResults.Ok(await service.Send(dto,cancellationToken));
}
=> TypedResults.Ok(await service.Send(dto, cancellationToken));
// DELETE:Delete Entity
private async Task<IResult> Delete(Guid id, IRepositoryWrapper repositoryWrapper, CancellationToken cancellationToken)
{
var ent = await repositoryWrapper.SetRepository<MedicalHistory>().GetByIdAsync(cancellationToken, id);
repositoryWrapper.SetRepository<MedicalHistory>().Delete(ent);
await repositoryWrapper.SaveChangesAsync(cancellationToken);
return TypedResults.Ok();
}
private async Task<IResult> Delete(Guid id, IMediator mediator, CancellationToken cancellationToken)
=> TypedResults.Ok(await mediator.Send(new DeleteHospitalCommand(id), cancellationToken));
}

View File

@ -14,7 +14,7 @@ public class UniversityController : ICarterModule
.WithDisplayName("Get All")
.HasApiVersion(1.0);
group.MapGet("{id}/section", GetAllByUniversityAsync)
group.MapGet("{id}/hospital", GetHospitalsAsync)
.WithDisplayName("Get All Sections")
.HasApiVersion(1.0);
@ -34,25 +34,25 @@ public class UniversityController : ICarterModule
// GET:Get All Sections
public virtual async Task<IResult> GetAllByUniversityAsync(Guid id, IRepositoryWrapper repositoryWrapper, ICurrentUserService currentUserService, CancellationToken cancellationToken)
private async Task<IResult> GetHospitalsAsync(Guid id, IRepositoryWrapper repositoryWrapper, ICurrentUserService currentUserService, CancellationToken cancellationToken)
{
return TypedResults.Ok(await repositoryWrapper.SetRepository<Section>().TableNoTracking
.Where(s => s.HospitalId == id)
.Select(SectionMapper.ProjectToSDto).ToListAsync(cancellationToken));
return TypedResults.Ok(await repositoryWrapper.SetRepository<Hospital>().TableNoTracking
.Where(s => s.UniversityId == id)
.Select(HospitalMapper.ProjectToSDto).ToListAsync(cancellationToken));
}
// GET:Get All Entity
public virtual async Task<IResult> GetAllAsync([FromQuery] int page, IRepositoryWrapper repositoryWrapper, CancellationToken cancellationToken)
private async Task<IResult> GetAllAsync([FromQuery] int page, IRepositoryWrapper repositoryWrapper, CancellationToken cancellationToken)
=> TypedResults.Ok(await repositoryWrapper.SetRepository<University>()
.TableNoTracking
.Select(UniversityMapper.ProjectToSDto).ToListAsync(cancellationToken));
// GET:Get An Entity By Id
public async Task<IResult> GetAsync(int id, IRepositoryWrapper repositoryWrapper, CancellationToken cancellationToken)
private async Task<IResult> GetAsync(int id, IRepositoryWrapper repositoryWrapper, CancellationToken cancellationToken)
=> TypedResults.Ok(await repositoryWrapper.SetRepository<University>().GetByIdAsync(cancellationToken, id));
// POST:Add New Entity
public virtual async Task<IResult> Post([FromBody] UniversitySDto dto, IRepositoryWrapper repositoryWrapper, CancellationToken cancellationToken)
private async Task<IResult> Post([FromBody] UniversitySDto dto, IRepositoryWrapper repositoryWrapper, CancellationToken cancellationToken)
{
var ent = University.Create(dto.Name,dto.Address,dto.CityId);
repositoryWrapper.SetRepository<University>().Add(ent);
@ -61,7 +61,7 @@ public class UniversityController : ICarterModule
}
// PUT:Update Entity
public virtual async Task<IResult> Put([FromBody] UniversitySDto dto, IRepositoryWrapper repositoryWrapper, CancellationToken cancellationToken)
private async Task<IResult> Put([FromBody] UniversitySDto dto, IRepositoryWrapper repositoryWrapper, CancellationToken cancellationToken)
{
var ent = University.Create(dto.Name,dto.Address,dto.CityId);
ent.Id = dto.Id;
@ -71,7 +71,7 @@ public class UniversityController : ICarterModule
}
// DELETE:Delete Entity
public virtual async Task<IResult> Delete(int id, IRepositoryWrapper repositoryWrapper, CancellationToken cancellationToken)
private async Task<IResult> Delete(int id, IRepositoryWrapper repositoryWrapper, CancellationToken cancellationToken)
{
var ent = await repositoryWrapper.SetRepository<University>().GetByIdAsync(cancellationToken, id);
repositoryWrapper.SetRepository<University>().Delete(ent);

View File

@ -6,8 +6,8 @@
<ImplicitUsings>enable</ImplicitUsings>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
<DockerComposeProjectPath>..\docker-compose.dcproj</DockerComposeProjectPath>
<AssemblyVersion>1.3.2.1</AssemblyVersion>
<FileVersion>1.3.2.1</FileVersion>
<AssemblyVersion>1.4.1.1</AssemblyVersion>
<FileVersion>1.4.1.1</FileVersion>
</PropertyGroup>
<ItemGroup>
@ -74,10 +74,13 @@
<Using Include="DocuMed.Core.EntityServices.Abstracts" />
<Using Include="DocuMed.Core.Models.Api" />
<Using Include="DocuMed.Domain" />
<Using Include="DocuMed.Domain.CommandQueries.Commands" />
<Using Include="DocuMed.Domain.CommandQueries.Queries" />
<Using Include="DocuMed.Domain.Dtos.LargDtos" />
<Using Include="DocuMed.Domain.Dtos.RequestDtos" />
<Using Include="DocuMed.Domain.Dtos.SmallDtos" />
<Using Include="DocuMed.Domain.Entities.City" />
<Using Include="DocuMed.Domain.Entities.Hospitals" />
<Using Include="DocuMed.Domain.Entities.MedicalHistoryTemplate" />
<Using Include="DocuMed.Domain.Entities.User" />
<Using Include="DocuMed.Domain.Enums.QueryFilters" />
@ -85,12 +88,15 @@
<Using Include="DocuMed.Domain.Models.Settings" />
<Using Include="DocuMed.Infrastructure" />
<Using Include="DocuMed.Infrastructure.Models" />
<Using Include="DocuMed.Infrastructure.Models.Metis" />
<Using Include="DocuMed.Infrastructure.RestServices" />
<Using Include="DocuMed.Repository" />
<Using Include="DocuMed.Repository.Abstracts" />
<Using Include="DocuMed.Repository.Extensions" />
<Using Include="DocuMed.Repository.Models" />
<Using Include="DocuMed.Repository.Repositories.Base.Contracts" />
<Using Include="DocuMed.Repository.Repositories.Entities.Abstracts" />
<Using Include="MediatR" />
<Using Include="MediatR.Extensions.Autofac.DependencyInjection" />
<Using Include="Microsoft.AspNetCore.Authentication.JwtBearer" />
<Using Include="Microsoft.AspNetCore.Identity" />

View File

@ -141,6 +141,8 @@ public class AccountService(
if (section != null)
{
token.User.SectionName = section.Name;
token.User.SectionId = section.Id;
token.User.HospitalId = section.HospitalId;
}
}

View File

@ -1,4 +1,6 @@
namespace DocuMed.Core.EntityServices;
using DocuMed.Domain.Entities.Staffs;
namespace DocuMed.Core.EntityServices;
public class UserService(
@ -85,6 +87,23 @@ public class UserService(
user.BirthDate = request.BirthDate;
user.Gender = request.Gender;
switch (request.ProfileType)
{
case ProfileType.Student:
var student = await repositoryWrapper.SetRepository<Student>().TableNoTracking
.FirstOrDefaultAsync(s => s.UserId == user.Id, cancellationToken);
if (student == null)
throw new AppException("Student not found", ApiResultStatusCode.NotFound);
student.SetSection(request.SectionId);
repositoryWrapper.SetRepository<Student>().Update(student);
await repositoryWrapper.SaveChangesAsync(cancellationToken);
break;
case ProfileType.Patient:
break;
default:
throw new ArgumentOutOfRangeException();
}
var result = await userManager.UpdateAsync(user);
if (!result.Succeeded)
throw new AppException(string.Join('|', result.Errors));

View File

@ -13,4 +13,6 @@ public class UserActionRequestDto
public string RoleName { get; set; } = string.Empty;
public Guid UniversityId { get; set; }
public Guid SectionId { get; set; }
public Guid HospitalId { get; set; }
public ProfileType ProfileType { get; set; }
}

View File

@ -14,6 +14,8 @@ public class ApplicationUserSDto : BaseDto<ApplicationUserSDto,ApplicationUser>
public Gender Gender { get; set; }
public SignUpStatus SignUpStatus { get; set; }
public Guid UniversityId { get; set; }
public Guid HospitalId { get; set; }
public string HospitalName { get; set; }
public Guid SectionId { get; set; }
public string SectionName { get; set; } = string.Empty;

View File

@ -0,0 +1,7 @@
namespace DocuMed.Domain.Enums;
public enum ProfileType
{
Student,
Patient
}

View File

@ -8,6 +8,7 @@
<ItemGroup>
<PackageReference Include="Refit" Version="7.2.1" />
<PackageReference Include="Refit.Newtonsoft.Json" Version="7.2.1" />
</ItemGroup>
<ItemGroup>
@ -27,6 +28,7 @@
<Using Include="DocuMed.Core.Abstracts" />
<Using Include="DocuMed.Domain.Models.Settings" />
<Using Include="DocuMed.Infrastructure.Models" />
<Using Include="DocuMed.Infrastructure.Models.Metis" />
<Using Include="DocuMed.Infrastructure.Models.RestApi.KaveNegar" />
<Using Include="DocuMed.Infrastructure.RestServices" />
<Using Include="Microsoft.Extensions.Logging" />

View File

@ -0,0 +1,9 @@
namespace DocuMed.Infrastructure.Models.Metis;
public class CreateSessionRequestDto
{
public string BotId { get; set; } = string.Empty;
public MetisUser? User { get; set; }
public MetisMessageRequest? InitializeMessage { get; set; }
}

View File

@ -0,0 +1,12 @@
namespace DocuMed.Infrastructure.Models.Metis;
public class MetisMessageRequest
{
public MetisMessage message { get; set; } = new();
}
public class MetisMessage
{
public string content { get; set; } = string.Empty;
public string type { get; set; } = "USER";
}

View File

@ -0,0 +1,13 @@
namespace DocuMed.Infrastructure.Models.Metis;
public class MetisMessageResponse
{
public string Id { get; set; } = string.Empty;
public string Type { get; set; } = string.Empty;
public string Content { get; set; } = string.Empty;
public object Attachments { get; set; } = string.Empty;
public long Timestamp { get; set; }
public string FinishReason { get; set; } = string.Empty;
public object Citations { get; set; } = string.Empty;
public object ToolCalls { get; set; } = string.Empty;
}

View File

@ -0,0 +1,7 @@
namespace DocuMed.Infrastructure.Models.Metis;
public class MetisUser
{
public string Name { get; set; } = string.Empty;
public string Id { get; set; } = Guid.NewGuid().ToString("N").Substring(0, 8);
}

View File

@ -0,0 +1,13 @@
namespace DocuMed.Infrastructure.RestServices;
public interface IMetisRestApi
{
[Post("/chat/session")]
public Task CreateSession(string sessionId, [Body] CreateSessionRequestDto request, [Header("x-api-key")] string metisToken);
[Post("/chat/session/{sessionId}/message")]
public Task<MetisMessageResponse> SendMessage(string sessionId, [Body] MetisMessageRequest request, [Header("x-api-key")] string metisToken);
[Post("/chat/session/{sessionId}/message/stream")]
public Task<MetisMessageResponse> SendStreamMessage(string sessionId, [Body] MetisMessageRequest request, [Header("x-api-key")] string metisToken);
}

View File

@ -1,11 +1,21 @@
namespace DocuMed.Infrastructure.RestServices;
using Newtonsoft.Json;
namespace DocuMed.Infrastructure.RestServices;
public interface IRestApiWrapper : IScopedDependency
{
IKaveNegarRestApi KaveNegarRestApi { get; }
IMetisRestApi MetisRestApi { get; }
}
public class RestApiWrapper : IRestApiWrapper
{
private static readonly RefitSettings setting = new RefitSettings(new NewtonsoftJsonContentSerializer(new JsonSerializerSettings
{
ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
}));
public IKaveNegarRestApi KaveNegarRestApi => RestService.For<IKaveNegarRestApi>(RestAddress.BaseKaveNegar);
public IMetisRestApi MetisRestApi => RestService.For<IMetisRestApi>("https://api.metisai.ir/api/v1", setting);
}

View File

@ -7,8 +7,8 @@
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<ServiceWorkerAssetsManifest>service-worker-assets.js</ServiceWorkerAssetsManifest>
<AssemblyVersion>1.3.2.1</AssemblyVersion>
<FileVersion>1.3.2.1</FileVersion>
<AssemblyVersion>1.4.1.1</AssemblyVersion>
<FileVersion>1.4.1.1</FileVersion>
</PropertyGroup>
<ItemGroup>

View File

@ -8,12 +8,15 @@ public static class Address
#else
public static string BaseAddress = "https://api.documed.ir/api";
#endif
public static string AuthController = $"{BaseAddress}/auth";
public static string CityController = $"{BaseAddress}/city";
public static string UniversityController = $"{BaseAddress}/university";
public static string AiController = $"{BaseAddress}/ai";
public static string SectionController = $"{BaseAddress}/section";
public static string UserController = $"{BaseAddress}/user";
public static string MedicalHistoryTemplateController = $"{BaseAddress}/medicalhistory/template";
public static string MedicalHistoryController = $"{BaseAddress}/medicalhistory";
public static string PatientController = $"{BaseAddress}/patient";
public static string HospitalController = $"{BaseAddress}/hospital";
}

View File

@ -221,7 +221,7 @@
{
var token = await UserUtility.GetBearerTokenAsync();
var user = await UserUtility.GetUserAsync();
Sections = await RestWrapper.SectionRestApi.GetByUniversityAsync(user.UniversityId, token);
Sections = await RestWrapper.HospitalRestApi.GetSectionsAsync(user.HospitalId, token);
if (section.IsNullOrEmpty())
return Sections;

View File

@ -27,17 +27,17 @@
</MudCarouselItem>
<MudCarouselItem>
<div class="flex flex-col h-full p-4">
<MedicalHistoryTemplateActionStep2 PiQuestions="@ViewModel.PiQuestions" />
<MedicalHistoryTemplateActionStep2 ChiefComplaint="@ViewModel.PageDto.ChiefComplaint" PiQuestions="@ViewModel.PiQuestions" />
</div>
</MudCarouselItem>
<MudCarouselItem>
<div class="flex flex-col h-full p-4">
<MedicalHistoryTemplateActionStep3 PdhQuestions="@ViewModel.PdhQuestions" PshQuestions="@ViewModel.PshQuestions" />
<MedicalHistoryTemplateActionStep3 ChiefComplaint="@ViewModel.PageDto.ChiefComplaint" PdhQuestions="@ViewModel.PdhQuestions" PshQuestions="@ViewModel.PshQuestions" />
</div>
</MudCarouselItem>
<MudCarouselItem>
<div class="flex flex-col h-full p-4">
<MedicalHistoryTemplateActionStep4 FamilyHistories="@ViewModel.FhQuestions" DrugHistories="@ViewModel.DhQuestions" AhMedicines="@ViewModel.AhQuestions" />
<MedicalHistoryTemplateActionStep4 ChiefComplaint="@ViewModel.PageDto.ChiefComplaint" FamilyHistories="@ViewModel.FhQuestions" DrugHistories="@ViewModel.DhQuestions" AhMedicines="@ViewModel.AhQuestions" />
</div>
</MudCarouselItem>
<MudCarouselItem>

View File

@ -66,7 +66,7 @@
{
var token = await UserUtility.GetBearerTokenAsync();
var user = await UserUtility.GetUserAsync();
Sections = await RestWrapper.SectionRestApi.GetByUniversityAsync(user.UniversityId, token);
Sections = await RestWrapper.HospitalRestApi.GetSectionsAsync(user.HospitalId, token);
if (section.IsNullOrEmpty())
return Sections;

View File

@ -1,4 +1,5 @@
@using DocuMed.Domain.Entities.MedicalHistoryTemplate
@inject IRestWrapper RestWrapper
@inject ISnackbar Snackbar
<MudStack class="pb-20 font-iranyekan">
<BasePartDivider Index="2" Title="تاریخچه بیماری فعلی ( PI )" />
@ -19,24 +20,47 @@
Variant="Variant.Outlined" />
@* <MudAutocomplete T="string" Label="وابستگی به سوال قبلی" Variant="Variant.Outlined" /> *@
<MudButton @onclick="AddQuestion" Variant="Variant.Filled" DisableElevation="true"
class="font-extrabold text-lg right-0 rounded-md py-3 bg-[--color-medicalhistory] text-gray-800">
class="font-extrabold text-lg right-0 rounded-md py-3 bg-[--color-medicalhistory] text-gray-800">
+ افزودن
</MudButton>
@AiResponse
<button @onclick="AskWithAi" class="relative inline-flex h-12 overflow-hidden rounded-full p-[1px] ">
<span class="absolute inset-[-1000%] animate-[spin_2s_linear_infinite] bg-[conic-gradient(from_90deg_at_50%_50%,#E2CBFF_0%,#393BB2_50%,#E2CBFF_100%)]"></span>
<span class="inline-flex h-full w-full cursor-pointer items-center justify-center rounded-full bg-white px-4 py-2 text-sm font-medium text-black backdrop-blur-3xl">
<p class="font-bold bg-gradient-to-r from-sky-600 via-violet-500 to-fuchsia-400 inline-block text-transparent bg-clip-text">
از هوش مصنوعی بپرس !
</p>
</span>
</button>
@if (IsProcessing)
{
<p class="mx-2 -mt-2 bg-gradient-to-r from-sky-600 via-blue-500 to-violet-400 inline-block text-transparent bg-clip-text">
در حال فکر کردن ....
</p>
}
</MudStack>
@* focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 focus:ring-offset-slate-50 *@
@code
{
private MedicalHistoryQuestionType _questionType;
private string _questionTitle = string.Empty;
private MedicalHistoryPart _questionPart = MedicalHistoryPart.PresentIllness;
private bool IsProcessing { get; set; }
private MarkupString AiResponse { get; set; }
[Parameter]
public List<MedicalHistoryQuestionSDto> PiQuestions { get; set; } = new();
[Parameter]
public string ChiefComplaint { get; set; } = string.Empty;
private void RemoveQuestion(MedicalHistoryQuestionSDto question)
{
PiQuestions.Remove(question);
@ -44,11 +68,43 @@
private void AddQuestion()
{
PiQuestions.Add(new MedicalHistoryQuestionSDto
{
Part = _questionPart,
Question = _questionTitle,
QuestionType = _questionType
});
{
Part = _questionPart,
Question = _questionTitle,
QuestionType = _questionType
});
_questionTitle = string.Empty;
}
private async Task AskWithAi()
{
try
{
IsProcessing = true;
var request = new
{
content = $"شکایت اصلی بیمار (CC) {ChiefComplaint} است. لطفاً سوالات بخش PI (Present Illness) را در سه دسته زیر تولید کنید: سوالات توضیحی (Open-ended): سوالاتی که بیمار باید توضیح دهد. سوالات بله/خیر (Yes/No): سوالاتی که پاسخ مشخص بله یا خیر دارند. سوالات زمانی (Time-based): سوالاتی مرتبط با زمان شروع، مدت و تغییرات مشکل. لطفاً سوالات مرتبط با سردرد شامل شدت، محل، عوامل تشدیدکننده یا تسکین‌دهنده و علائم همراه (مثل تهوع یا تاری دید) باشد. use html concept for response and just send html and remove , head , html and body tag"
};
var response = await RestWrapper.AiRestApi.ChatAsync(request);
response = response.Replace("```html", null);
response = response.Replace("```", null);
response = response.Replace("\\n", null);
response = response.Replace(@"""", null);
AiResponse = (MarkupString)response;
}
catch (ApiException ex)
{
var exe = await ex.GetContentAsAsync<ApiResult>();
Snackbar.Add(exe != null ? exe.Message : ex.Content, Severity.Error);
}
catch (Exception e)
{
Snackbar.Add(e.Message, Severity.Error);
}
finally
{
IsProcessing = false;
}
}
}

View File

@ -1,5 +1,5 @@
@using DocuMed.Domain.Entities.MedicalHistoryTemplate
@inject IRestWrapper RestWrapper
@inject ISnackbar Snackbar
<MudStack class="pb-20 font-iranyekan">
<BasePartDivider Index="3" Title="تاریخچه بیماری قبلی ( PMH )" />
@ -22,6 +22,25 @@
+ افزودن
</MudButton>
@AiPMHResponse
<button @onclick="AskPMHWithAi" class="relative inline-flex h-12 overflow-hidden rounded-full p-[1px] ">
<span class="absolute inset-[-1000%] animate-[spin_2s_linear_infinite] bg-[conic-gradient(from_90deg_at_50%_50%,#E2CBFF_0%,#393BB2_50%,#E2CBFF_100%)]"></span>
<span class="inline-flex h-full w-full cursor-pointer items-center justify-center rounded-full bg-white px-4 py-2 text-sm font-medium text-black backdrop-blur-3xl">
<p class="font-bold bg-gradient-to-r from-sky-600 via-violet-500 to-fuchsia-400 inline-block text-transparent bg-clip-text">
از هوش مصنوعی بپرس !
</p>
</span>
</button>
@if (IsProcessing)
{
<p class="mx-2 -mt-2 bg-gradient-to-r from-sky-600 via-blue-500 to-violet-400 inline-block text-transparent bg-clip-text">
در حال فکر کردن ....
</p>
}
<BasePartDivider Index="4" class="mt-9" Title="تاریخچه جراحی های قبلی ( PSH )" />
@foreach (var item in PshQuestions)
@ -50,6 +69,23 @@
+ افزودن
</MudButton>
@AiPSHResponse
<button @onclick="AskPSHWithAi" class="relative inline-flex h-12 overflow-hidden rounded-full p-[1px] ">
<span class="absolute inset-[-1000%] animate-[spin_2s_linear_infinite] bg-[conic-gradient(from_90deg_at_50%_50%,#E2CBFF_0%,#393BB2_50%,#E2CBFF_100%)]"></span>
<span class="inline-flex h-full w-full cursor-pointer items-center justify-center rounded-full bg-white px-4 py-2 text-sm font-medium text-black backdrop-blur-3xl">
<p class="font-bold bg-gradient-to-r from-sky-600 via-violet-500 to-fuchsia-400 inline-block text-transparent bg-clip-text">
از هوش مصنوعی بپرس !
</p>
</span>
</button>
@if (IsProcessing)
{
<p class="mx-2 -mt-2 bg-gradient-to-r from-sky-600 via-blue-500 to-violet-400 inline-block text-transparent bg-clip-text">
در حال فکر کردن ....
</p>
}
</MudStack>
@ -60,11 +96,12 @@
[Parameter]
public List<MedicalHistoryQuestionSDto> PshQuestions { get; set; } = new();
private string _pdhQuestionTitle = string.Empty;
private MedicalHistoryQuestionType _pdhQuestionType;
private string _pshQuestionTitle = string.Empty;
private MedicalHistoryQuestionType _pshQuestionType;
[Parameter]
public string ChiefComplaint { get; set; } = string.Empty;
private void RemovePiQuestion(MedicalHistoryQuestionSDto question)
{
@ -96,4 +133,72 @@
_pshQuestionTitle = string.Empty;
}
private bool IsProcessing { get; set; }
private MarkupString AiPMHResponse { get; set; }
private async Task AskPMHWithAi()
{
try
{
IsProcessing = true;
var request = new
{
content = $"شکایت اصلی بیمار (CC) {ChiefComplaint} است. لطفاً سوالات بخش تاریخچه بیماری قبلی ( Past Medical History ) را در سه دسته زیر تولید کنید: سوالات توضیحی (Open-ended): سوالاتی که بیمار باید توضیح دهد. سوالات بله/خیر (Yes/No): سوالاتی که پاسخ مشخص بله یا خیر دارند. سوالات زمانی (Time-based): سوالاتی مرتبط با زمان شروع، مدت و تغییرات مشکل. لطفاً سوالات مرتبط با سردرد شامل شدت، محل، عوامل تشدیدکننده یا تسکین‌دهنده و علائم همراه (مثل تهوع یا تاری دید) باشد. use html concept for response and just send html and remove , head , html and body tag"
};
var response = await RestWrapper.AiRestApi.ChatAsync(request);
response = response.Replace("```html", null);
response = response.Replace("```", null);
response = response.Replace("\\n", null);
response = response.Replace(@"""", null);
AiPMHResponse = (MarkupString)response;
}
catch (ApiException ex)
{
var exe = await ex.GetContentAsAsync<ApiResult>();
Snackbar.Add(exe != null ? exe.Message : ex.Content, Severity.Error);
}
catch (Exception e)
{
Snackbar.Add(e.Message, Severity.Error);
}
finally
{
IsProcessing = false;
}
}
private MarkupString AiPSHResponse { get; set; }
private async Task AskPSHWithAi()
{
try
{
IsProcessing = true;
var request = new
{
content = $"شکایت اصلی بیمار (CC) {ChiefComplaint} است. لطفاً سوالات بخش تاریخچه جراحی های قبلی ( Past Surgery History ) را در سه دسته زیر تولید کنید: سوالات توضیحی (Open-ended): سوالاتی که بیمار باید توضیح دهد. سوالات بله/خیر (Yes/No): سوالاتی که پاسخ مشخص بله یا خیر دارند. سوالات زمانی (Time-based): سوالاتی مرتبط با زمان شروع، مدت و تغییرات مشکل. لطفاً سوالات مرتبط با سردرد شامل شدت، محل، عوامل تشدیدکننده یا تسکین‌دهنده و علائم همراه (مثل تهوع یا تاری دید) باشد. use html concept for response and just send html and remove , head , html and body tag"
};
var response = await RestWrapper.AiRestApi.ChatAsync(request);
response = response.Replace("```html", null);
response = response.Replace("```", null);
response = response.Replace("\\n", null);
response = response.Replace(@"""", null);
AiPSHResponse = (MarkupString)response;
}
catch (ApiException ex)
{
var exe = await ex.GetContentAsAsync<ApiResult>();
Snackbar.Add(exe != null ? exe.Message : ex.Content, Severity.Error);
}
catch (Exception e)
{
Snackbar.Add(e.Message, Severity.Error);
}
finally
{
IsProcessing = false;
}
}
}

View File

@ -1,4 +1,5 @@
@using DocuMed.Domain.Entities.MedicalHistoryTemplate
@inject IRestWrapper RestWrapper
@inject ISnackbar Snackbar
<MudStack class="pb-20 font-iranyekan">
<BasePartDivider Index="5" Title="تاریخچه بیماری های خانوادگی ( FH )" />
@ -22,6 +23,23 @@
+ افزودن
</MudButton>
@AiResponse
<button @onclick="AskWithAi" class="relative inline-flex h-12 overflow-hidden rounded-full p-[1px] ">
<span class="absolute inset-[-1000%] animate-[spin_2s_linear_infinite] bg-[conic-gradient(from_90deg_at_50%_50%,#E2CBFF_0%,#393BB2_50%,#E2CBFF_100%)]"></span>
<span class="inline-flex h-full w-full cursor-pointer items-center justify-center rounded-full bg-white px-4 py-2 text-sm font-medium text-black backdrop-blur-3xl">
<p class="font-bold bg-gradient-to-r from-sky-600 via-violet-500 to-fuchsia-400 inline-block text-transparent bg-clip-text">
از هوش مصنوعی بپرس !
</p>
</span>
</button>
@if (IsProcessing)
{
<p class="mx-2 -mt-2 bg-gradient-to-r from-sky-600 via-blue-500 to-violet-400 inline-block text-transparent bg-clip-text">
در حال فکر کردن ....
</p>
}
<BasePartDivider Index="6" class="mt-9" Title="داروهای مصرفی ( DH )" />
@ -66,9 +84,14 @@
</div>
</MudStack>
@code {
@code
{
private bool IsProcessing { get; set; }
private MarkupString AiResponse { get; set; }
[Parameter]
public string ChiefComplaint { get; set; } = string.Empty;
private string _familyHistoryQuestionTitle = string.Empty;
private MedicalHistoryQuestionType _familyHistoryQuestionType;
[Parameter]
@ -125,4 +148,37 @@
});
_hhName = string.Empty;
}
private async Task AskWithAi()
{
try
{
IsProcessing = true;
var request = new
{
content = $"شکایت اصلی بیمار (CC) {ChiefComplaint} است. لطفاً سوالات بخش PI (Present Illness) را در سه دسته زیر تولید کنید: سوالات توضیحی (Open-ended): سوالاتی که بیمار باید توضیح دهد. سوالات بله/خیر (Yes/No): سوالاتی که پاسخ مشخص بله یا خیر دارند. سوالات زمانی (Time-based): سوالاتی مرتبط با زمان شروع، مدت و تغییرات مشکل. لطفاً سوالات مرتبط با سردرد شامل شدت، محل، عوامل تشدیدکننده یا تسکین‌دهنده و علائم همراه (مثل تهوع یا تاری دید) باشد. use html concept for response and just send html and remove , head , html and body tag"
};
var response = await RestWrapper.AiRestApi.ChatAsync(request);
response = response.Replace("```html", null);
response = response.Replace("```", null);
response = response.Replace("\\n", null);
response = response.Replace(@"""", null);
AiResponse = (MarkupString)response;
}
catch (ApiException ex)
{
var exe = await ex.GetContentAsAsync<ApiResult>();
Snackbar.Add(exe != null ? exe.Message : ex.Content, Severity.Error);
}
catch (Exception e)
{
Snackbar.Add(e.Message, Severity.Error);
}
finally
{
IsProcessing = false;
}
}
}

View File

@ -52,6 +52,25 @@
</ItemTemplate>
</MudAutocomplete>
<MudAutocomplete @bind-Value="@ViewModel.SelectedHospital"
ToStringFunc="dto => dto.Name"
SearchFunc="@ViewModel.SearchHospital"
T="HospitalSDto" Label="بیمارستان" Variant="Variant.Outlined">
<ProgressIndicatorInPopoverTemplate>
<MudList Clickable="false">
<MudListItem>
<div class="flex flex-row w-full mx-auto">
<MudProgressCircular class="my-auto mr-1 -ml-4" Size="Size.Small" Indeterminate="true" />
<p class="font-bold my-1 mx-auto text-md">منتظر بمانید</p>
</div>
</MudListItem>
</MudList>
</ProgressIndicatorInPopoverTemplate>
<ItemTemplate Context="e">
<p>@e.Name</p>
</ItemTemplate>
</MudAutocomplete>
<MudAutocomplete T="SectionSDto" Label="بخش فعلی" Variant="Variant.Outlined"
ToStringFunc="dto => dto.Name"
SearchFunc="@ViewModel.SearchSection"

View File

@ -1,4 +1,5 @@
using DocuMed.Domain.Entities.City;
using DocuMed.Domain.Entities.Hospitals;
using Mapster;
namespace DocuMed.PWA.Pages;
@ -18,10 +19,12 @@ public class ProfilePageViewModel(
public List<CitySDto> Cities { get; private set; } = new List<CitySDto>();
public List<UniversitySDto> Universities { get; private set; } = new List<UniversitySDto>();
public List<HospitalSDto> Hospitals { get; private set; } = new List<HospitalSDto>();
public List<SectionSDto> Sections { get; private set; } = new List<SectionSDto>();
public SectionSDto? SelectedSection { get; set; }
public CitySDto? SelectedCity { get; set; }
public UniversitySDto? SelectedUni { get; set; }
public HospitalSDto? SelectedHospital { get; set; }
public readonly string Version = Assembly.GetAssembly(typeof(Program))?.GetName()?.Version?.ToString() ?? string.Empty;
@ -37,12 +40,14 @@ public class ProfilePageViewModel(
{
SelectedUni = Universities.FirstOrDefault(u => u.Id == User.UniversityId);
SelectedCity = Cities.FirstOrDefault(c => c.Id == SelectedUni?.CityId);
if (SelectedUni != null)
if (SelectedHospital != null)
{
if (User.SectionId != Guid.Empty)
{
Sections = await RestWrapper.SectionRestApi.GetByUniversityAsync(SelectedUni.Id, token);
Sections = await RestWrapper.HospitalRestApi.GetSectionsAsync(SelectedHospital.Id, token);
SelectedSection = Sections.FirstOrDefault(s => s.Id == User.SectionId);
}
}
}
await base.InitializeAsync();
@ -55,6 +60,7 @@ public class ProfilePageViewModel(
IsProcessing = true;
var token = await UserUtility.GetBearerTokenAsync();
var request = User.Adapt<UserActionRequestDto>();
request.ProfileType = ProfileType.Student;
if (SelectedUni != null)
{
request.UniversityId = SelectedUni.Id;
@ -66,6 +72,13 @@ public class ProfilePageViewModel(
User.SectionId = SelectedSection.Id;
User.SectionName = SelectedSection.Name;
}
if (SelectedHospital != null)
{
request.HospitalId = SelectedHospital.Id;
User.HospitalId = SelectedHospital.Id;
User.HospitalName = SelectedHospital.Name;
}
await RestWrapper.UserRestApi.UpdateUserAsync(request, token);
await UserUtility.SetUserAsync(User);
Snackbar.Add("ویرایش حساب کاربری با موفقیت انجام شد", Severity.Success);
@ -92,8 +105,6 @@ public class ProfilePageViewModel(
navigationManager.NavigateTo("");
}
public async Task<IEnumerable<CitySDto>> SearchCity(string city)
{
try
@ -123,10 +134,10 @@ public class ProfilePageViewModel(
{
try
{
if (SelectedUni != null)
if (SelectedHospital != null)
{
var token = await UserUtility.GetBearerTokenAsync();
Sections = await RestWrapper.SectionRestApi.GetByUniversityAsync(SelectedUni.Id, token);
Sections = await RestWrapper.HospitalRestApi.GetSectionsAsync(SelectedHospital.Id, token);
}
if (section.IsNullOrEmpty())
return Sections;
@ -169,4 +180,29 @@ public class ProfilePageViewModel(
return Universities;
}
}
public async Task<IEnumerable<HospitalSDto>> SearchHospital(string hospital)
{
try
{
if (SelectedUni != null)
{
var token = await UserUtility.GetBearerTokenAsync();
Hospitals = await RestWrapper.UniversityRestApi.GetHospitalsAsync(SelectedUni.Id, token);
}
if (hospital.IsNullOrEmpty())
return Hospitals;
return Hospitals.Where(c => c.Name.Contains(hospital));
}
catch (ApiException ex)
{
var exe = await ex.GetContentAsAsync<ApiResult>();
Snackbar.Add(exe != null ? exe.Message : ex.Content, Severity.Error);
return Hospitals;
}
catch (Exception e)
{
Snackbar.Add(e.Message, Severity.Error);
return Hospitals;
}
}
}

View File

@ -0,0 +1,7 @@
namespace DocuMed.PWA.Services.RestServices;
public interface IAiRestApi
{
[Post("/chat")]
public Task<string> ChatAsync([Body]object request);
}

View File

@ -0,0 +1,7 @@
namespace DocuMed.PWA.Services.RestServices;
public interface IHospitalRestApi
{
[Get("/{hospitalId}/section")]
Task<List<SectionSDto>> GetSectionsAsync(Guid hospitalId, [Header("Authorization")] string authorization);
}

View File

@ -12,4 +12,7 @@ public interface IRestWrapper
public IUserRestApi UserRestApi { get; }
public IMedicalHistoryRestApi MedicalHistoryRestApi { get; }
public IPatientRestApi PatientRestApi { get; }
public IHospitalRestApi HospitalRestApi { get; }
public IUniversityRestApi UniversityRestApi { get; }
public IAiRestApi AiRestApi { get; }
}

View File

@ -4,6 +4,4 @@ namespace DocuMed.PWA.Services.RestServices;
public interface ISectionRestApi : ICrudDtoApiRest<Section,SectionSDto,Guid>
{
[Get("/university/{universityId}")]
Task<List<SectionSDto>> GetByUniversityAsync(Guid universityId, [Header("Authorization")] string authorization);
}

View File

@ -0,0 +1,7 @@
namespace DocuMed.PWA.Services.RestServices;
public interface IUniversityRestApi
{
[Get("/{universityId}/hospital")]
Task<List<HospitalSDto>> GetHospitalsAsync(Guid universityId, [Header("Authorization")] string authorization);
}

View File

@ -26,4 +26,7 @@ public class RestWrapper : IRestWrapper
public IUserRestApi UserRestApi => RestService.For<IUserRestApi>(Address.UserController, setting);
public IMedicalHistoryRestApi MedicalHistoryRestApi => RestService.For<IMedicalHistoryRestApi>(Address.MedicalHistoryController);
public IPatientRestApi PatientRestApi => RestService.For<IPatientRestApi>(Address.PatientController,setting);
public IHospitalRestApi HospitalRestApi => RestService.For<IHospitalRestApi>(Address.HospitalController, setting);
public IUniversityRestApi UniversityRestApi => RestService.For<IUniversityRestApi>(Address.UniversityController, setting);
public IAiRestApi AiRestApi => RestService.For<IAiRestApi>(Address.AiController, setting);
}

View File

@ -20,6 +20,9 @@ module.exports = {
'6xl': '4rem',
'7xl': '5rem'
},
animation: {
'gradient': 'gradient 8s linear infinite',
},
fontFamily: {
"iranyekan": ["'iranyekan'"],
},

View File

@ -518,6 +518,15 @@ video {
.fixed {
position: fixed;
}
.absolute {
position: absolute;
}
.relative {
position: relative;
}
.inset-\[-1000\%\] {
inset: -1000%;
}
.bottom-0 {
bottom: 0px;
}
@ -720,6 +729,9 @@ video {
.mt-auto {
margin-top: auto;
}
.inline-block {
display: inline-block;
}
.flex {
display: flex;
}
@ -853,6 +865,18 @@ video {
.basis-full {
flex-basis: 100%;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.animate-\[spin_2s_linear_infinite\] {
animation: spin 2s linear infinite;
}
.cursor-pointer {
cursor: pointer;
}
.grid-cols-1 {
grid-template-columns: repeat(1, minmax(0, 1fr));
}
@ -989,6 +1013,35 @@ video {
.bg-opacity-20 {
--tw-bg-opacity: 0.2;
}
.bg-\[conic-gradient\(from_90deg_at_50\%_50\%\2c \#E2CBFF_0\%\2c \#393BB2_50\%\2c \#E2CBFF_100\%\)\] {
background-image: conic-gradient(from 90deg at 50% 50%,#E2CBFF 0%,#393BB2 50%,#E2CBFF 100%);
}
.bg-gradient-to-r {
background-image: linear-gradient(to right, var(--tw-gradient-stops));
}
.from-sky-600 {
--tw-gradient-from: #0284c7 var(--tw-gradient-from-position);
--tw-gradient-to: rgb(2 132 199 / 0) var(--tw-gradient-to-position);
--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
}
.via-blue-500 {
--tw-gradient-to: rgb(59 130 246 / 0) var(--tw-gradient-to-position);
--tw-gradient-stops: var(--tw-gradient-from), #3b82f6 var(--tw-gradient-via-position), var(--tw-gradient-to);
}
.via-violet-500 {
--tw-gradient-to: rgb(139 92 246 / 0) var(--tw-gradient-to-position);
--tw-gradient-stops: var(--tw-gradient-from), #8b5cf6 var(--tw-gradient-via-position), var(--tw-gradient-to);
}
.to-fuchsia-400 {
--tw-gradient-to: #e879f9 var(--tw-gradient-to-position);
}
.to-violet-400 {
--tw-gradient-to: #a78bfa var(--tw-gradient-to-position);
}
.bg-clip-text {
-webkit-background-clip: text;
background-clip: text;
}
.p-0 {
padding: 0px;
}
@ -1004,6 +1057,9 @@ video {
.p-5 {
padding: 1.25rem;
}
.p-\[1px\] {
padding: 1px;
}
.px-1 {
padding-left: 0.25rem;
padding-right: 0.25rem;
@ -1133,6 +1189,10 @@ video {
.text-\[--color-primary\] {
color: var(--color-primary);
}
.text-black {
--tw-text-opacity: 1;
color: rgb(0 0 0 / var(--tw-text-opacity));
}
.text-blue-500 {
--tw-text-opacity: 1;
color: rgb(59 130 246 / var(--tw-text-opacity));
@ -1157,6 +1217,9 @@ video {
--tw-text-opacity: 1;
color: rgb(31 41 55 / var(--tw-text-opacity));
}
.text-transparent {
color: transparent;
}
.text-white {
--tw-text-opacity: 1;
color: rgb(255 255 255 / var(--tw-text-opacity));
@ -1173,6 +1236,11 @@ video {
.filter {
filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
}
.backdrop-blur-3xl {
--tw-backdrop-blur: blur(64px);
-webkit-backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);
backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);
}
@font-face {
font-family: iranyekan;
@ -1359,11 +1427,35 @@ a, .btn-link {
border-color: rgb(20 184 166 / var(--tw-border-opacity));
}
.focus\:outline-none:focus {
outline: 2px solid transparent;
outline-offset: 2px;
}
.focus\:ring-2:focus {
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
}
.focus\:ring-slate-400:focus {
--tw-ring-opacity: 1;
--tw-ring-color: rgb(148 163 184 / var(--tw-ring-opacity));
}
.focus\:ring-teal-500:focus {
--tw-ring-opacity: 1;
--tw-ring-color: rgb(20 184 166 / var(--tw-ring-opacity));
}
.focus\:ring-offset-2:focus {
--tw-ring-offset-width: 2px;
}
.focus\:ring-offset-slate-50:focus {
--tw-ring-offset-color: #f8fafc;
}
.group:hover .group-hover\:text-\[--color-primary\] {
color: var(--color-primary);
}