From a4ccb41e7e6a0717fabe88b78ee4f4c67b267523 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Wed, 23 Jul 2025 14:43:20 +0000 Subject: [PATCH 01/17] feat: add preset selector in TasksPage --- site/src/pages/TasksPage/TasksPage.tsx | 120 +++++++++++++++++++------ 1 file changed, 92 insertions(+), 28 deletions(-) diff --git a/site/src/pages/TasksPage/TasksPage.tsx b/site/src/pages/TasksPage/TasksPage.tsx index d678098affd17..93be42e8e4879 100644 --- a/site/src/pages/TasksPage/TasksPage.tsx +++ b/site/src/pages/TasksPage/TasksPage.tsx @@ -2,7 +2,11 @@ import Skeleton from "@mui/material/Skeleton"; import { API } from "api/api"; import { getErrorDetail, getErrorMessage } from "api/errors"; import { disabledRefetchOptions } from "api/queries/util"; -import type { Template, TemplateVersionExternalAuth } from "api/typesGenerated"; +import type { + Preset, + Template, + TemplateVersionExternalAuth, +} from "api/typesGenerated"; import { ErrorAlert } from "components/Alert/ErrorAlert"; import { Avatar } from "components/Avatar/Avatar"; import { AvatarData } from "components/Avatar/AvatarData"; @@ -50,7 +54,7 @@ import { RedoIcon, RotateCcwIcon, SendIcon } from "lucide-react"; import { AI_PROMPT_PARAMETER_NAME, type Task } from "modules/tasks/tasks"; import { WorkspaceAppStatus } from "modules/workspaces/WorkspaceAppStatus/WorkspaceAppStatus"; import { generateWorkspaceName } from "modules/workspaces/generateWorkspaceName"; -import { type FC, type ReactNode, useState } from "react"; +import { type FC, type ReactNode, useEffect, useState } from "react"; import { Helmet } from "react-helmet-async"; import { useMutation, useQuery, useQueryClient } from "react-query"; import { Link as RouterLink, useNavigate } from "react-router-dom"; @@ -210,7 +214,11 @@ const TaskFormSection: FC<{ ); }; -type CreateTaskMutationFnProps = { prompt: string; templateVersionId: string }; +type CreateTaskMutationFnProps = { + prompt: string; + templateVersionId: string; + presetId: string | null; +}; type TaskFormProps = { templates: Template[]; @@ -223,6 +231,8 @@ const TaskForm: FC = ({ templates, onSuccess }) => { const [selectedTemplateId, setSelectedTemplateId] = useState( templates[0].id, ); + const [presets, setPresets] = useState(null); + const [selectedPresetId, setSelectedPresetId] = useState(null); const selectedTemplate = templates.find( (t) => t.id === selectedTemplateId, ) as Template; @@ -232,6 +242,28 @@ const TaskForm: FC = ({ templates, onSuccess }) => { isPollingExternalAuth, isLoadingExternalAuth, } = useExternalAuth(selectedTemplate.active_version_id); + + // Fetch presets when template changes + const { data: presetsData } = useQuery({ + queryKey: ["template-version-presets", selectedTemplate.active_version_id], + queryFn: () => + API.getTemplateVersionPresets(selectedTemplate.active_version_id), + ...disabledRefetchOptions, + }); + + // Handle preset data changes + useEffect(() => { + if (presetsData) { + setPresets(presetsData); + // Set default preset if available + const defaultPreset = presetsData.find((p: Preset) => p.Default); + if (defaultPreset) { + setSelectedPresetId(defaultPreset.ID); + } else { + setSelectedPresetId(null); + } + } + }, [presetsData]); const missedExternalAuth = externalAuth?.filter( (auth) => !auth.optional && !auth.authenticated, ); @@ -243,8 +275,9 @@ const TaskForm: FC = ({ templates, onSuccess }) => { mutationFn: async ({ prompt, templateVersionId, + presetId, }: CreateTaskMutationFnProps) => - data.createTask(prompt, user.id, templateVersionId), + data.createTask(prompt, user.id, templateVersionId, presetId), onSuccess: async (task) => { await queryClient.invalidateQueries({ queryKey: ["tasks"], @@ -265,6 +298,7 @@ const TaskForm: FC = ({ templates, onSuccess }) => { await createTaskMutation.mutateAsync({ prompt, templateVersionId: selectedTemplate.active_version_id, + presetId: selectedPresetId, }); } catch (error) { const message = getErrorMessage(error, "Error creating task"); @@ -297,27 +331,50 @@ const TaskForm: FC = ({ templates, onSuccess }) => { text-sm shadow-sm text-content-primary placeholder:text-content-secondary md:text-sm`} />
- +
+ + + {presets && presets.length > 0 && ( + + )} +
{missedExternalAuth && ( @@ -608,13 +665,20 @@ export const data = { prompt: string, userId: string, templateVersionId: string, + presetId: string | null = null, ): Promise { - const presets = await API.getTemplateVersionPresets(templateVersionId); - const defaultPreset = presets?.find((p) => p.Default); + // If no preset is selected, get the default preset + let preset_id: string | undefined = presetId || undefined; + if (!preset_id) { + const presets = await API.getTemplateVersionPresets(templateVersionId); + const defaultPreset = presets?.find((p) => p.Default); + preset_id = defaultPreset?.ID; + } + const workspace = await API.createWorkspace(userId, { name: `task-${generateWorkspaceName()}`, template_version_id: templateVersionId, - template_version_preset_id: defaultPreset?.ID, + template_version_preset_id: preset_id || undefined, rich_parameter_values: [ { name: AI_PROMPT_PARAMETER_NAME, value: prompt }, ], From 8cafbc9825206e1b407cff232a9d13704f56649e Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Thu, 24 Jul 2025 11:11:43 +0100 Subject: [PATCH 02/17] sort presets with default at top --- site/src/pages/TasksPage/TasksPage.tsx | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/site/src/pages/TasksPage/TasksPage.tsx b/site/src/pages/TasksPage/TasksPage.tsx index 93be42e8e4879..bdddac2c01b45 100644 --- a/site/src/pages/TasksPage/TasksPage.tsx +++ b/site/src/pages/TasksPage/TasksPage.tsx @@ -364,13 +364,21 @@ const TaskForm: FC = ({ templates, onSuccess }) => { - {presets.map((preset) => ( - - - {preset.Name} {preset.Default && "(Default)"} - - - ))} + {presets + .sort((a, b) => { + // Default preset should come first + if (a.Default && !b.Default) return -1; + if (!a.Default && b.Default) return 1; + // Otherwise, sort alphabetically by name + return a.Name.localeCompare(b.Name); + }) + .map((preset) => ( + + + {preset.Name} {preset.Default && "(Default)"} + + + ))} )} From 071c8968898632ce6353afb078fd4bf11ff4339f Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Thu, 24 Jul 2025 11:13:53 +0100 Subject: [PATCH 03/17] add labels for template and preset selectors --- site/src/pages/TasksPage/TasksPage.tsx | 106 +++++++++++++++---------- 1 file changed, 65 insertions(+), 41 deletions(-) diff --git a/site/src/pages/TasksPage/TasksPage.tsx b/site/src/pages/TasksPage/TasksPage.tsx index bdddac2c01b45..6e652ab16259e 100644 --- a/site/src/pages/TasksPage/TasksPage.tsx +++ b/site/src/pages/TasksPage/TasksPage.tsx @@ -331,56 +331,80 @@ const TaskForm: FC = ({ templates, onSuccess }) => { text-sm shadow-sm text-content-primary placeholder:text-content-secondary md:text-sm`} />
-
- - - {presets && presets.length > 0 && ( +
+
+ +
+ + {presets && presets.length > 0 && ( +
+ + +
)}
From 9938038c7abe05789e682ad51a6258bdb0f452ca Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Thu, 24 Jul 2025 11:18:07 +0100 Subject: [PATCH 04/17] increase template and preset select width --- site/src/pages/TasksPage/TasksPage.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/site/src/pages/TasksPage/TasksPage.tsx b/site/src/pages/TasksPage/TasksPage.tsx index 6e652ab16259e..90955451ee5d5 100644 --- a/site/src/pages/TasksPage/TasksPage.tsx +++ b/site/src/pages/TasksPage/TasksPage.tsx @@ -347,7 +347,7 @@ const TaskForm: FC = ({ templates, onSuccess }) => { > @@ -382,7 +382,7 @@ const TaskForm: FC = ({ templates, onSuccess }) => { > From ec3d72d9f7386e03b7e17f29bda8853872cd0e51 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Thu, 24 Jul 2025 11:29:00 +0100 Subject: [PATCH 05/17] override ai prompt with preset if defined --- site/src/pages/TasksPage/TasksPage.tsx | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/site/src/pages/TasksPage/TasksPage.tsx b/site/src/pages/TasksPage/TasksPage.tsx index 90955451ee5d5..5cfc77868f6e8 100644 --- a/site/src/pages/TasksPage/TasksPage.tsx +++ b/site/src/pages/TasksPage/TasksPage.tsx @@ -236,6 +236,13 @@ const TaskForm: FC = ({ templates, onSuccess }) => { const selectedTemplate = templates.find( (t) => t.id === selectedTemplateId, ) as Template; + + // Extract AI prompt from selected preset + const selectedPreset = presets?.find((p) => p.ID === selectedPresetId); + const presetAIPrompt = selectedPreset?.Parameters.find( + (param) => param.Name === "ai_prompt", + )?.Value; + const isPromptReadOnly = !!presetAIPrompt; const { externalAuth, externalAuthError, @@ -291,8 +298,7 @@ const TaskForm: FC = ({ templates, onSuccess }) => { const form = e.currentTarget; const formData = new FormData(form); - const prompt = formData.get("prompt") as string; - const templateID = formData.get("templateID") as string; + const prompt = presetAIPrompt || (formData.get("prompt") as string); try { await createTaskMutation.mutateAsync({ @@ -326,9 +332,13 @@ const TaskForm: FC = ({ templates, onSuccess }) => { required id="prompt" name="prompt" + value={presetAIPrompt || undefined} + readOnly={isPromptReadOnly} placeholder={textareaPlaceholder} className={`border-0 resize-none w-full h-full bg-transparent rounded-lg outline-none flex min-h-[60px] - text-sm shadow-sm text-content-primary placeholder:text-content-secondary md:text-sm`} + text-sm shadow-sm text-content-primary placeholder:text-content-secondary md:text-sm ${ + isPromptReadOnly ? "opacity-60 cursor-not-allowed" : "" + }`} />
From 31901f94dab76c0a8664a928b5cdb027e03b7691 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Thu, 24 Jul 2025 11:42:45 +0100 Subject: [PATCH 06/17] hide presets selector if no presets exist for template --- site/src/pages/TasksPage/TasksPage.tsx | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/site/src/pages/TasksPage/TasksPage.tsx b/site/src/pages/TasksPage/TasksPage.tsx index 5cfc77868f6e8..450a324d2b145 100644 --- a/site/src/pages/TasksPage/TasksPage.tsx +++ b/site/src/pages/TasksPage/TasksPage.tsx @@ -260,14 +260,19 @@ const TaskForm: FC = ({ templates, onSuccess }) => { // Handle preset data changes useEffect(() => { - if (presetsData) { + if (presetsData !== undefined) { setPresets(presetsData); - // Set default preset if available - const defaultPreset = presetsData.find((p: Preset) => p.Default); - if (defaultPreset) { - setSelectedPresetId(defaultPreset.ID); - } else { + // Reset selected preset when changing templates or when no presets available + if (presetsData === null || presetsData.length === 0) { setSelectedPresetId(null); + } else { + // Set default preset if available + const defaultPreset = presetsData.find((p: Preset) => p.Default); + if (defaultPreset) { + setSelectedPresetId(defaultPreset.ID); + } else { + setSelectedPresetId(null); + } } } }, [presetsData]); From c4dbd2f2547c37604599ab6658c8dae0278a999d Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Thu, 24 Jul 2025 12:19:03 +0100 Subject: [PATCH 07/17] fix ai prompt parameter name --- site/src/pages/TasksPage/TasksPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/src/pages/TasksPage/TasksPage.tsx b/site/src/pages/TasksPage/TasksPage.tsx index 450a324d2b145..05a64e2c38946 100644 --- a/site/src/pages/TasksPage/TasksPage.tsx +++ b/site/src/pages/TasksPage/TasksPage.tsx @@ -240,7 +240,7 @@ const TaskForm: FC = ({ templates, onSuccess }) => { // Extract AI prompt from selected preset const selectedPreset = presets?.find((p) => p.ID === selectedPresetId); const presetAIPrompt = selectedPreset?.Parameters.find( - (param) => param.Name === "ai_prompt", + (param) => param.Name === AI_PROMPT_PARAMETER_NAME, )?.Value; const isPromptReadOnly = !!presetAIPrompt; const { From 72db7b69baad476492a9f00526721d558cb518f8 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Thu, 24 Jul 2025 12:19:17 +0100 Subject: [PATCH 08/17] update storybook --- .../src/pages/TasksPage/TasksPage.stories.tsx | 125 ++++++++++-------- site/src/testHelpers/entities.ts | 98 ++++++++++++++ 2 files changed, 167 insertions(+), 56 deletions(-) diff --git a/site/src/pages/TasksPage/TasksPage.stories.tsx b/site/src/pages/TasksPage/TasksPage.stories.tsx index 1b1770f586768..85052484d31f0 100644 --- a/site/src/pages/TasksPage/TasksPage.stories.tsx +++ b/site/src/pages/TasksPage/TasksPage.stories.tsx @@ -4,12 +4,14 @@ import { API } from "api/api"; import { MockUsers } from "pages/UsersPage/storybookData/users"; import { reactRouterParameters } from "storybook-addon-remix-react-router"; import { + MockAIPromptPresets, + MockNewTaskData, + MockPresets, + MockTasks, MockTemplate, MockTemplateVersionExternalAuthGithub, MockTemplateVersionExternalAuthGithubAuthenticated, MockUserOwner, - MockWorkspace, - MockWorkspaceAppStatus, mockApiError, } from "testHelpers/entities"; import { @@ -31,6 +33,7 @@ const meta: Meta = { }, beforeEach: () => { spyOn(API, "getTemplateVersionExternalAuth").mockResolvedValue([]); + spyOn(API, "getTemplateVersionPresets").mockResolvedValue(null); spyOn(API, "getUsers").mockResolvedValue({ users: MockUsers, count: MockUsers.length, @@ -53,7 +56,7 @@ type Story = StoryObj; export const LoadingAITemplates: Story = { beforeEach: () => { spyOn(data, "fetchAITemplates").mockImplementation( - () => new Promise((res) => 1000 * 60 * 60), + () => new Promise(() => 1000 * 60 * 60), ); }, }; @@ -79,7 +82,7 @@ export const LoadingTasks: Story = { beforeEach: () => { spyOn(data, "fetchAITemplates").mockResolvedValue([MockTemplate]); spyOn(data, "fetchTasks").mockImplementation( - () => new Promise((res) => 1000 * 60 * 60), + () => new Promise(() => 1000 * 60 * 60), ); }, play: async ({ canvasElement, step }) => { @@ -119,15 +122,57 @@ export const LoadedTasks: Story = { }, }; -const newTaskData = { - prompt: "Create a new task", - workspace: { - ...MockWorkspace, - id: "workspace-4", - latest_app_status: { - ...MockWorkspaceAppStatus, - message: "Task created successfully!", - }, +export const LoadedTasksWithPresets: Story = { + decorators: [withProxyProvider()], + beforeEach: () => { + const mockTemplateWithPresets = { + ...MockTemplate, + id: "test-template-2", + name: "template-with-presets", + display_name: "Template with Presets", + }; + + spyOn(data, "fetchAITemplates").mockResolvedValue([ + MockTemplate, + mockTemplateWithPresets, + ]); + spyOn(data, "fetchTasks").mockResolvedValue(MockTasks); + spyOn(API, "getTemplateVersionPresets").mockImplementation( + async (versionId) => { + // Return presets only for the second template + if (versionId === mockTemplateWithPresets.active_version_id) { + return MockPresets; + } + return null; + }, + ); + }, +}; + +export const LoadedTasksWithAIPromptPresets: Story = { + decorators: [withProxyProvider()], + beforeEach: () => { + const mockTemplateWithPresets = { + ...MockTemplate, + id: "test-template-2", + name: "template-with-presets", + display_name: "Template with AI Prompt Presets", + }; + + spyOn(data, "fetchAITemplates").mockResolvedValue([ + MockTemplate, + mockTemplateWithPresets, + ]); + spyOn(data, "fetchTasks").mockResolvedValue(MockTasks); + spyOn(API, "getTemplateVersionPresets").mockImplementation( + async (versionId) => { + // Return presets only for the second template + if (versionId === mockTemplateWithPresets.active_version_id) { + return MockAIPromptPresets; + } + return null; + }, + ); }, }; @@ -154,15 +199,15 @@ export const CreateTaskSuccessfully: Story = { spyOn(data, "fetchAITemplates").mockResolvedValue([MockTemplate]); spyOn(data, "fetchTasks") .mockResolvedValueOnce(MockTasks) - .mockResolvedValue([newTaskData, ...MockTasks]); - spyOn(data, "createTask").mockResolvedValue(newTaskData); + .mockResolvedValue([MockNewTaskData, ...MockTasks]); + spyOn(data, "createTask").mockResolvedValue(MockNewTaskData); }, play: async ({ canvasElement, step }) => { const canvas = within(canvasElement); await step("Run task", async () => { const prompt = await canvas.findByLabelText(/prompt/i); - await userEvent.type(prompt, newTaskData.prompt); + await userEvent.type(prompt, MockNewTaskData.prompt); const submitButton = canvas.getByRole("button", { name: /run task/i }); await waitFor(() => expect(submitButton).toBeEnabled()); await userEvent.click(submitButton); @@ -208,8 +253,8 @@ export const WithAuthenticatedExternalAuth: Story = { beforeEach: () => { spyOn(data, "fetchTasks") .mockResolvedValueOnce(MockTasks) - .mockResolvedValue([newTaskData, ...MockTasks]); - spyOn(data, "createTask").mockResolvedValue(newTaskData); + .mockResolvedValue([MockNewTaskData, ...MockTasks]); + spyOn(data, "createTask").mockResolvedValue(MockNewTaskData); spyOn(API, "getTemplateVersionExternalAuth").mockResolvedValue([ MockTemplateVersionExternalAuthGithubAuthenticated, ]); @@ -235,8 +280,8 @@ export const MissingExternalAuth: Story = { beforeEach: () => { spyOn(data, "fetchTasks") .mockResolvedValueOnce(MockTasks) - .mockResolvedValue([newTaskData, ...MockTasks]); - spyOn(data, "createTask").mockResolvedValue(newTaskData); + .mockResolvedValue([MockNewTaskData, ...MockTasks]); + spyOn(data, "createTask").mockResolvedValue(MockNewTaskData); spyOn(API, "getTemplateVersionExternalAuth").mockResolvedValue([ MockTemplateVersionExternalAuthGithub, ]); @@ -246,7 +291,7 @@ export const MissingExternalAuth: Story = { await step("Submit is disabled", async () => { const prompt = await canvas.findByLabelText(/prompt/i); - await userEvent.type(prompt, newTaskData.prompt); + await userEvent.type(prompt, MockNewTaskData.prompt); const submitButton = canvas.getByRole("button", { name: /run task/i }); expect(submitButton).toBeDisabled(); }); @@ -262,8 +307,8 @@ export const ExternalAuthError: Story = { beforeEach: () => { spyOn(data, "fetchTasks") .mockResolvedValueOnce(MockTasks) - .mockResolvedValue([newTaskData, ...MockTasks]); - spyOn(data, "createTask").mockResolvedValue(newTaskData); + .mockResolvedValue([MockNewTaskData, ...MockTasks]); + spyOn(data, "createTask").mockResolvedValue(MockNewTaskData); spyOn(API, "getTemplateVersionExternalAuth").mockRejectedValue( mockApiError({ message: "Failed to load external auth", @@ -275,7 +320,7 @@ export const ExternalAuthError: Story = { await step("Submit is disabled", async () => { const prompt = await canvas.findByLabelText(/prompt/i); - await userEvent.type(prompt, newTaskData.prompt); + await userEvent.type(prompt, MockNewTaskData.prompt); const submitButton = canvas.getByRole("button", { name: /run task/i }); expect(submitButton).toBeDisabled(); }); @@ -308,35 +353,3 @@ export const NonAdmin: Story = { }); }, }; - -const MockTasks = [ - { - workspace: { - ...MockWorkspace, - latest_app_status: MockWorkspaceAppStatus, - }, - prompt: "Create competitors page", - }, - { - workspace: { - ...MockWorkspace, - id: "workspace-2", - latest_app_status: { - ...MockWorkspaceAppStatus, - message: "Avatar size fixed!", - }, - }, - prompt: "Fix user avatar size", - }, - { - workspace: { - ...MockWorkspace, - id: "workspace-3", - latest_app_status: { - ...MockWorkspaceAppStatus, - message: "Accessibility issues fixed!", - }, - }, - prompt: "Fix accessibility issues", - }, -]; diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index 045d6ad06ddeb..fe7202814d846 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -4571,3 +4571,101 @@ export function createTimestamp(minuteOffset: number, secondOffset: number) { baseDate.setSeconds(baseDate.getSeconds() + secondOffset); return baseDate.toISOString(); } + +// Mock Presets for AI Tasks +export const MockPresets: TypesGen.Preset[] = [ + { + ID: "preset-1", + Name: "Development", + Parameters: [ + { Name: "cpu", Value: "4" }, + { Name: "memory", Value: "8GB" }, + ], + Default: true, + }, + { + ID: "preset-2", + Name: "Testing", + Parameters: [ + { Name: "cpu", Value: "2" }, + { Name: "memory", Value: "4GB" }, + ], + Default: false, + }, + { + ID: "preset-3", + Name: "Production", + Parameters: [ + { Name: "cpu", Value: "8" }, + { Name: "memory", Value: "16GB" }, + ], + Default: false, + }, +]; + +export const MockAIPromptPresets: TypesGen.Preset[] = [ + { + ID: "ai-preset-1", + Name: "Code Review", + Parameters: [ + { Name: "AI Prompt", Value: "Review the code for best practices" }, + { Name: "cpu", Value: "4" }, + { Name: "memory", Value: "8GB" }, + ], + Default: true, + }, + { + ID: "ai-preset-2", + Name: "Custom Prompt", + Parameters: [ + { Name: "cpu", Value: "4" }, + { Name: "memory", Value: "8GB" }, + ], + Default: false, + }, +]; + +// Mock Tasks for AI Tasks page +export const MockTasks = [ + { + workspace: { + ...MockWorkspace, + latest_app_status: MockWorkspaceAppStatus, + }, + prompt: "Create competitors page", + }, + { + workspace: { + ...MockWorkspace, + id: "workspace-2", + latest_app_status: { + ...MockWorkspaceAppStatus, + message: "Avatar size fixed!", + }, + }, + prompt: "Fix user avatar size", + }, + { + workspace: { + ...MockWorkspace, + id: "workspace-3", + latest_app_status: { + ...MockWorkspaceAppStatus, + message: "Accessibility issues fixed!", + }, + }, + prompt: "Fix accessibility issues", + }, +]; + +export const MockNewTaskData = { + prompt: "Create a new task", + workspace: { + ...MockWorkspace, + id: "workspace-4", + latest_app_status: { + ...MockWorkspaceAppStatus, + message: "Task created successfully!", + }, + }, +}; From 4f2d820a1bf8c5a80b260219dad247fad8d065cf Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Thu, 24 Jul 2025 12:55:45 +0100 Subject: [PATCH 09/17] address copilot comments --- site/src/pages/TasksPage/TasksPage.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/site/src/pages/TasksPage/TasksPage.tsx b/site/src/pages/TasksPage/TasksPage.tsx index 05a64e2c38946..5efc6584cf3ae 100644 --- a/site/src/pages/TasksPage/TasksPage.tsx +++ b/site/src/pages/TasksPage/TasksPage.tsx @@ -715,11 +715,13 @@ export const data = { presetId: string | null = null, ): Promise { // If no preset is selected, get the default preset - let preset_id: string | undefined = presetId || undefined; + let preset_id = presetId; if (!preset_id) { const presets = await API.getTemplateVersionPresets(templateVersionId); const defaultPreset = presets?.find((p) => p.Default); - preset_id = defaultPreset?.ID; + if (defaultPreset) { + preset_id = defaultPreset.ID; + } } const workspace = await API.createWorkspace(userId, { From 7602ef6d626650a37b828fc3d49e1d7ed408b3af Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Thu, 24 Jul 2025 14:23:53 +0100 Subject: [PATCH 10/17] address code review comments --- site/src/pages/TasksPage/TasksPage.tsx | 55 ++++++++++++-------------- 1 file changed, 26 insertions(+), 29 deletions(-) diff --git a/site/src/pages/TasksPage/TasksPage.tsx b/site/src/pages/TasksPage/TasksPage.tsx index 5efc6584cf3ae..6d7691897e66b 100644 --- a/site/src/pages/TasksPage/TasksPage.tsx +++ b/site/src/pages/TasksPage/TasksPage.tsx @@ -260,21 +260,14 @@ const TaskForm: FC = ({ templates, onSuccess }) => { // Handle preset data changes useEffect(() => { - if (presetsData !== undefined) { - setPresets(presetsData); - // Reset selected preset when changing templates or when no presets available - if (presetsData === null || presetsData.length === 0) { - setSelectedPresetId(null); - } else { - // Set default preset if available - const defaultPreset = presetsData.find((p: Preset) => p.Default); - if (defaultPreset) { - setSelectedPresetId(defaultPreset.ID); - } else { - setSelectedPresetId(null); - } - } + if (!presetsData) { + setPresets(null); + return; } + setPresets(presetsData); + const defaultPreset = presetsData.find((p: Preset) => p.Default); + const defaultPresetID = defaultPreset?.ID || null; + setSelectedPresetId(defaultPresetID); }, [presetsData]); const missedExternalAuth = externalAuth?.filter( (auth) => !auth.optional && !auth.authenticated, @@ -402,21 +395,13 @@ const TaskForm: FC = ({ templates, onSuccess }) => { - {presets - .sort((a, b) => { - // Default preset should come first - if (a.Default && !b.Default) return -1; - if (!a.Default && b.Default) return 1; - // Otherwise, sort alphabetically by name - return a.Name.localeCompare(b.Name); - }) - .map((preset) => ( - - - {preset.Name} {preset.Default && "(Default)"} - - - ))} + {sortedPresets(presets).map((preset) => ( + + + {preset.Name} {preset.Default && "(Default)"} + + + ))}
@@ -740,4 +725,16 @@ export const data = { }, }; +// sortedPresets sorts presets with the default preset first, +// followed by the rest sorted alphabetically by name ascending. +const sortedPresets = (presets: Preset[]): Preset[] => { + return presets.sort((a, b) => { + // Default preset should come first + if (a.Default && !b.Default) return -1; + if (!a.Default && b.Default) return 1; + // Otherwise, sort alphabetically by name + return a.Name.localeCompare(b.Name); + }); +}; + export default TasksPage; From 80b96de8b2dd05e5accbf9eed979bec017cbf136 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Thu, 24 Jul 2025 16:25:53 +0100 Subject: [PATCH 11/17] fix crash if presets is null --- .../src/pages/TasksPage/TasksPage.stories.tsx | 20 +++++++++++++++++++ site/src/pages/TasksPage/TasksPage.tsx | 2 +- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/site/src/pages/TasksPage/TasksPage.stories.tsx b/site/src/pages/TasksPage/TasksPage.stories.tsx index 85052484d31f0..0bdc9d27a7eed 100644 --- a/site/src/pages/TasksPage/TasksPage.stories.tsx +++ b/site/src/pages/TasksPage/TasksPage.stories.tsx @@ -176,6 +176,26 @@ export const LoadedTasksWithAIPromptPresets: Story = { }, }; +export const LoadedTasksEdgeCases: Story = { + decorators: [withProxyProvider()], + beforeEach: () => { + spyOn(data, "fetchAITemplates").mockResolvedValue([MockTemplate]); + spyOn(data, "fetchTasks").mockResolvedValue(MockTasks); + + // Test various edge cases for presets + spyOn(API, "getTemplateVersionPresets").mockImplementation(async () => { + return [ + { + ID: "malformed", + Name: "Malformed Preset", + Default: true, + }, + // biome-ignore lint/suspicious/noExplicitAny: Testing malformed data edge cases + ] as any; + }); + }, +}; + export const CreateTaskSuccessfully: Story = { decorators: [withProxyProvider()], parameters: { diff --git a/site/src/pages/TasksPage/TasksPage.tsx b/site/src/pages/TasksPage/TasksPage.tsx index 6d7691897e66b..c327bedee818b 100644 --- a/site/src/pages/TasksPage/TasksPage.tsx +++ b/site/src/pages/TasksPage/TasksPage.tsx @@ -239,7 +239,7 @@ const TaskForm: FC = ({ templates, onSuccess }) => { // Extract AI prompt from selected preset const selectedPreset = presets?.find((p) => p.ID === selectedPresetId); - const presetAIPrompt = selectedPreset?.Parameters.find( + const presetAIPrompt = selectedPreset?.Parameters?.find( (param) => param.Name === AI_PROMPT_PARAMETER_NAME, )?.Value; const isPromptReadOnly = !!presetAIPrompt; From e248c147a04500ed7f20fb7f650d1fad568eaf65 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Thu, 24 Jul 2025 16:58:38 +0100 Subject: [PATCH 12/17] adjust label styling --- site/src/pages/TasksPage/TasksPage.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/site/src/pages/TasksPage/TasksPage.tsx b/site/src/pages/TasksPage/TasksPage.tsx index c327bedee818b..ba24f04a7e4df 100644 --- a/site/src/pages/TasksPage/TasksPage.tsx +++ b/site/src/pages/TasksPage/TasksPage.tsx @@ -343,7 +343,7 @@ const TaskForm: FC = ({ templates, onSuccess }) => {
@@ -377,7 +377,7 @@ const TaskForm: FC = ({ templates, onSuccess }) => {
From bf6f11303f39ea290e81db85341aa5e46617bb34 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Thu, 24 Jul 2025 17:58:43 +0100 Subject: [PATCH 13/17] correctly handle selecting default preset on presetData change --- site/src/pages/TasksPage/TasksPage.tsx | 88 +++++++++++++++++--------- 1 file changed, 57 insertions(+), 31 deletions(-) diff --git a/site/src/pages/TasksPage/TasksPage.tsx b/site/src/pages/TasksPage/TasksPage.tsx index ba24f04a7e4df..385e2e9a6aa52 100644 --- a/site/src/pages/TasksPage/TasksPage.tsx +++ b/site/src/pages/TasksPage/TasksPage.tsx @@ -231,18 +231,11 @@ const TaskForm: FC = ({ templates, onSuccess }) => { const [selectedTemplateId, setSelectedTemplateId] = useState( templates[0].id, ); - const [presets, setPresets] = useState(null); const [selectedPresetId, setSelectedPresetId] = useState(null); const selectedTemplate = templates.find( (t) => t.id === selectedTemplateId, ) as Template; - // Extract AI prompt from selected preset - const selectedPreset = presets?.find((p) => p.ID === selectedPresetId); - const presetAIPrompt = selectedPreset?.Parameters?.find( - (param) => param.Name === AI_PROMPT_PARAMETER_NAME, - )?.Value; - const isPromptReadOnly = !!presetAIPrompt; const { externalAuth, externalAuthError, @@ -251,24 +244,41 @@ const TaskForm: FC = ({ templates, onSuccess }) => { } = useExternalAuth(selectedTemplate.active_version_id); // Fetch presets when template changes - const { data: presetsData } = useQuery({ + const { data: presetsData, isLoading: isLoadingPresets } = useQuery< + Preset[] | null, + Error + >({ queryKey: ["template-version-presets", selectedTemplate.active_version_id], queryFn: () => API.getTemplateVersionPresets(selectedTemplate.active_version_id), ...disabledRefetchOptions, }); - // Handle preset data changes + // Handle preset selection when data changes useEffect(() => { - if (!presetsData) { - setPresets(null); + if (presetsData === undefined) { + // Still loading + return; + } + + if (!presetsData || presetsData.length === 0) { + setSelectedPresetId(null); return; } - setPresets(presetsData); + + // Always select the default preset when new data arrives const defaultPreset = presetsData.find((p: Preset) => p.Default); const defaultPresetID = defaultPreset?.ID || null; setSelectedPresetId(defaultPresetID); }, [presetsData]); + + // Extract AI prompt from selected preset + const selectedPreset = presetsData?.find((p) => p.ID === selectedPresetId); + const presetAIPrompt = selectedPreset?.Parameters?.find( + (param) => param.Name === AI_PROMPT_PARAMETER_NAME, + )?.Value; + const isPromptReadOnly = !!presetAIPrompt; + const missedExternalAuth = externalAuth?.filter( (auth) => !auth.optional && !auth.authenticated, ); @@ -373,39 +383,55 @@ const TaskForm: FC = ({ templates, onSuccess }) => {
- {presets && presets.length > 0 && ( -
- +
+ + {isLoadingPresets ? ( + + ) : ( -
- )} + )} +
From b3916e29c06902e922716eb600701d27799c34b8 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Thu, 24 Jul 2025 18:20:23 +0100 Subject: [PATCH 14/17] DesiredPrebuildInstances --- site/src/testHelpers/entities.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index fe7202814d846..14fbb2d2913af 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -4582,6 +4582,7 @@ export const MockPresets: TypesGen.Preset[] = [ { Name: "memory", Value: "8GB" }, ], Default: true, + DesiredPrebuildInstances: 0, }, { ID: "preset-2", @@ -4591,6 +4592,7 @@ export const MockPresets: TypesGen.Preset[] = [ { Name: "memory", Value: "4GB" }, ], Default: false, + DesiredPrebuildInstances: 0, }, { ID: "preset-3", @@ -4600,6 +4602,7 @@ export const MockPresets: TypesGen.Preset[] = [ { Name: "memory", Value: "16GB" }, ], Default: false, + DesiredPrebuildInstances: 0, }, ]; @@ -4613,6 +4616,7 @@ export const MockAIPromptPresets: TypesGen.Preset[] = [ { Name: "memory", Value: "8GB" }, ], Default: true, + DesiredPrebuildInstances: 0, }, { ID: "ai-preset-2", @@ -4622,6 +4626,7 @@ export const MockAIPromptPresets: TypesGen.Preset[] = [ { Name: "memory", Value: "8GB" }, ], Default: false, + DesiredPrebuildInstances: 0, }, ]; From 0fd55e295bc142270f9f77c0d4688f9b46923954 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Thu, 24 Jul 2025 18:38:16 +0100 Subject: [PATCH 15/17] add label to indicate if prompt is defined by preset --- site/src/pages/TasksPage/TasksPage.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/site/src/pages/TasksPage/TasksPage.tsx b/site/src/pages/TasksPage/TasksPage.tsx index 385e2e9a6aa52..b0c4afbb37743 100644 --- a/site/src/pages/TasksPage/TasksPage.tsx +++ b/site/src/pages/TasksPage/TasksPage.tsx @@ -333,9 +333,14 @@ const TaskForm: FC = ({ templates, onSuccess }) => { className="border border-border border-solid rounded-lg p-4" disabled={createTaskMutation.isPending} > - + {isPromptReadOnly && ( + + )} Date: Thu, 24 Jul 2025 19:46:26 +0100 Subject: [PATCH 16/17] use preexisting query --- site/src/pages/TasksPage/TasksPage.tsx | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/site/src/pages/TasksPage/TasksPage.tsx b/site/src/pages/TasksPage/TasksPage.tsx index b0c4afbb37743..28f2624cd9f90 100644 --- a/site/src/pages/TasksPage/TasksPage.tsx +++ b/site/src/pages/TasksPage/TasksPage.tsx @@ -40,6 +40,7 @@ import { TableRowSkeleton, } from "components/TableLoader/TableLoader"; +import { templateVersionPresets } from "api/queries/templates"; import { ExternalImage } from "components/ExternalImage/ExternalImage"; import { FeatureStageBadge } from "components/FeatureStageBadge/FeatureStageBadge"; import { @@ -247,12 +248,7 @@ const TaskForm: FC = ({ templates, onSuccess }) => { const { data: presetsData, isLoading: isLoadingPresets } = useQuery< Preset[] | null, Error - >({ - queryKey: ["template-version-presets", selectedTemplate.active_version_id], - queryFn: () => - API.getTemplateVersionPresets(selectedTemplate.active_version_id), - ...disabledRefetchOptions, - }); + >(templateVersionPresets(selectedTemplate.active_version_id)); // Handle preset selection when data changes useEffect(() => { From 26801113c929adf4d03e61410beae2a1583c747e Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Thu, 24 Jul 2025 19:48:45 +0100 Subject: [PATCH 17/17] keep form label around --- site/src/pages/TasksPage/TasksPage.tsx | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/site/src/pages/TasksPage/TasksPage.tsx b/site/src/pages/TasksPage/TasksPage.tsx index 28f2624cd9f90..4866dbfb49222 100644 --- a/site/src/pages/TasksPage/TasksPage.tsx +++ b/site/src/pages/TasksPage/TasksPage.tsx @@ -329,14 +329,16 @@ const TaskForm: FC = ({ templates, onSuccess }) => { className="border border-border border-solid rounded-lg p-4" disabled={createTaskMutation.isPending} > - {isPromptReadOnly && ( - - )} + pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy