From 3e365de69c72b035550a7e4fcf2a6bbc10b509e6 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Wed, 28 May 2025 13:31:15 +0000 Subject: [PATCH 1/4] Add base structure --- site/src/components/Table/Table.tsx | 28 +- site/src/modules/apps/AppStatusIcon.tsx | 47 ++++ site/src/modules/tasks/tasks.ts | 8 + site/src/pages/TaskPage/TaskPage.tsx | 278 +++++++++++++++++++ site/src/pages/TasksPage/TasksPage.tsx | 42 ++- site/src/pages/TerminalPage/TerminalPage.tsx | 4 +- site/src/pages/WorkspacePage/AppStatuses.tsx | 45 +-- site/src/router.tsx | 2 + 8 files changed, 398 insertions(+), 56 deletions(-) create mode 100644 site/src/modules/apps/AppStatusIcon.tsx create mode 100644 site/src/modules/tasks/tasks.ts create mode 100644 site/src/pages/TaskPage/TaskPage.tsx diff --git a/site/src/components/Table/Table.tsx b/site/src/components/Table/Table.tsx index c20fe99428e09..a418c630868c9 100644 --- a/site/src/components/Table/Table.tsx +++ b/site/src/components/Table/Table.tsx @@ -3,6 +3,7 @@ * @see {@link https://ui.shadcn.com/docs/components/table} */ +import { cva, type VariantProps } from "class-variance-authority"; import * as React from "react"; import { cn } from "utils/cn"; @@ -60,15 +61,38 @@ const TableFooter = React.forwardRef< /> )); +const tableRowVariants = cva( + [ + "border-0 border-b border-solid border-border transition-colors", + "data-[state=selected]:bg-muted", + ], + { + variants: { + hover: { + false: null, + true: cn([ + "cursor-pointer hover:outline focus:outline outline-1 -outline-offset-1 outline-border-hover", + "first:rounded-t-md last:rounded-b-md", + ]), + }, + }, + defaultVariants: { + hover: false, + }, + }, +); + export const TableRow = React.forwardRef< HTMLTableRowElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( + React.HTMLAttributes & + VariantProps +>(({ className, hover, ...props }, ref) => ( = ({ + status, + latest, + className: customClassName, +}) => { + const className = cn(["size-4 shrink-0", customClassName]); + + switch (status.state) { + case "complete": + return ( + + ); + case "failure": + return ( + + ); + case "working": + return latest ? ( + + ) : ( + + ); + default: + return ( + + ); + } +}; diff --git a/site/src/modules/tasks/tasks.ts b/site/src/modules/tasks/tasks.ts new file mode 100644 index 0000000000000..c48f5ec1c3f22 --- /dev/null +++ b/site/src/modules/tasks/tasks.ts @@ -0,0 +1,8 @@ +import type { Workspace } from "api/typesGenerated"; + +export const AI_PROMPT_PARAMETER_NAME = "AI Prompt"; + +export type Task = { + workspace: Workspace; + prompt: string; +}; diff --git a/site/src/pages/TaskPage/TaskPage.tsx b/site/src/pages/TaskPage/TaskPage.tsx new file mode 100644 index 0000000000000..a43f0fbc55066 --- /dev/null +++ b/site/src/pages/TaskPage/TaskPage.tsx @@ -0,0 +1,278 @@ +import { API } from "api/api"; +import type { WorkspaceApp } from "api/typesGenerated"; +import { Button } from "components/Button/Button"; +import { ExternalImage } from "components/ExternalImage/ExternalImage"; +import { Loader } from "components/Loader/Loader"; +import { ScrollArea } from "components/ScrollArea/ScrollArea"; +import { ArrowLeftIcon, CircleCheckIcon, LayoutGridIcon } from "lucide-react"; +import { getAppHref } from "modules/apps/apps"; +import { useAppLink } from "modules/apps/useAppLink"; +import { AI_PROMPT_PARAMETER_NAME, type Task } from "modules/tasks/tasks"; +import { useState, type FC } from "react"; +import { Helmet } from "react-helmet-async"; +import { useQuery } from "react-query"; +import { useParams } from "react-router-dom"; +import { cn } from "utils/cn"; +import { pageTitle } from "utils/page"; +import { timeFrom } from "utils/time"; +import { Link as RouterLink } from "react-router-dom"; +import { useProxy } from "contexts/ProxyContext"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "components/Tooltip/Tooltip"; +import { AppStatusIcon } from "modules/apps/AppStatusIcon"; + +const TaskPage = () => { + const { workspace: workspaceName, username } = useParams() as { + workspace: string; + username: string; + }; + const { data: task } = useQuery({ + queryKey: ["tasks", username, workspaceName], + queryFn: () => data.fetchTask(username, workspaceName), + refetchInterval: 5_000, + }); + + if (!task) { + return ( + <> + + {pageTitle("Loading task")} + + + + ); + } + + const statuses = task.workspace.latest_build.resources + .flatMap((r) => r.agents) + .flatMap((a) => a?.apps) + .flatMap((a) => a?.statuses) + .filter((s) => !!s) + .sort( + (a, b) => + new Date(b.created_at).getTime() - new Date(a.created_at).getTime(), + ); + + return ( + <> + + {pageTitle(task.prompt)} + + +
+
+
+ + + + + + Back to tasks + + + +
+

{task.prompt}

+ + Created by {task.workspace.owner_name}{" "} + {timeFrom(new Date(task.workspace.created_at))} + +
+
+
+ +
+ + + +
+
+ + ); +}; + +export default TaskPage; + +type TaskAppsProps = { + task: Task; +}; + +const TaskApps: FC = ({ task }) => { + const agents = task.workspace.latest_build.resources + .flatMap((r) => r.agents) + .filter((a) => !!a); + + const apps = agents.flatMap((a) => a?.apps).filter((a) => !!a); + + const [activeAppId, setActiveAppId] = useState(() => { + const appId = task.workspace.latest_app_status?.app_id; + if (!appId) { + throw new Error("No active app found in task"); + } + return appId; + }); + + const activeApp = apps.find((app) => app.id === activeAppId); + if (!activeApp) { + throw new Error(`Active app with ID ${activeAppId} not found in task`); + } + + const agent = agents.find((a) => + a.apps.some((app) => app.id === activeAppId), + ); + if (!agent) { + throw new Error(`Agent for app ${activeAppId} not found in task workspace`); + } + + const { proxy } = useProxy(); + const [iframeSrc, setIframeSrc] = useState(() => { + const src = getAppHref(activeApp, { + agent, + workspace: task.workspace, + path: proxy.preferredPathAppURL, + host: proxy.preferredWildcardHostname, + }); + return src; + }); + + return ( +
+
+ {apps.map((app) => ( + { + if (app.external) { + return; + } + + e.preventDefault(); + setActiveAppId(app.id); + setIframeSrc(e.currentTarget.href); + }} + /> + ))} +
+ +
+