Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
df72410
Add celery_task_id to Job model and store the id when job is created
saimouu Feb 23, 2026
196f4c3
Add base route and service logic for task cancelling
saimouu Feb 23, 2026
a8228c6
Add task cancel button to front and fix project delete bug
saimouu Feb 24, 2026
9818980
Change /job/uuid/cancel to accept delete_data option and update unfin…
saimouu Feb 24, 2026
40879d6
Merge branch 'main' into feature/cancelable-llm-calls
saimouu Feb 27, 2026
ea643e1
Add confirmation modal on cancel and add option for task data deletion
saimouu Feb 27, 2026
6a632ab
WIP: handle cancelled state
saimouu Mar 4, 2026
7583d9d
Fix task canceled label showing on top of progress bar and celery tas…
saimouu Mar 4, 2026
f9587f4
Add check when canceling if job has already been cancelled
saimouu Mar 4, 2026
475a8a7
Change frontend job status branching to use JobStatus
saimouu Mar 6, 2026
7e05689
Fix type check
saimouu Mar 6, 2026
9a575bd
Add eslint ignore for error any type
saimouu Mar 6, 2026
aebc472
Change task canceling to use useCallback
saimouu Mar 11, 2026
bb7dab8
Add separate button for task deletion
saimouu Mar 11, 2026
98c53df
Make confirmation modal reusable and confirmation dialog for task del…
saimouu Mar 12, 2026
00315a0
Merge branch 'main' into feature/cancelable-llm-calls
saimouu Mar 12, 2026
85e4e81
Quick fix typecheck. ModelSuggestion types should be fixed properly l…
saimouu Mar 12, 2026
a15e281
Add comment regarding the temporary ts-expect-error
saimouu Mar 12, 2026
e6f4cd3
Merge branch 'main' into feature/cancelable-llm-calls
saimouu Mar 19, 2026
9ddaf5c
Remove unused ts-expect-error after types fix
saimouu Mar 19, 2026
506b447
Fix typo and clean unused commented out code
saimouu Mar 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions client/src/components/ConfirmationModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import {
Dialog,
DialogPanel,
DialogTitle,
Description,
} from "@headlessui/react";
import { X } from "lucide-react";
import { Button } from "./Button";

type ConfirmationModalProps = {
open: boolean;
onClose: () => void;
onConfirm: () => void;
title: string;
description: string;
confirmButtonLabel: string;
confirmButtonVariant: "green" | "yellow" | "red" | "purple" | "gray" | "slate";
confirmButtonIcon: React.ReactNode;
};

export const ConfirmationModal: React.FC<ConfirmationModalProps> = ({
open,
onClose,
onConfirm,
title,
description,
confirmButtonLabel,
confirmButtonVariant,
confirmButtonIcon,
}) => {

return (
<Dialog
open={open}
onClose={onClose}
className="fixed inset-0 z-50 flex items-center justify-center p-4"
>
<div className="fixed inset-0 bg-black/30" aria-hidden="true" />

<DialogPanel className="relative bg-white shadow-2xl rounded-xl w-full max-w-md p-8">
<X
onClick={onClose}
className="absolute top-4 right-4 h-5 w-5 cursor-pointer text-gray-500 hover:text-gray-700 transition duration-200"
/>

<DialogTitle className="text-lg font-bold mb-3">
{title}
</DialogTitle>

<Description className="text-sm text-gray-600 mb-6 leading-relaxed">
{description}
</Description>

<div className="flex gap-3 justify-end">
<Button
variant="gray"
onClick={onClose}
>
Go back
</Button>
<Button
variant={confirmButtonVariant}
onClick={onConfirm}
>
<div className="flex items-center justify-center gap-2 font-semibold">
{confirmButtonIcon}
<span>{confirmButtonLabel}</span>
</div>
</Button>
</div>
</DialogPanel>
</Dialog >
)
}
2 changes: 2 additions & 0 deletions client/src/components/DropDownMenus.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Ellipsis } from "lucide-react";

type EllipsisItem = {
label: React.ElementType;
disabled?: boolean;
onClick: () => void;
};

