Skip to content

Commit

Permalink
Merge pull request #309 from Pixilib/dev
Browse files Browse the repository at this point in the history
Dev
  • Loading branch information
salimkanoun authored Sep 7, 2024
2 parents 9382eb2 + 9e81b52 commit 0fa777f
Show file tree
Hide file tree
Showing 12 changed files with 363 additions and 112 deletions.
1 change: 0 additions & 1 deletion src/content/series/EditSeries.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@ const EditSeries: React.FC<EditSeriesProps> = ({ series, onEditSeries, onClose,
<SeriesEditForm
data={series}
onSubmit={handleSubmit}
onCancel={onClose}
jobId={jobId ?? undefined}
onJobCompleted={handleJobCompletion}
/>
Expand Down
77 changes: 35 additions & 42 deletions src/content/series/PreviewSeries.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<PreviewSeriesProps> = ({ seriesId, onClose, show }) => {
const PreviewSeries: React.FC<PreviewSeriesProps> = ({ seriesId}) => {

const [rows, setRows] = useState(1)
const [columns, setColumns] = useState(3)
Expand Down Expand Up @@ -63,45 +61,40 @@ const PreviewSeries: React.FC<PreviewSeriesProps> = ({ seriesId, onClose, show }
};

return (
<Modal show={show} size='xl'>
<Modal.Header onClose={onClose} >
<Modal.Title>Preview Series</Modal.Title>
</Modal.Header>
<Modal.Body>
<div className={"flex w-full h-full"} style={{ display: 'grid', gridTemplateColumns: `repeat(${columns}, 1fr)`, gridTemplateRows: `repeat(${rows}, 1fr)` }}>
{
selectedInstanceUIDs?.map((instance: Instances) => {
return <PreviewInstance key={instance.id} instanceUID={instance.id} />
})
}
<>
<div className={"flex w-full h-full"} style={{ display: 'grid', gridTemplateColumns: `repeat(${columns}, 1fr)`, gridTemplateRows: `repeat(${rows}, 1fr)` }}>
{
selectedInstanceUIDs?.map((instance: Instances) => {
return <PreviewInstance key={instance.id} instanceUID={instance.id} />
})
}
</div>
<input className="w-full" type="range" value={imageIndex} min={0} max={(instanceUIDs?.length ?? 1) - 1} onChange={(event: ChangeEvent<HTMLInputElement>) => setImageIndex(Number(event.target.value))} />
<div className={"flex w-full justify-center"}>
<div className="flex gap-3">
<Input
label="Columns :"
type="number"
id="number"
name="columns"
min={0}
max={10}
value={columns}
onChange={handleColumnChange}
/>
<Input
label="Rows :"
type="number"
id="number"
name="rows"
min={1}
max={10}
value={rows}
onChange={handleRowChange}
/>
</div>
<input className="w-full" type="range" value={imageIndex} min={0} max={(instanceUIDs?.length ?? 1) - 1} onChange={(event: ChangeEvent<HTMLInputElement>) => setImageIndex(Number(event.target.value))} />
<div className={"flex w-full justify-center"}>
<div className="flex gap-3">
<Input
label="Columns :"
type="number"
id="number"
name="columns"
min={0}
max={10}
value={columns}
onChange={handleColumnChange}
/>
<Input
label="Rows :"
type="number"
id="number"
name="rows"
min={1}
max={10}
value={rows}
onChange={handleRowChange}
/>
</div>
</div>
</Modal.Body>
</Modal>
</div>
</>
);
};

Expand Down
6 changes: 5 additions & 1 deletion src/content/series/SeriesEditForm.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -120,6 +121,9 @@ const SeriesEditForm = ({ data, onSubmit, jobId, onJobCompleted }: SeriesEditFor
bordered={false}
/>
</div>
<div>
<Button type="submit" color={Colors.secondary}>Modify</Button>
</div>
{
jobId && (
<div className="flex flex-col items-center justify-center">
Expand Down
35 changes: 19 additions & 16 deletions src/content/series/SeriesRoot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -23,11 +23,11 @@ const SeriesRoot: React.FC<SeriesRootProps> = ({ studyId }) => {

const { confirm } = useConfirm();
const { toastSuccess, toastError } = useCustomToast();

const { data: seriesList, isLoading, refetch: refetchSeries } = useCustomQuery<Series[]>(
['series', studyId],
() => getSeriesOfStudy(studyId),
{
{
onError: (error) => {
console.error(`No series for this study or an error occured: ${error}`);
}
Expand All @@ -37,8 +37,8 @@ const SeriesRoot: React.FC<SeriesRootProps> = ({ studyId }) => {
(id) => deleteSeries(id),
[],
{
onSuccess: (_, variables) => {
toastSuccess('Series deleted successfully' + variables);
onSuccess: () => {
toastSuccess('Series deleted successfully');
refetchSeries();
},
onError: (error: any) => {
Expand All @@ -58,13 +58,13 @@ const SeriesRoot: React.FC<SeriesRootProps> = ({ studyId }) => {
const handleDeleteSeries = async (seriesId: string) => {
const confirmContent = (
<div className="italic">
Are you sure you want to delete this Series:
<span className="text-xl not-italic font-bold text-primary">{seriesId} ?</span>
Are you sure you want to delete this Series:
<span className="text-xl not-italic font-bold text-primary">{seriesId} ?</span>
</div>
);
if (await confirm({content: confirmContent})) {
mutateDeleteSeries(seriesId);
}
);
if (await confirm({ content: confirmContent })) {
mutateDeleteSeries(seriesId);
}
};


Expand Down Expand Up @@ -110,11 +110,14 @@ const SeriesRoot: React.FC<SeriesRootProps> = ({ studyId }) => {
/>
)}
{previewSeries && (
<PreviewSeries
seriesId={previewSeries.id}
onClose={() => setPreviewSeries(null)}
show={!!previewSeries}
/>
<Modal show={!!previewSeries} size='xl'>
<Modal.Header onClose={() => setPreviewSeries(null)} >
<Modal.Title>Preview Series</Modal.Title>
</Modal.Header>
<Modal.Body>
<PreviewSeries seriesId={previewSeries.id} />
</Modal.Body>
</Modal>
)}
</div>
);
Expand Down
113 changes: 113 additions & 0 deletions src/content/studies/AiStudy.tsx
Original file line number Diff line number Diff line change
@@ -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<AIStudyProps> = ({ studyId, onClose, show }) => {
const { toastSuccess, toastError } = useCustomToast()

const [selectedSeries, setSelectedSeries] = useState([]);
const [jobId, setJobId] = useState<string | null>(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<ProcessingJob[]>(
['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 <Spinner />

return (
<Modal show={show} size='xl'>
<Modal.Header onClose={onClose} >
<Modal.Title>AI Inference</Modal.Title>
</Modal.Header>
<Modal.Body>
<div className={"flex justify-center gap-3"}>
{
series?.map((series: Series) => {
return (
<Button key={series.id} color={selectedSeries.includes(series.id) ? Colors.success : Colors.secondary} onClick={() => handleSeriesClick(series.id)} >
{series.mainDicomTags.modality} - {series.mainDicomTags.seriesDescription}
</Button>
)
})
}
</div>
</Modal.Body>
<Modal.Footer>
<div className={"flex justify-center"}>
<Button color={Colors.success} onClick={handleExecute}>Execute TMTV Model</Button>
{jobData && jobData.map(job => <ProgressCircle progress={job.progress} text={job.state} />)}
</div>
</Modal.Footer>
</Modal>
);
};

export default AiStudy;
57 changes: 57 additions & 0 deletions src/content/studies/PreviewStudy.tsx
Original file line number Diff line number Diff line change
@@ -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<PreviewStudyProps> = ({ 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 <Spinner />

return (
<Modal show={show} size='xl'>
<Modal.Header onClose={onClose} >
<Modal.Title>Preview Study</Modal.Title>
</Modal.Header>
<Modal.Body>
<div className={"flex flex-col w-full h-full gap-3"}>
{
series?.map((series: Series) => {
return (
<Accordion variant="secondary" header={<AccordionHeader variant="primary">{(series.mainDicomTags?.seriesDescription?.length ?? 0) > 0 ? series.mainDicomTags?.seriesDescription : "N/A"}</AccordionHeader>}>
<PreviewSeries seriesId={series.id} />
</Accordion>
)
})
}
</div>
</Modal.Body>
</Modal>
);
};

export default PreviewStudy;
Loading

0 comments on commit 0fa777f

Please sign in to comment.