-
Notifications
You must be signed in to change notification settings - Fork 11.2k
feat(frontend): classic skin default + frontend UI rework #4914
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 33 commits
013edda
8d3b1b2
16422d1
6426419
0f2de46
72b9561
cf3c545
85b2ba0
30e8435
e863f95
441c807
d26a7fd
1a5f32e
3b030e3
d44673a
a4e03d2
9cb7f4e
158be8c
4f14ad9
e433ad0
c46d4a8
6fae72c
acca5a2
112be92
49b5ad7
4c22a5e
78f3d19
91a0872
d8da426
bd73174
bbd2aae
eb7ee01
b5f19c0
bbf1096
e11d5fa
05a1fe0
78dd7a5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,208 @@ | ||
| "use client"; | ||
|
|
||
| import { ArrowLeftIcon, CalendarClock, TriangleAlertIcon } from "lucide-react"; | ||
| import { useRouter, useSearchParams } from "next/navigation"; | ||
| import { 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 { useCreateScheduledTask } from "@/core/scheduled-tasks/hooks"; | ||
|
|
||
| function ReuseThreadNotice({ | ||
| title, | ||
| description, | ||
| }: { | ||
| title: string; | ||
| description: string; | ||
| }) { | ||
| return ( | ||
| <Alert className="border-amber-500/50 bg-amber-500/10"> | ||
| <TriangleAlertIcon className="text-amber-600 dark:text-amber-400" /> | ||
| <AlertTitle>{title}</AlertTitle> | ||
| <AlertDescription>{description}</AlertDescription> | ||
| </Alert> | ||
| ); | ||
| } | ||
|
|
||
| export default function NewScheduledTaskPage() { | ||
| const { t } = useI18n(); | ||
| const router = useRouter(); | ||
| const searchParams = useSearchParams(); | ||
| const initialThreadId = searchParams.get("thread_id"); | ||
| const initialTitle = searchParams.get("title") ?? ""; | ||
| const initialPrompt = searchParams.get("prompt") ?? ""; | ||
| const initialContextMode = searchParams.get("context_mode"); | ||
| const initialScheduleType = searchParams.get("schedule_type"); | ||
| const initialCron = searchParams.get("cron"); | ||
| const initialRunAt = searchParams.get("run_at"); | ||
| const initialTimezone = searchParams.get("timezone"); | ||
| const st = t.scheduledTasks; | ||
| const createTask = useCreateScheduledTask(); | ||
| const [contextMode, setContextMode] = useState< | ||
| "fresh_thread_per_run" | "reuse_thread" | ||
| >( | ||
| initialContextMode === "reuse_thread" || initialThreadId | ||
| ? "reuse_thread" | ||
| : "fresh_thread_per_run", | ||
| ); | ||
| const [targetThreadId, setTargetThreadId] = useState(initialThreadId ?? ""); | ||
| const [title, setTitle] = useState(initialTitle); | ||
| const [prompt, setPrompt] = useState(initialPrompt); | ||
| const [createSchedule, setCreateSchedule] = useState<ScheduleValue>(() => { | ||
| if (initialScheduleType === "once") { | ||
| return { | ||
| schedule_type: "once", | ||
| schedule_spec: initialRunAt ? { run_at: initialRunAt } : {}, | ||
| timezone: initialTimezone ?? "", | ||
| }; | ||
| } | ||
| return { | ||
| schedule_type: "cron", | ||
| schedule_spec: { cron: initialCron ?? "0 9 * * *" }, | ||
| timezone: initialTimezone ?? "", | ||
| }; | ||
| }); | ||
|
|
||
| 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 ( | ||
| <div className="flex size-full flex-col"> | ||
| <header className="flex shrink-0 items-center justify-between gap-3 border-b px-4 py-3"> | ||
| <div className="flex items-center gap-3"> | ||
| <Button | ||
| variant="ghost" | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [P2] Give the back button an accessible name This icon-only navigation button has no
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in d8da426. The icon-only back button now has a localized |
||
| size="icon-sm" | ||
| aria-label={st.create.back} | ||
| title={st.create.back} | ||
| onClick={() => router.push(listHref)} | ||
| > | ||
| <ArrowLeftIcon className="h-4 w-4" /> | ||
| </Button> | ||
| <h1 className="text-sm font-semibold">{st.create.title}</h1> | ||
| </div> | ||
| </header> | ||
| <main className="flex flex-1 justify-center overflow-y-auto px-4 py-6"> | ||
| <div | ||
| className="my-auto w-full max-w-xl space-y-4" | ||
| data-testid="scheduled-task-create-form" | ||
| > | ||
| <div className="space-y-3 pb-2 text-center"> | ||
| <div className="bg-primary/10 mx-auto flex h-14 w-14 items-center justify-center rounded-full"> | ||
| <CalendarClock className="text-primary h-7 w-7" /> | ||
| </div> | ||
| <div className="space-y-1"> | ||
| <h2 className="text-xl font-semibold">{st.create.title}</h2> | ||
| <p className="text-muted-foreground text-sm">{st.description}</p> | ||
| </div> | ||
| </div> | ||
| <div className="flex flex-wrap gap-2"> | ||
| <Button | ||
| type="button" | ||
| variant={ | ||
| contextMode === "fresh_thread_per_run" ? "default" : "outline" | ||
| } | ||
| size="sm" | ||
| onClick={() => setContextMode("fresh_thread_per_run")} | ||
| > | ||
| {st.context.fresh} | ||
| </Button> | ||
| <Button | ||
| type="button" | ||
| variant={contextMode === "reuse_thread" ? "default" : "outline"} | ||
| size="sm" | ||
| onClick={() => setContextMode("reuse_thread")} | ||
| > | ||
| {st.context.reuse} | ||
| </Button> | ||
| </div> | ||
| {contextMode === "reuse_thread" && ( | ||
| <> | ||
| <Input | ||
| value={targetThreadId} | ||
| onChange={(event) => setTargetThreadId(event.target.value)} | ||
| placeholder={st.context.threadIdPlaceholder} | ||
| /> | ||
| <ReuseThreadNotice | ||
| title={st.context.reuseNoticeTitle} | ||
| description={st.context.reuseNoticeDescription} | ||
| /> | ||
| </> | ||
| )} | ||
| <Input | ||
| autoFocus | ||
| value={title} | ||
| onChange={(event) => setTitle(event.target.value)} | ||
| placeholder={st.create.taskTitle} | ||
| /> | ||
| <Textarea | ||
| rows={4} | ||
| value={prompt} | ||
| onChange={(event) => setPrompt(event.target.value)} | ||
| placeholder={st.create.prompt} | ||
| /> | ||
| <ScheduledTaskScheduleInput | ||
| initial={createSchedule} | ||
| onChange={setCreateSchedule} | ||
| /> | ||
| {createSchedule.schedule_type === "once" && | ||
| !createSchedule.schedule_spec.run_at && ( | ||
| <div className="text-muted-foreground text-sm"> | ||
| {st.create.invalidOnce} | ||
| </div> | ||
| )} | ||
| <div className="flex justify-end gap-2"> | ||
| <Button | ||
| variant="outline" | ||
| onClick={() => router.push(listHref)} | ||
| disabled={createTask.isPending} | ||
| > | ||
| {t.common.cancel} | ||
| </Button> | ||
| <Button | ||
| onClick={handleCreate} | ||
| disabled={ | ||
| !title || | ||
| !prompt || | ||
| (!createSchedule.schedule_spec.cron && | ||
| !createSchedule.schedule_spec.run_at) || | ||
| (contextMode === "reuse_thread" && !targetThreadId) || | ||
| createTask.isPending | ||
| } | ||
| > | ||
| {createTask.isPending ? t.common.loading : st.create.submit} | ||
| </Button> | ||
| </div> | ||
| </div> | ||
| </main> | ||
| </div> | ||
| ); | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[P1] Preserve explicit fresh-thread mode
The duplicate route can provide
context_mode=fresh_thread_per_runtogether with a storedthread_id, but this condition converts that combination toreuse_thread. Submitting the duplicate then changes it from isolated runs to reusing an existing conversation. Give an explicitcontext_modeprecedence and infer reuse fromthread_idonly when the mode parameter is absent.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Addressed both P1s:
?from=<taskId>and hydrates the create form from sessionStorage, falling back to the scheduled-tasks list cache/API.context_modetakes precedence overthread_id. Duplicating afresh_thread_per_runtask that still has a storedthread_idstays on isolated runs instead of being coerced toreuse_thread.Also merged origin/main (copy-data cache + sandbox timeout) so this PR is no longer DIRTY.