From b791c0a6968c7e3e3ce9f84d3815b93e01f22c7b Mon Sep 17 00:00:00 2001 From: Hugo Dutka Date: Thu, 19 Jun 2025 17:28:39 +0000 Subject: [PATCH] tasks page efficient backend querying --- site/src/pages/TasksPage/TasksPage.tsx | 326 ++++++++++++------------- 1 file changed, 159 insertions(+), 167 deletions(-) diff --git a/site/src/pages/TasksPage/TasksPage.tsx b/site/src/pages/TasksPage/TasksPage.tsx index 02f7f5651092e..f86979f8eae00 100644 --- a/site/src/pages/TasksPage/TasksPage.tsx +++ b/site/src/pages/TasksPage/TasksPage.tsx @@ -1,3 +1,4 @@ +import Skeleton from "@mui/material/Skeleton"; import { API } from "api/api"; import { getErrorDetail, getErrorMessage } from "api/errors"; import { disabledRefetchOptions } from "api/queries/util"; @@ -5,6 +6,7 @@ import type { Template } from "api/typesGenerated"; import { ErrorAlert } from "components/Alert/ErrorAlert"; import { Avatar } from "components/Avatar/Avatar"; import { AvatarData } from "components/Avatar/AvatarData"; +import { AvatarDataSkeleton } from "components/Avatar/AvatarDataSkeleton"; import { Button } from "components/Button/Button"; import { Form, FormFields, FormSection } from "components/Form/Form"; import { displayError } from "components/GlobalSnackbar/utils"; @@ -30,10 +32,14 @@ import { TableHeader, TableRow, } from "components/Table/Table"; +import { + TableLoaderSkeleton, + TableRowSkeleton, +} from "components/TableLoader/TableLoader"; import { useAuthenticated } from "hooks"; import { useExternalAuth } from "hooks/useExternalAuth"; -import { ExternalLinkIcon, RotateCcwIcon, SendIcon } from "lucide-react"; +import { 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"; @@ -50,17 +56,7 @@ import { type UserOption, UsersCombobox } from "./UsersCombobox"; type TasksFilter = { user: UserOption | undefined; }; - const TasksPage: FC = () => { - const { - data: templates, - error, - refetch, - } = useQuery({ - queryKey: ["templates", "ai"], - queryFn: data.fetchAITemplates, - ...disabledRefetchOptions, - }); const { user, permissions } = useAuthenticated(); const [filter, setFilter] = useState({ user: { @@ -70,92 +66,132 @@ const TasksPage: FC = () => { }, }); - let content: ReactNode = null; - - if (error) { - const message = getErrorMessage(error, "Error loading AI templates"); - const detail = getErrorDetail(error) ?? "Please, try again"; - - content = ( -
-
-

- {message} -

- {detail} - -
-
- ); - } else if (templates) { - content = - templates.length === 0 ? ( -
-
-

- No AI templates found -

- - Create an AI template to get started - - -
-
- ) : ( - <> - - {permissions.viewDeploymentConfig && ( - - )} - - - ); - } else { - content = ( -
-
- -

- Loading AI templates -

- - This might take a few minutes - -
-
- ); - } - return ( <> {pageTitle("AI Tasks")} - - - Read the docs - - } - > + Tasks Automate tasks with AI -
{content}
+
+ + +
); }; +const textareaPlaceholder = "Prompt your AI agent to start a task..."; + +const LoadingTemplatesPlaceholder: FC = () => { + return ( +
+
+ {/* Textarea skeleton */} + + + {/* Bottom controls skeleton */} +
+ + +
+
+
+ ); +}; + +const NoTemplatesPlaceholder: FC = () => { + return ( +
+
+

+ No Task templates found +

+ + Create a Task template to get started + +
+
+ ); +}; + +const ErrorContent: FC<{ + title: string; + detail: string; + onRetry: () => void; +}> = ({ title, detail, onRetry }) => { + return ( +
+
+

+ {title} +

+ {detail} + +
+
+ ); +}; + +const TaskFormSection: FC<{ + showFilter: boolean; + filter: TasksFilter; + onFilterChange: (filter: TasksFilter) => void; +}> = ({ showFilter, filter, onFilterChange }) => { + const { + data: templates, + error, + refetch, + } = useQuery({ + queryKey: ["templates", "ai"], + queryFn: data.fetchAITemplates, + ...disabledRefetchOptions, + }); + + if (error) { + return ( + refetch()} + /> + ); + } + if (templates === undefined) { + return ; + } + if (templates.length === 0) { + return ; + } + return ( + <> + + {showFilter && ( + + )} + + ); +}; + type CreateTaskMutationFnProps = { prompt: string; templateId: string }; type TaskFormProps = { @@ -211,7 +247,7 @@ const TaskForm: FC = ({ templates }) => { form.reset(); } catch (error) { const message = getErrorMessage(error, "Error creating task"); - const detail = getErrorDetail(error) ?? "Please, try again"; + const detail = getErrorDetail(error) ?? "Please try again"; displayError(message, detail); } }; @@ -231,7 +267,7 @@ const TaskForm: FC = ({ templates }) => { required id="prompt" name="prompt" - placeholder="Prompt your AI agent to start a task..." + 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`} /> @@ -322,18 +358,17 @@ const TasksFilter: FC = ({ filter, onFilterChange }) => { }; type TasksTableProps = { - templates: Template[]; filter: TasksFilter; }; -const TasksTable: FC = ({ templates, filter }) => { +const TasksTable: FC = ({ filter }) => { const { data: tasks, error, refetch, } = useQuery({ queryKey: ["tasks", filter], - queryFn: () => data.fetchTasks(templates, filter), + queryFn: () => data.fetchTasks(filter), refetchInterval: 10_000, }); @@ -341,7 +376,7 @@ const TasksTable: FC = ({ templates, filter }) => { if (error) { const message = getErrorMessage(error, "Error loading tasks"); - const detail = getErrorDetail(error) ?? "Please, try again"; + const detail = getErrorDetail(error) ?? "Please try again"; body = ( @@ -382,11 +417,6 @@ const TasksTable: FC = ({ templates, filter }) => { tasks.map(({ workspace, prompt }) => { const templateDisplayName = workspace.template_display_name ?? workspace.template_name; - const status = workspace.latest_app_status; - const agent = workspace.latest_build.resources - .flatMap((r) => r.agents) - .find((a) => a?.id === status?.agent_id); - const app = agent?.apps.find((a) => a.id === status?.app_id); return ( @@ -439,21 +469,19 @@ const TasksTable: FC = ({ templates, filter }) => { ); } else { body = ( - - -
-
- -

- Loading tasks -

- - This might take a few minutes - -
-
-
-
+ + + + + + + + + + + + + ); } @@ -472,69 +500,33 @@ const TasksTable: FC = ({ templates, filter }) => { }; export const data = { - // TODO: This function is currently inefficient because it fetches all templates - // and their parameters individually, resulting in many API calls and slow - // performance. After confirming the requirements, consider adding a backend - // endpoint that returns only AI templates (those with an "AI Prompt" parameter) - // in a single request. async fetchAITemplates() { - const templates = await API.getTemplates(); - const parameters = await Promise.all( - templates.map(async (template) => - API.getTemplateVersionRichParameters(template.active_version_id), - ), - ); - return templates.filter((_template, index) => { - return parameters[index].some((p) => p.name === AI_PROMPT_PARAMETER_NAME); - }); + return API.getTemplates({ q: "has-ai-task:true" }); }, - // TODO: This function is inefficient because it fetches workspaces for each - // template individually and its build parameters resulting in excessive API - // calls and slow performance. Consider implementing a backend endpoint that - // returns all AI-related workspaces in a single request to improve efficiency. - async fetchTasks(aiTemplates: Template[], filter: TasksFilter) { - const workspaces = await Promise.all( - aiTemplates.map((template) => { - const queryParts = [`template:${template.name}`]; - if (filter.user) { - queryParts.push(`owner:${filter.user.value}`); - } - - return API.getWorkspaces({ - q: queryParts.join(" "), - limit: 100, - }); - }), - ).then((results) => - results - .flatMap((r) => r.workspaces) - .toSorted((a, b) => { - return ( - new Date(b.created_at).getTime() - new Date(a.created_at).getTime() - ); - }), + async fetchTasks(filter: TasksFilter) { + let filterQuery = "has-ai-task:true"; + if (filter.user) { + filterQuery += ` owner:${filter.user.value}`; + } + const workspaces = await API.getWorkspaces({ + q: filterQuery, + }); + const prompts = await API.experimental.getAITasksPrompts( + workspaces.workspaces.map((workspace) => workspace.latest_build.id), ); - - return Promise.all( - workspaces.map(async (workspace) => { - const parameters = await API.getWorkspaceBuildParameters( - workspace.latest_build.id, - ); - const prompt = parameters.find( - (p) => p.name === AI_PROMPT_PARAMETER_NAME, - )?.value; - - if (!prompt) { - return; - } - - return { - workspace, - prompt, - } satisfies Task; - }), - ).then((tasks) => tasks.filter((t) => t !== undefined)); + return workspaces.workspaces.map((workspace) => { + let prompt = prompts.prompts[workspace.latest_build.id]; + if (prompt === undefined) { + prompt = "Unknown prompt"; + } else if (prompt === "") { + prompt = "Empty prompt"; + } + return { + workspace, + prompt, + } satisfies Task; + }); }, async createTask( 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