diff --git a/src/content/series/EditSeries.tsx b/src/content/series/EditSeries.tsx index e3f00c1a..a42ef190 100644 --- a/src/content/series/EditSeries.tsx +++ b/src/content/series/EditSeries.tsx @@ -57,7 +57,6 @@ const EditSeries: React.FC = ({ series, onEditSeries, onClose, diff --git a/src/content/series/PreviewSeries.tsx b/src/content/series/PreviewSeries.tsx index 115dada2..af4a84d4 100644 --- a/src/content/series/PreviewSeries.tsx +++ b/src/content/series/PreviewSeries.tsx @@ -7,17 +7,15 @@ import React, { ChangeEvent, useMemo, useState } from "react"; import { getInstancesOfSeries } from "../../services/orthanc"; import { useCustomQuery } from "../../utils"; -import { Input, Modal, Spinner } from "../../ui"; +import { Input, Spinner } from "../../ui"; import PreviewInstance from "./PreviewInstance"; import { Instances } from "../../utils/types"; type PreviewSeriesProps = { seriesId: string; - onClose: () => void; - show: boolean; } -const PreviewSeries: React.FC = ({ seriesId, onClose, show }) => { +const PreviewSeries: React.FC = ({ seriesId}) => { const [rows, setRows] = useState(1) const [columns, setColumns] = useState(3) @@ -63,45 +61,40 @@ const PreviewSeries: React.FC = ({ seriesId, onClose, show } }; return ( - - - Preview Series - - -
- { - selectedInstanceUIDs?.map((instance: Instances) => { - return - }) - } + <> +
+ { + selectedInstanceUIDs?.map((instance: Instances) => { + return + }) + } +
+ ) => setImageIndex(Number(event.target.value))} /> +
+
+ +
- ) => setImageIndex(Number(event.target.value))} /> -
-
- - -
-
- - +
+ ); }; diff --git a/src/content/series/SeriesEditForm.tsx b/src/content/series/SeriesEditForm.tsx index cff6777f..b5e45f85 100644 --- a/src/content/series/SeriesEditForm.tsx +++ b/src/content/series/SeriesEditForm.tsx @@ -1,8 +1,9 @@ import React, { ChangeEvent, useState } from "react"; import { Series, SeriesPayload, SeriesMainDicomTags } from '../../utils/types'; -import { InputWithDelete, CheckBox } from "../../ui"; +import { InputWithDelete, CheckBox, Button } from "../../ui"; import ProgressJobs from "../../query/ProgressJobs"; +import { Colors } from "../../utils"; type SeriesEditFormProps = { data: Series; @@ -120,6 +121,9 @@ const SeriesEditForm = ({ data, onSubmit, jobId, onJobCompleted }: SeriesEditFor bordered={false} />
+
+ +
{ jobId && (
diff --git a/src/content/series/SeriesRoot.tsx b/src/content/series/SeriesRoot.tsx index 72faaf19..2fde2340 100644 --- a/src/content/series/SeriesRoot.tsx +++ b/src/content/series/SeriesRoot.tsx @@ -11,7 +11,7 @@ import EditSeries from './EditSeries'; import PreviewSeries from './PreviewSeries'; import { useConfirm } from '../../services/ConfirmContextProvider'; import { useCustomToast } from '../../utils/toastify'; -import { Spinner } from '../../ui'; +import { Modal, Spinner } from '../../ui'; interface SeriesRootProps { studyId: string; @@ -23,11 +23,11 @@ const SeriesRoot: React.FC = ({ studyId }) => { const { confirm } = useConfirm(); const { toastSuccess, toastError } = useCustomToast(); - + const { data: seriesList, isLoading, refetch: refetchSeries } = useCustomQuery( ['series', studyId], () => getSeriesOfStudy(studyId), - { + { onError: (error) => { console.error(`No series for this study or an error occured: ${error}`); } @@ -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) => { @@ -58,13 +58,13 @@ const SeriesRoot: React.FC = ({ studyId }) => { const handleDeleteSeries = async (seriesId: string) => { const confirmContent = (
- Are you sure you want to delete this Series: - {seriesId} ? + Are you sure you want to delete this Series: + {seriesId} ?
- ); - if (await confirm({content: confirmContent})) { - mutateDeleteSeries(seriesId); -} + ); + if (await confirm({ content: confirmContent })) { + mutateDeleteSeries(seriesId); + } }; @@ -110,11 +110,14 @@ const SeriesRoot: React.FC = ({ studyId }) => { /> )} {previewSeries && ( - setPreviewSeries(null)} - show={!!previewSeries} - /> + + setPreviewSeries(null)} > + Preview Series + + + + + )}
); 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/PreviewStudy.tsx b/src/content/studies/PreviewStudy.tsx index e69de29b..eecd6ca3 100644 --- a/src/content/studies/PreviewStudy.tsx +++ b/src/content/studies/PreviewStudy.tsx @@ -0,0 +1,57 @@ +/** + * Component for a modal to preview a study + * @name PreviewStudy + */ + +import React from "react"; +import { getSeriesOfStudy } from "../../services/orthanc"; +import { useCustomQuery } from "../../utils"; + +import { Accordion, Modal, Spinner } from "../../ui"; +import { Series } from "../../utils/types"; +import { AccordionHeader } from "../../ui/Accordion"; +import PreviewSeries from "../series/PreviewSeries"; + +type PreviewStudyProps = { + studyId: string; + onClose: () => void; + show: boolean; +} + +const PreviewStudy: React.FC = ({ studyId, onClose, show }) => { + + 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) + } + } + ) + + if (isLoading) return + + return ( + + + Preview Study + + +
+ { + series?.map((series: Series) => { + return ( + {(series.mainDicomTags?.seriesDescription?.length ?? 0) > 0 ? series.mainDicomTags?.seriesDescription : "N/A"}}> + + + ) + }) + } +
+
+
+ ); +}; + +export default PreviewStudy; 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 4fd799b6..f2562389 100644 --- a/src/content/studies/StudyRoot.tsx +++ b/src/content/studies/StudyRoot.tsx @@ -3,10 +3,11 @@ import { useCustomMutation } from '../../utils/reactQuery'; import { deleteStudy } from '../../services/orthanc'; import StudyTable from './StudyTable'; import EditStudy from './EditStudy'; -// import PreviewStudy from './PreviewStudy'; +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,8 @@ 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(); const { toastSuccess, toastError } = useCustomToast(); @@ -55,6 +58,14 @@ const StudyRoot: React.FC = ({ patient, onStudyUpdated, onStudyS } }; + const handlePreviewStudy = (studyId: string) => { + setPreviewStudyId(studyId); + } + + const handleAIStudy=(studyId : string) => { + setAIStudyId(studyId) + } + const handleStudyAction = (action: string, studyId: string) => { switch (action) { case 'edit': @@ -64,7 +75,10 @@ const StudyRoot: React.FC = ({ patient, onStudyUpdated, onStudyS handleDeleteStudy(studyId); break; case 'preview': - // handlePreviewStudy(studyId); + handlePreviewStudy(studyId); + break; + case 'ai': + handleAIStudy(studyId); break; default: break; @@ -91,13 +105,20 @@ const StudyRoot: React.FC = ({ patient, onStudyUpdated, onStudyS show={!!editingStudy} /> )} - {/* {previewStudy && ( + {previewStudyId && ( setPreviewStudy(null)} - show={!!previewStudy} + studyId={previewStudyId} + onClose={() => setPreviewStudyId(null)} + 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 2d94afa3..a7af4a4e 100644 --- a/src/ui/Accordion.tsx +++ b/src/ui/Accordion.tsx @@ -43,7 +43,7 @@ const Accordion: React.FC = ({ return (
{header} @@ -56,25 +56,25 @@ const Accordion: React.FC = ({ type AccordionHeaderProps = { children?: React.ReactNode; variant?: string - className? : string + className?: string } -const AccordionHeader = ({ children, className="", variant="default" }: AccordionHeaderProps) => { +const AccordionHeader = ({ children, className = "", variant = "default" }: AccordionHeaderProps) => { const getVariantClasses = () => { switch (variant) { case "secondary": - return "cursor-pointer flex justify-between items-center p-4 bg-secondary text-white" + return "bg-secondary text-white" case "primary": - return "cursor-pointer flex justify-between items-center p-4 bg-primary-active text-white" + return "bg-primary-active text-white" case "default": default: - return "cursor-pointer flex justify-between items-center p-4 bg-gray-100" + return "bg-gray-100" }; } 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;