|
|
import { useState, useCallback, useRef, useEffect } from 'react' |
|
|
import { apiClient } from '../api/client' |
|
|
import type { SegmentationResult, JobStatus } from '../types' |
|
|
|
|
|
|
|
|
const POLLING_INTERVAL = 2000 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export function useSegmentation() { |
|
|
|
|
|
const [result, setResult] = useState<SegmentationResult | null>(null) |
|
|
const [error, setError] = useState<string | null>(null) |
|
|
|
|
|
|
|
|
const [jobId, setJobId] = useState<string | null>(null) |
|
|
const [jobStatus, setJobStatus] = useState<JobStatus | null>(null) |
|
|
const [progress, setProgress] = useState(0) |
|
|
const [progressMessage, setProgressMessage] = useState('') |
|
|
const [elapsedSeconds, setElapsedSeconds] = useState<number | undefined>( |
|
|
undefined |
|
|
) |
|
|
|
|
|
|
|
|
const [isLoading, setIsLoading] = useState(false) |
|
|
|
|
|
|
|
|
const currentJobRef = useRef<string | null>(null) |
|
|
const pollingIntervalRef = useRef<number | null>(null) |
|
|
const abortControllerRef = useRef<AbortController | null>(null) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const stopPolling = useCallback(() => { |
|
|
if (pollingIntervalRef.current) { |
|
|
clearInterval(pollingIntervalRef.current) |
|
|
pollingIntervalRef.current = null |
|
|
} |
|
|
}, []) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const pollJobStatus = useCallback( |
|
|
async (id: string, signal: AbortSignal) => { |
|
|
|
|
|
if (id !== currentJobRef.current) { |
|
|
stopPolling() |
|
|
return |
|
|
} |
|
|
|
|
|
try { |
|
|
const response = await apiClient.getJobStatus(id, signal) |
|
|
|
|
|
|
|
|
if (id !== currentJobRef.current) return |
|
|
|
|
|
|
|
|
setJobStatus(response.status) |
|
|
setProgress(response.progress) |
|
|
setProgressMessage(response.progressMessage) |
|
|
setElapsedSeconds(response.elapsedSeconds) |
|
|
|
|
|
|
|
|
if (response.status === 'completed' && response.result) { |
|
|
stopPolling() |
|
|
setIsLoading(false) |
|
|
setResult({ |
|
|
dwiUrl: response.result.dwiUrl, |
|
|
predictionUrl: response.result.predictionUrl, |
|
|
metrics: { |
|
|
caseId: response.result.caseId, |
|
|
diceScore: response.result.diceScore, |
|
|
volumeMl: response.result.volumeMl, |
|
|
elapsedSeconds: response.result.elapsedSeconds, |
|
|
}, |
|
|
}) |
|
|
} |
|
|
|
|
|
|
|
|
if (response.status === 'failed') { |
|
|
stopPolling() |
|
|
setIsLoading(false) |
|
|
setError(response.error || 'Job failed') |
|
|
setResult(null) |
|
|
} |
|
|
} catch (err) { |
|
|
|
|
|
if (err instanceof Error && err.name === 'AbortError') return |
|
|
|
|
|
|
|
|
console.warn('Polling error (will retry):', err) |
|
|
} |
|
|
}, |
|
|
[stopPolling] |
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const runSegmentation = useCallback( |
|
|
async (caseId: string, fastMode = true) => { |
|
|
|
|
|
stopPolling() |
|
|
abortControllerRef.current?.abort() |
|
|
|
|
|
const abortController = new AbortController() |
|
|
abortControllerRef.current = abortController |
|
|
|
|
|
|
|
|
setError(null) |
|
|
setResult(null) |
|
|
setProgress(0) |
|
|
setProgressMessage('Creating job...') |
|
|
setJobStatus('pending') |
|
|
setElapsedSeconds(undefined) |
|
|
setIsLoading(true) |
|
|
|
|
|
try { |
|
|
|
|
|
const response = await apiClient.createSegmentJob( |
|
|
caseId, |
|
|
fastMode, |
|
|
abortController.signal |
|
|
) |
|
|
|
|
|
|
|
|
const newJobId = response.jobId |
|
|
setJobId(newJobId) |
|
|
currentJobRef.current = newJobId |
|
|
setJobStatus(response.status) |
|
|
setProgressMessage(response.message) |
|
|
|
|
|
|
|
|
pollingIntervalRef.current = window.setInterval(() => { |
|
|
pollJobStatus(newJobId, abortController.signal) |
|
|
}, POLLING_INTERVAL) |
|
|
|
|
|
|
|
|
await pollJobStatus(newJobId, abortController.signal) |
|
|
} catch (err) { |
|
|
|
|
|
if (err instanceof Error && err.name === 'AbortError') return |
|
|
|
|
|
const message = err instanceof Error ? err.message : 'Failed to start job' |
|
|
setError(message) |
|
|
setIsLoading(false) |
|
|
setJobStatus('failed') |
|
|
} |
|
|
}, |
|
|
[pollJobStatus, stopPolling] |
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const cancelJob = useCallback(() => { |
|
|
stopPolling() |
|
|
abortControllerRef.current?.abort() |
|
|
currentJobRef.current = null |
|
|
setIsLoading(false) |
|
|
setJobStatus(null) |
|
|
setProgress(0) |
|
|
setProgressMessage('') |
|
|
}, [stopPolling]) |
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
return () => { |
|
|
stopPolling() |
|
|
abortControllerRef.current?.abort() |
|
|
} |
|
|
}, [stopPolling]) |
|
|
|
|
|
return { |
|
|
|
|
|
result, |
|
|
error, |
|
|
|
|
|
|
|
|
jobId, |
|
|
jobStatus, |
|
|
progress, |
|
|
progressMessage, |
|
|
elapsedSeconds, |
|
|
|
|
|
|
|
|
isLoading, |
|
|
|
|
|
|
|
|
runSegmentation, |
|
|
cancelJob, |
|
|
} |
|
|
} |
|
|
|