diff --git a/frontend/src/app/workspace/scheduled-tasks/new/page.tsx b/frontend/src/app/workspace/scheduled-tasks/new/page.tsx new file mode 100644 index 00000000000..dfc9a2aa96b --- /dev/null +++ b/frontend/src/app/workspace/scheduled-tasks/new/page.tsx @@ -0,0 +1,308 @@ +"use client"; + +import { useQueryClient } from "@tanstack/react-query"; +import { ArrowLeftIcon, CalendarClock, TriangleAlertIcon } from "lucide-react"; +import { useRouter, useSearchParams } from "next/navigation"; +import { useEffect, useState } from "react"; + +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Textarea } from "@/components/ui/textarea"; +import { + ScheduledTaskScheduleInput, + type ScheduleValue, +} from "@/components/workspace/scheduled-task-schedule-input"; +import { useI18n } from "@/core/i18n/hooks"; +import { fetchScheduledTasks } from "@/core/scheduled-tasks/api"; +import { + clearDuplicateDraft, + draftFromScheduledTask, + getSessionDuplicateDraftStorage, + readDuplicateDraft, + resolveCreateContextMode, + type DuplicateDraft, +} from "@/core/scheduled-tasks/duplicate-draft"; +import { useCreateScheduledTask } from "@/core/scheduled-tasks/hooks"; +import type { ScheduledTask } from "@/core/scheduled-tasks/types"; + +function ReuseThreadNotice({ + title, + description, +}: { + title: string; + description: string; +}) { + return ( + + + {title} + {description} + + ); +} + +function scheduleFromDraft(draft: DuplicateDraft): ScheduleValue { + const spec = draft.schedule_spec as { cron?: string; run_at?: string }; + if (draft.schedule_type === "once") { + return { + schedule_type: "once", + schedule_spec: spec.run_at ? { run_at: spec.run_at } : {}, + timezone: draft.timezone ?? "", + }; + } + return { + schedule_type: "cron", + schedule_spec: { cron: spec.cron ?? "0 9 * * *" }, + timezone: draft.timezone ?? "", + }; +} + +export default function NewScheduledTaskPage() { + const { t } = useI18n(); + const router = useRouter(); + const searchParams = useSearchParams(); + const queryClient = useQueryClient(); + const sourceTaskId = searchParams.get("from"); + const initialThreadId = searchParams.get("thread_id"); + const initialContextMode = searchParams.get("context_mode"); + const st = t.scheduledTasks; + const createTask = useCreateScheduledTask(); + const [contextMode, setContextMode] = useState< + "fresh_thread_per_run" | "reuse_thread" + >(() => + resolveCreateContextMode({ + contextModeParam: initialContextMode, + threadIdParam: sourceTaskId ? null : initialThreadId, + }), + ); + const [targetThreadId, setTargetThreadId] = useState( + sourceTaskId ? "" : (initialThreadId ?? ""), + ); + const [title, setTitle] = useState(""); + const [prompt, setPrompt] = useState(""); + const [createSchedule, setCreateSchedule] = useState({ + schedule_type: "cron", + schedule_spec: { cron: "0 9 * * *" }, + timezone: "", + }); + const [sourceStatus, setSourceStatus] = useState< + "idle" | "loading" | "ready" | "missing" + >(sourceTaskId ? "loading" : "idle"); + + const applyDraft = (draft: DuplicateDraft) => { + setTitle(draft.title); + setPrompt(draft.prompt); + setContextMode(draft.context_mode); + setTargetThreadId(draft.thread_id ?? ""); + setCreateSchedule(scheduleFromDraft(draft)); + setSourceStatus("ready"); + }; + + useEffect(() => { + if (!sourceTaskId) { + return; + } + let cancelled = false; + const applyIfCurrent = (draft: DuplicateDraft) => { + if (!cancelled) { + applyDraft(draft); + } + }; + + const sessionDraft = readDuplicateDraft( + getSessionDuplicateDraftStorage(), + sourceTaskId, + ); + if (sessionDraft) { + applyIfCurrent(sessionDraft); + clearDuplicateDraft(getSessionDuplicateDraftStorage(), sourceTaskId); + return () => { + cancelled = true; + }; + } + + const cached = queryClient + .getQueryData(["scheduled-tasks"]) + ?.find((task) => task.id === sourceTaskId); + if (cached) { + applyIfCurrent( + draftFromScheduledTask(cached, st.actions.duplicateTitleSuffix), + ); + return () => { + cancelled = true; + }; + } + + void fetchScheduledTasks() + .then((tasks) => { + const found = tasks.find((task) => task.id === sourceTaskId); + if (!found) { + if (!cancelled) { + setSourceStatus("missing"); + } + return; + } + applyIfCurrent( + draftFromScheduledTask(found, st.actions.duplicateTitleSuffix), + ); + }) + .catch(() => { + if (!cancelled) { + setSourceStatus("missing"); + } + }); + + return () => { + cancelled = true; + }; + }, [queryClient, sourceTaskId, st.actions.duplicateTitleSuffix]); + + const listHref = initialThreadId + ? `/workspace/scheduled-tasks?thread_id=${encodeURIComponent(initialThreadId)}` + : "/workspace/scheduled-tasks"; + + const successHref = + contextMode === "reuse_thread" && targetThreadId + ? `/workspace/scheduled-tasks?thread_id=${encodeURIComponent(targetThreadId)}` + : "/workspace/scheduled-tasks"; + + const handleCreate = () => { + createTask.mutate( + { + context_mode: contextMode, + thread_id: contextMode === "reuse_thread" ? targetThreadId : null, + title, + prompt, + schedule_type: createSchedule.schedule_type, + schedule_spec: createSchedule.schedule_spec, + timezone: createSchedule.timezone || "UTC", + }, + { onSuccess: () => router.push(successHref) }, + ); + }; + + return ( +
+
+
+ +

{st.create.title}

+
+
+
+
+
+
+ +
+
+

{st.create.title}

+

{st.description}

+
+
+ {sourceStatus === "missing" && ( +
+ {st.create.sourceMissing} +
+ )} +
+ + +
+ {contextMode === "reuse_thread" && ( + <> + setTargetThreadId(event.target.value)} + placeholder={st.context.threadIdPlaceholder} + /> + + + )} + setTitle(event.target.value)} + placeholder={st.create.taskTitle} + /> +