diff --git a/src/content/series/SeriesRoot.tsx b/src/content/series/SeriesRoot.tsx index fbcc834b..2fde2340 100644 --- a/src/content/series/SeriesRoot.tsx +++ b/src/content/series/SeriesRoot.tsx @@ -37,8 +37,8 @@ const SeriesRoot: React.FC = ({ studyId }) => { (id) => deleteSeries(id), [], { - onSuccess: (_, variables) => { - toastSuccess('Series deleted successfully' + variables); + onSuccess: () => { + toastSuccess('Series deleted successfully'); refetchSeries(); }, onError: (error: any) => { diff --git a/src/content/studies/AiStudy.tsx b/src/content/studies/AiStudy.tsx new file mode 100644 index 00000000..39615021 --- /dev/null +++ b/src/content/studies/AiStudy.tsx @@ -0,0 +1,113 @@ +/** + * Component for a modal to preview a study + * @name PreviewStudy + */ + +import React, { useState } from "react"; +import { getSeriesOfStudy } from "../../services/orthanc"; +import { Colors, useCustomMutation, useCustomQuery, useCustomToast } from "../../utils"; + +import { Button, Modal, ProgressCircle, Spinner } from "../../ui"; +import { ProcessingJob, Series } from "../../utils/types"; +import { createProcessingJob, getProcessingJob } from "../../services/processing"; + +type AIStudyProps = { + studyId: string; + onClose: () => void; + show: boolean; +} + +const AiStudy: React.FC = ({ studyId, onClose, show }) => { + const { toastSuccess, toastError } = useCustomToast() + + const [selectedSeries, setSelectedSeries] = useState([]); + const [jobId, setJobId] = useState(null); + + const { data: series, isLoading } = useCustomQuery( + ['study', studyId, 'series'], + () => getSeriesOfStudy(studyId), + { + select: (instances: Series[]) => { + return instances.sort((a, b) => a.mainDicomTags?.seriesDescription?.localeCompare(b.mainDicomTags?.seriesDescription ?? "") ?? 0) + } + } + ) + + const { data: jobData } = useCustomQuery( + ['processing', jobId ?? ''], + () => getProcessingJob(jobId), + { + enabled: jobId != null, + refetchInterval: 2000 + } + ) + + const { mutate: createProcessingJobMutate } = useCustomMutation( + ({ jobType, jobPayload }) => createProcessingJob(jobType, jobPayload), + [[]], + { + onSuccess: (jobId) => { + toastSuccess("Job Created " + jobId) + setJobId(jobId) + } + } + ) + + const handleSeriesClick = (seriesId: string) => { + if (selectedSeries.includes(seriesId)) { + const newSelected = selectedSeries.filter((id) => id !== seriesId) + setSelectedSeries(newSelected) + + } else { + setSelectedSeries(previousSeries => [...previousSeries, seriesId]) + } + } + + const handleExecute = () => { + if (selectedSeries.length !== 2) { + toastError('Select only 2 series, PT and CT') + return; + } + createProcessingJobMutate({ + jobType: 'tmtv', + jobPayload: { + CtOrthancSeriesId: selectedSeries[0], + PtOrthancSeriesId: selectedSeries[1], + SendMaskToOrthancAs: ['seg'], + WithFragmentedMask: true, + } + }) + } + + console.log(jobData) + if (isLoading) return + + return ( + + + AI Inference + + +
+ { + series?.map((series: Series) => { + return ( + + ) + }) + } +
+
+ +
+ + {jobData && jobData.map(job => )} +
+
+
+ ); +}; + +export default AiStudy; diff --git a/src/content/studies/StudyActions.tsx b/src/content/studies/StudyActions.tsx index 825f49a9..4b5f8194 100644 --- a/src/content/studies/StudyActions.tsx +++ b/src/content/studies/StudyActions.tsx @@ -1,6 +1,7 @@ // StudyActions.tsx import React from 'react'; -import { FaEdit, FaEye, FaTrash } from "react-icons/fa"; +import { FaEdit as EditIcon, FaEye as EyeIcon, FaTrash as TrashIcon } from "react-icons/fa"; +import { GiBrain as BrainIcon } from 'react-icons/gi' import { StudyMainDicomTags } from "../../utils/types"; import DropdownButton from '../../ui/menu/DropDownButton'; import OhifViewerLink from '../OhifViewerLink'; @@ -15,34 +16,41 @@ const StudyActions: React.FC = ({ study, onActionClick }) => const options = [ { label: '', - icon: , + icon: , color: 'green', component: }, { label: '', - icon: , + icon: , color: 'green', component: }, { label: 'Modify', - icon: , + icon: , color: 'orange', action: () => onActionClick('edit', study.id) }, { - label: 'Delete', - icon: , - color: 'red', - action: () => onActionClick('delete', study.id) + label: 'AI', + icon: , + color: 'green', + action: () => onActionClick('ai', study.id) }, { label: 'Preview Study', - icon: , + icon: , color: 'green', action: () => onActionClick('preview', study.id) }, + { + label: 'Delete', + icon: , + color: 'red', + action: () => onActionClick('delete', study.id) + }, + ]; const handleClick = (e: React.MouseEvent) => { diff --git a/src/content/studies/StudyRoot.tsx b/src/content/studies/StudyRoot.tsx index 3a8d561e..f2562389 100644 --- a/src/content/studies/StudyRoot.tsx +++ b/src/content/studies/StudyRoot.tsx @@ -7,6 +7,7 @@ import PreviewStudy from './PreviewStudy'; import { useConfirm } from '../../services/ConfirmContextProvider'; import { useCustomToast } from '../../utils/toastify'; import Patient from '../../model/Patient'; +import AiStudy from './AiStudy'; type StudyRootProps = { patient: Patient; @@ -16,6 +17,7 @@ type StudyRootProps = { const StudyRoot: React.FC = ({ patient, onStudyUpdated, onStudySelected }) => { const [editingStudy, setEditingStudy] = useState(null); + const [aiStudyId, setAIStudyId] = useState(null); const [previewStudyId, setPreviewStudyId] = useState(null); const { confirm } = useConfirm(); @@ -60,6 +62,10 @@ const StudyRoot: React.FC = ({ patient, onStudyUpdated, onStudyS setPreviewStudyId(studyId); } + const handleAIStudy=(studyId : string) => { + setAIStudyId(studyId) + } + const handleStudyAction = (action: string, studyId: string) => { switch (action) { case 'edit': @@ -71,6 +77,9 @@ const StudyRoot: React.FC = ({ patient, onStudyUpdated, onStudyS case 'preview': handlePreviewStudy(studyId); break; + case 'ai': + handleAIStudy(studyId); + break; default: break; } @@ -103,6 +112,13 @@ const StudyRoot: React.FC = ({ patient, onStudyUpdated, onStudyS show={!!previewStudyId} /> )} + {aiStudyId && ( + setAIStudyId(null)} + show={!!aiStudyId} + /> + )} ); }; diff --git a/src/services/axios.ts b/src/services/axios.ts index d592c5cb..47a9b8d0 100644 --- a/src/services/axios.ts +++ b/src/services/axios.ts @@ -33,4 +33,11 @@ export const getToken = () => { return store?.getState()?.user.token } +export const handleAxiosError = (error: any) => { + if (error.response) { + throw error.response; + } + throw error; +} + export default axios \ No newline at end of file diff --git a/src/services/processing.ts b/src/services/processing.ts new file mode 100644 index 00000000..73484ca1 --- /dev/null +++ b/src/services/processing.ts @@ -0,0 +1,39 @@ +import { ProcessingJob } from "../utils/types"; +import axios, { handleAxiosError } from "./axios"; + +export const createProcessingJob = ( + jobType: string, + jobPayload: Record +): Promise => { + const payload = { + JobType: jobType, + TmtvJob: jobPayload, + }; + + return axios + .post(`/api/processing`, payload) + .then((response) => response.data.JobId) + .catch(handleAxiosError); +}; + +export const getProcessingJob = (jobId: string): Promise => { + return axios + .get(`/api/processing/` + jobId) + .then((response) => { + const data = response.data; + return data.map((jobdata) => ({ + progress: jobdata.Progress, + state: jobdata.State, + id: jobdata.Id, + results: jobdata.Results, + })); + }) + .catch(handleAxiosError); +}; + +export const deleteProcessingJob = (jobId: string): Promise => { + return axios + .delete(`/api/processing/` + jobId) + .then((response) => undefined) + .catch(handleAxiosError); +}; diff --git a/src/ui/Accordion.tsx b/src/ui/Accordion.tsx index eacd690c..a7af4a4e 100644 --- a/src/ui/Accordion.tsx +++ b/src/ui/Accordion.tsx @@ -74,7 +74,7 @@ const AccordionHeader = ({ children, className = "", variant = "default" }: Acco return ( -
+
{children}
) diff --git a/src/utils/types.ts b/src/utils/types.ts index 8a426524..13738593 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -92,15 +92,15 @@ export type OrthancJob = { type: string; progress: number; state: StateJob | string; - completionTime :string; - content : Record - creationTime : string; - effectiveRuntime :number; - errorCode :number - errorDescription :string; - errorDetails : string; - priority : number; - timestamp :string; + completionTime: string; + content: Record; + creationTime: string; + effectiveRuntime: number; + errorCode: number; + errorDescription: string; + errorDetails: string; + priority: number; + timestamp: string; }; export type Role = { @@ -135,6 +135,13 @@ export type SignInResponse = { userId: number; }; +export type ProcessingJob = { + progress: number; + state: string; + id: string; + results : Record +}; + export type Peer = { name: string; password: string; @@ -151,7 +158,7 @@ export type Modality = { }; export type ModalityExtended = { - name : string; + name: string; aet: string; allowEcho: boolean; allowEventReport: boolean; @@ -209,7 +216,7 @@ export type QueryPayload = { export type FindPayload = QueryPayload & { Labels: string[]; - LabelsConstraint : string + LabelsConstraint: string; }; export type ExtendedQueryPayload = { @@ -281,24 +288,24 @@ export type StudyMainDicomTags = { }; export type Instances = { - fileSize : number - fileUuid : string - id : string - indexInSeries : number - labels : string[] - mainDicomTags : { - acquisitionNumber : string|null - imageComments : string|null - imageOrientationPatient: string|null - imagePositionPatient : string|null - instanceCreationDate :string|null - instanceCreationTime :string|null - instanceNumber : string|null - sopInstanceUID : string|null - } - parentSeries :string - type :string -} + fileSize: number; + fileUuid: string; + id: string; + indexInSeries: number; + labels: string[]; + mainDicomTags: { + acquisitionNumber: string | null; + imageComments: string | null; + imageOrientationPatient: string | null; + imagePositionPatient: string | null; + instanceCreationDate: string | null; + instanceCreationTime: string | null; + instanceNumber: string | null; + sopInstanceUID: string | null; + }; + parentSeries: string; + type: string; +}; export type Series = { expectedNumberOfInstances: number | null;