Expand Down Expand Up @@ -47,6 +48,7 @@ export const DropdownMenuEllipsis: React.FC<EllipsisProps> = ({ items }) => {
as="button"
onClick={item.onClick}
className="block w-full px-4 py-2 text-left text-sm text-gray-700 data-focus:bg-gray-100 focus:outline-none cursor-pointer data-disabled:opacity-50"
disabled={item.disabled}
>
<Label />
</MenuItem>
Expand Down
197 changes: 157 additions & 40 deletions client/src/pages/ProjectPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,17 @@ import { useEffect, useState, useCallback, useMemo } from "react";
import { toast } from "react-toastify";
import { Layout } from "../components/Layout";
import { H6 } from "../components/Typography";
import { DropdownMenuText, DropdownOption } from "../components/DropDownMenus";
import { DropdownMenuText, DropdownOption, DropdownMenuEllipsis } from "../components/DropDownMenus";
import { FileDropArea } from "../components/FileDropArea";
import { ExpandableToast } from "../components/ExpandableToast";
import { TruncatedFileNames } from "../components/TruncatedFileNames";
import { ConfirmationModal } from "../components/ConfirmationModal";
import { createJob } from "../services/jobService";
import {
fileUploadToBackend,
fileFetchFromBackend,
} from "../services/fileService";
import { JobStatus } from "../state/types";
import { ManualEvaluationModal } from "../components/ManualEvaluationModal";
import { Button } from "../components/Button";
import {
Expand All @@ -28,13 +30,16 @@ import {
ChartCandlestick,
CircleAlert,
CircleCheck,
CircleStop,
Download,
FileText,
Loader,
Sparkles,
Square,
SquareCheckBig,
Trash2,
TriangleAlert,
XCircle
} from "lucide-react";
import { Card } from "../components/Card";
import { TabButton } from "../components/TabButton";
Expand Down Expand Up @@ -417,6 +422,12 @@ export const ProjectPage = () => {
const getPapers = useTypedStoreState((state) => state.getPapersForProject);
const papers = getPapers(projectUuid);

const [jobToCancel, setJobToCancel] = useState<string | null>(null);
const [jobToDelete, setJobToDelete] = useState<string | null>(null);

const cancelJob = useTypedStoreActions((actions) => actions.cancelJob);
const deleteJob = useTypedStoreActions((actions) => actions.deleteJob);

const [fetchedFiles, setFetchedFiles] = useState<FetchedFile[]>([]);
const [availableModels, setAvailableModels] = useState<
Array<{ id: string; created: number; object: "model"; owned_by: string }>
Expand Down Expand Up @@ -522,6 +533,46 @@ export const ProjectPage = () => {

const evaluationFinished = jobs.length === 0 && pendingTasks.length === 0;

const handleTaskCancel = useCallback(() => {
if (!jobToCancel) {
return;
}
cancelJob({
jobUuid: jobToCancel,
projectUuid: projectUuid,
})
.then(() => {
toast.success("Task cancelled successfully", { autoClose: 1500 });
setJobToCancel(null);
})
.catch((error: unknown) => {
toast.error(`Error canceling task: ${error instanceof Error ? error.message : String(error)}`);
})
}, [jobToCancel, projectUuid, cancelJob])

const handleCancelModalClose = useCallback(() => {
setJobToCancel(null);
}, [])

const handleTaskDelete = useCallback(() => {
if (!jobToDelete) {
return;
}
deleteJob({ jobUuid: jobToDelete, projectUuid: projectUuid })
.then(() => {
toast.success("Task deleted successfully", { autoClose: 1500 });
setJobToDelete(null);
})
.catch((error: unknown) => {
toast.error(`Error deleting task: ${error instanceof Error ? error.message : String(error)}`);
})
}, [jobToDelete, projectUuid, deleteJob])

const handleDeleteModalClose = useCallback(() => {
setJobToDelete(null);
}, [])


const fetchModels = useCallback(() => {
async function fetch_models() {
if (selectedLlmProvider && selectedLlmProvider.value) {
Expand Down Expand Up @@ -805,10 +856,11 @@ export const ProjectPage = () => {
totalCount === 0
? 0
: Math.round((completedCount / totalCount) * 100);
const status = job.stats.status;

return (
<Card key={job.uuid} className="flex-row justify-between">
<div className="grid grid-cols-[50px_1fr_auto] gap-4 w-full">
<div className="grid grid-cols-[50px_1fr_auto_auto] gap-4 w-full">
<>
{job.prompting_config.screening_type ==
JobPromptingType.ZERO_SHOT && <Badge text="ZS" invert />}
Expand All @@ -826,54 +878,95 @@ export const ProjectPage = () => {
</div>
<div className="flex justify-end items-end w-full">
<div className="relative w-56 h-8">
{progress !== 100 && (
<progress
value={progress}
max={100}
className={classNames(
"h-full w-full [&::-webkit-progress-bar]:rounded-xl [&::-webkit-progress-bar]:bg-gray-400 [&::-webkit-progress-value]:bg-blue-200 [&::-webkit-progress-value]:rounded-xl",
{
"[&::-webkit-progress-bar]:bg-yellow-200 [&::-webkit-progress-value]:bg-yellow-400":
progress < 100,
"[&::-webkit-progress-value]:bg-green-400":
progress === 100,
},
)}
/>
)}
<div className="absolute inset-0 flex gap-2 items-center justify-center text-xs font-semibold select-none">
{progress < 100 && (
<>
<Loader
className="animate-spin"
size={16}
strokeWidth={2}
/>
<span>
Screening paper {completedCount} of {totalCount}
</span>
</>
)}
{progress === 100 && errorCount === 0 && (
<>
<CircleCheck size={14} className="text-green-600" />
<span className="text-green-600">Done</span>
</>
)}
{progress === 100 && errorCount > 0 && (
{status === JobStatus.CANCELLED ? (
<div className="absolute inset-0 flex gap-2 items-center justify-center text-xs font-semibold select-none">
<>
<TriangleAlert
size={14}
className="text-orange-600"
/>
<span className="text-orange-600">
Done with errors ({errorCount})
Task Cancelled ({completedCount}/{totalCount})
</span>
</>
)}
</div>
</div>
) : (
<>
{status === JobStatus.RUNNING && (
<progress
value={progress}
max={100}
className={classNames(
"h-full w-full [&::-webkit-progress-bar]:rounded-xl [&::-webkit-progress-bar]:bg-gray-400 [&::-webkit-progress-value]:bg-blue-200 [&::-webkit-progress-value]:rounded-xl",
{
"[&::-webkit-progress-bar]:bg-yellow-200 [&::-webkit-progress-value]:bg-yellow-400":
progress < 100,
"[&::-webkit-progress-value]:bg-green-400":
progress === 100,
},
)}
/>
)}
<div className="absolute inset-0 flex gap-2 items-center justify-center text-xs font-semibold select-none">
{status === JobStatus.RUNNING && (
<>
<Loader
className="animate-spin"
size={16}
strokeWidth={2}
/>
<span>
Screening paper {completedCount} of {totalCount}
</span>
</>
)}
{status === JobStatus.SUCCESS && (
<>
<CircleCheck size={14} className="text-green-600" />
<span className="text-green-600">Done</span>
</>
)}
{status === JobStatus.PARTIAL_SUCCESS && (
<>
<TriangleAlert
size={14}
className="text-orange-600"
/>
<span className="text-orange-600">
Done with errors ({errorCount})
</span>
</>
)}
</div>
</>
)}
</div>
</div>
<div>
<DropdownMenuEllipsis
items={[
{
label: () => (
<div className="text-yellow-700 flex flex-row gap-3 items-center">
<CircleStop />
<span>Cancel</span>
</div>
),
onClick: () => setJobToCancel(job.uuid),
disabled: progress === 100 || status === JobStatus.CANCELLED,
},
{
label: () => (
<div className="text-red-700 flex flex-row gap-3 items-center">
<XCircle />
<span>Delete</span>
</div>
),
onClick: () => setJobToDelete(job.uuid),
},
]}
/>
</div>
</div>
</Card>
);
Expand Down Expand Up @@ -1110,6 +1203,30 @@ export const ProjectPage = () => {
onClose={() => navigate(`/project/${projectUuid}`)}
/>
)}
{jobToCancel && (
<ConfirmationModal
open={true}
onClose={handleCancelModalClose}
onConfirm={handleTaskCancel}
title="Cancel screening task?"
description="This will cancel running and scheduled screening jobs."
confirmButtonLabel="Cancel task"
confirmButtonVariant="yellow"
confirmButtonIcon={<CircleStop size={16} />}
/>
)}
{jobToDelete && (
<ConfirmationModal
open={true}
onClose={handleDeleteModalClose}
onConfirm={handleTaskDelete}
title="Delete screening task?"
description="This action cannot be undone. All data related to this task will be permanently deleted."
confirmButtonLabel="Delete"
confirmButtonVariant="red"
confirmButtonIcon={<Trash2 size={16} />}
/>
)}
</Layout>
);
};
20 changes: 20 additions & 0 deletions client/src/services/jobService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,23 @@ export const fetchJobsForProject = async (projectUuid: string) => {
throw error;
}
};

export const cancelJob = async (jobUuid: string) => {
try {
const res = await api.post(`/api/v1/job/${jobUuid}/cancel`);
return res.data;
} catch (error) {
console.error("Canceling task unsuccessful:", error);
throw error;
}
};

export const deleteJob = async (jobUuid: string) => {
try {
const res = await api.delete(`/api/v1/job/${jobUuid}`);
return res.data;
} catch (error) {
console.error("Task deletion unsuccessful:", error);
throw error;
}
}
Loading
Loading