-
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
Open
LittleChenLiya
wants to merge
37
commits into
bytedance:main
Choose a base branch
from
LittleChenLiya:feat/frontend-ui-rework
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
37 commits
Select commit
Hold shift + click to select a range
013edda
feat(frontend): replace default workspace look with classic UI
LittleChenLiya 8d3b1b2
feat(frontend): add observatory workspace skin
LittleChenLiya 16422d1
fix(frontend): remove observatory sky-toggle button from workspace he…
LittleChenLiya 6426419
chore: checkpoint current frontend UI rework (WIP checkpoint)
LittleChenLiya 0f2de46
feat(frontend): classic skin + UI rework (observatory stripped)
LittleChenLiya 72b9561
fix(frontend): constrain classic skin card width in appearance settings
LittleChenLiya cf3c545
style(frontend): prettier format for CI
LittleChenLiya 85b2ba0
chore(frontend): remove stray scratch files from checkpoint
LittleChenLiya 30e8435
refactor(frontend): remove dead observatory i18n keys from PR1
LittleChenLiya e863f95
refactor(frontend): scheduled tasks page to card grid + detail dialog
LittleChenLiya 441c807
fix(frontend): address review findings - destructive contrast, chats …
LittleChenLiya d26a7fd
feat(frontend): show icon header on create scheduled task page
LittleChenLiya 1a5f32e
fix(frontend): restore destructive contrast in dark theme
LittleChenLiya 3b030e3
refactor(frontend): remove dead css vars, redundant header wrapper, d…
LittleChenLiya d44673a
refactor(frontend): move skin scaffolding out of PR1 (SkinProvider/Pa…
LittleChenLiya a4e03d2
refactor(frontend): split out non-skin extras (chats settings page, z…
LittleChenLiya 9cb7f4e
style(frontend): vertically center new scheduled task form
LittleChenLiya 158be8c
fix(frontend): address review feedback on scheduled-tasks redesign an…
LittleChenLiya 4f14ad9
refactor(frontend): remove orphaned memory/workspace i18n keys and re…
LittleChenLiya e433ad0
feat(frontend): timezone dropdown with common+all zones, and e2e fixes
LittleChenLiya c46d4a8
fix(frontend): forward thread_id to create page, extract run-at valid…
LittleChenLiya 6fae72c
fix(frontend): guard edit submit against empty schedule spec, announc…
LittleChenLiya acca5a2
fix(frontend): gate empty state on query pending, drop dead form-erro…
LittleChenLiya 112be92
fix(frontend): use nullish coalescing for last task error fallback
LittleChenLiya 49b5ad7
fix(frontend): drop orphan fillRequired key, mirror invalidOnce hint …
LittleChenLiya 4c22a5e
refactor(frontend): drop orphan lastRunId i18n key
LittleChenLiya 78f3d19
fix(frontend): restore reuse-thread context notice after rebase onto …
LittleChenLiya 91a0872
test(frontend): close detail dialog before navigating to create page …
LittleChenLiya d8da426
fix(frontend): preserve thread_id on create exits, label back button,…
LittleChenLiya bd73174
fix(frontend): return created scheduled tasks to a list that can cont…
LittleChenLiya bbd2aae
merge origin/main into feat/frontend-ui-rework
LittleChenLiya eb7ee01
merge origin/main and port scheduled-task duplication
LittleChenLiya b5f19c0
Merge remote-tracking branch 'origin/main' into feat/frontend-ui-rework
LittleChenLiya bbf1096
fix(frontend): keep scheduled-task duplicate drafts out of URLs
LittleChenLiya e11d5fa
Merge remote-tracking branch 'origin/main' into feat/frontend-ui-rework
LittleChenLiya 05a1fe0
Merge remote-tracking branch 'origin/main' into feat/frontend-ui-rework
LittleChenLiya 78dd7a5
fix(frontend): restore describeSchedule coverage dropped in rebase fa…
LittleChenLiya File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
308 changes: 308 additions & 0 deletions
308
frontend/src/app/workspace/scheduled-tasks/new/page.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 ( | ||
| <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> | ||
| ); | ||
| } | ||
|
|
||
| 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<ScheduleValue>({ | ||
| 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<ScheduledTask[]>(["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 ( | ||
| <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" | ||
| 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> | ||
| {sourceStatus === "missing" && ( | ||
| <div className="text-destructive text-sm"> | ||
| {st.create.sourceMissing} | ||
| </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 | ||
| key={ | ||
| sourceTaskId | ||
| ? `${sourceTaskId}:${createSchedule.schedule_type}:${JSON.stringify(createSchedule.schedule_spec)}` | ||
| : "new" | ||
| } | ||
| 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={ | ||
| sourceStatus === "loading" || | ||
| sourceStatus === "missing" || | ||
| !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> | ||
| ); | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
[P2] Give the back button an accessible name
This icon-only navigation button has no
aria-labelor other accessible name, so assistive technology announces it only as an unnamed button. Please add a localized label such as “Back to scheduled tasks” (and optionally a matchingtitle).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.
Fixed in d8da426. The icon-only back button now has a localized
aria-label/title(st.create.back: "Back to scheduled tasks" / "返回定时任务").