Task details page improvements (#1004)
This commit is contained in:
@@ -200,6 +200,7 @@ export const ActionTypes = {
|
|||||||
complete: "complete",
|
complete: "complete",
|
||||||
wait: "wait",
|
wait: "wait",
|
||||||
terminate: "terminate",
|
terminate: "terminate",
|
||||||
|
SolveCaptcha: "solve_captcha",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export type ActionType = (typeof ActionTypes)[keyof typeof ActionTypes];
|
export type ActionType = (typeof ActionTypes)[keyof typeof ActionTypes];
|
||||||
@@ -214,6 +215,7 @@ export const ReadableActionTypes: {
|
|||||||
complete: "Complete",
|
complete: "Complete",
|
||||||
wait: "Wait",
|
wait: "Wait",
|
||||||
terminate: "Terminate",
|
terminate: "Terminate",
|
||||||
|
solve_captcha: "Solve Captcha",
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Option = {
|
export type Option = {
|
||||||
|
|||||||
22
skyvern-frontend/src/routes/tasks/detail/ActionTypePill.tsx
Normal file
22
skyvern-frontend/src/routes/tasks/detail/ActionTypePill.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { ActionType, ReadableActionTypes } from "@/api/types";
|
||||||
|
import { CursorArrowIcon, InputIcon } from "@radix-ui/react-icons";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
actionType: ActionType;
|
||||||
|
};
|
||||||
|
|
||||||
|
const icons: Partial<Record<ActionType, React.ReactNode>> = {
|
||||||
|
click: <CursorArrowIcon className="h-4 w-4" />,
|
||||||
|
input_text: <InputIcon className="h-4 w-4" />,
|
||||||
|
};
|
||||||
|
|
||||||
|
function ActionTypePill({ actionType }: Props) {
|
||||||
|
return (
|
||||||
|
<div className="flex gap-1 rounded-sm bg-slate-elevation5 px-2 py-1">
|
||||||
|
{icons[actionType] ?? null}
|
||||||
|
<span className="text-xs">{ReadableActionTypes[actionType]}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { ActionTypePill };
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
import { getClient } from "@/api/AxiosClient";
|
import { getClient } from "@/api/AxiosClient";
|
||||||
import { Action, ActionTypes, ReadableActionTypes } from "@/api/types";
|
import { Action, ActionTypes } from "@/api/types";
|
||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { ScrollArea, ScrollAreaViewport } from "@/components/ui/scroll-area";
|
import { ScrollArea, ScrollAreaViewport } from "@/components/ui/scroll-area";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import {
|
import {
|
||||||
@@ -13,8 +11,6 @@ import {
|
|||||||
import { useCredentialGetter } from "@/hooks/useCredentialGetter";
|
import { useCredentialGetter } from "@/hooks/useCredentialGetter";
|
||||||
import { cn } from "@/util/utils";
|
import { cn } from "@/util/utils";
|
||||||
import {
|
import {
|
||||||
ArrowDownIcon,
|
|
||||||
ArrowUpIcon,
|
|
||||||
CheckCircledIcon,
|
CheckCircledIcon,
|
||||||
CrossCircledIcon,
|
CrossCircledIcon,
|
||||||
DotFilledIcon,
|
DotFilledIcon,
|
||||||
@@ -22,6 +18,7 @@ import {
|
|||||||
import { useQueryClient } from "@tanstack/react-query";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
import { ReactNode, useEffect, useRef } from "react";
|
import { ReactNode, useEffect, useRef } from "react";
|
||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
|
import { ActionTypePill } from "./ActionTypePill";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
data: Array<Action | null>;
|
data: Array<Action | null>;
|
||||||
@@ -30,15 +27,19 @@ type Props = {
|
|||||||
onActiveIndexChange: (index: number | "stream") => void;
|
onActiveIndexChange: (index: number | "stream") => void;
|
||||||
activeIndex: number | "stream";
|
activeIndex: number | "stream";
|
||||||
showStreamOption: boolean;
|
showStreamOption: boolean;
|
||||||
|
taskDetails: {
|
||||||
|
steps: number;
|
||||||
|
actions: number;
|
||||||
|
cost?: string;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
function ScrollableActionList({
|
function ScrollableActionList({
|
||||||
data,
|
data,
|
||||||
onNext,
|
|
||||||
onPrevious,
|
|
||||||
activeIndex,
|
activeIndex,
|
||||||
onActiveIndexChange,
|
onActiveIndexChange,
|
||||||
showStreamOption,
|
showStreamOption,
|
||||||
|
taskDetails,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const { taskId } = useParams();
|
const { taskId } = useParams();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
@@ -78,9 +79,11 @@ function ScrollableActionList({
|
|||||||
refs.current[actionIndex] = element;
|
refs.current[actionIndex] = element;
|
||||||
}}
|
}}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex cursor-pointer rounded-lg border p-4 shadow-md hover:border-slate-300",
|
"flex cursor-pointer rounded-lg border-2 bg-slate-elevation3 hover:border-slate-50",
|
||||||
{
|
{
|
||||||
"border-slate-300": selected,
|
"border-l-destructive": !action.success,
|
||||||
|
"border-l-success": action.success,
|
||||||
|
"border-slate-50": selected,
|
||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
onClick={() => onActiveIndexChange(actionIndex)}
|
onClick={() => onActiveIndexChange(actionIndex)}
|
||||||
@@ -96,36 +99,49 @@ function ScrollableActionList({
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex-1 space-y-2 p-2 pt-0">
|
<div className="flex-1 space-y-2 p-4 pl-5">
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span>#{i + 1}</span>
|
<span>#{i + 1}</span>
|
||||||
<Badge>{ReadableActionTypes[action.type]}</Badge>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
<ActionTypePill actionType={action.type} />
|
||||||
|
{action.success ? (
|
||||||
|
<div className="flex gap-1 rounded-sm bg-slate-elevation5 px-2 py-1">
|
||||||
|
<CheckCircledIcon className="h-4 w-4 text-success" />
|
||||||
|
<span className="text-xs">Success</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex gap-1 rounded-sm bg-slate-elevation5 px-2 py-1">
|
||||||
|
<CrossCircledIcon className="h-4 w-4 text-destructive" />
|
||||||
|
<span className="text-xs">Fail</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{typeof action.confidence === "number" && (
|
{typeof action.confidence === "number" && (
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger>
|
<TooltipTrigger>
|
||||||
<Badge variant="secondary">{action.confidence}</Badge>
|
<div className="flex gap-1 rounded-sm bg-slate-elevation5 px-2 py-1">
|
||||||
|
<span className="text-xs text-success">
|
||||||
|
{action.confidence}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs">CS</span>
|
||||||
|
</div>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>Confidence Score</TooltipContent>
|
<TooltipContent>Confidence Score</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
)}
|
)}
|
||||||
{action.success ? (
|
|
||||||
<CheckCircledIcon className="h-6 w-6 text-success" />
|
|
||||||
) : (
|
|
||||||
<CrossCircledIcon className="h-6 w-6 text-destructive" />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-sm">{action.reasoning}</div>
|
<div className="text-xs text-slate-400">{action.reasoning}</div>
|
||||||
{action.type === ActionTypes.InputText && (
|
{action.type === ActionTypes.InputText && (
|
||||||
<>
|
<>
|
||||||
<Separator className="block bg-slate-50" />
|
<Separator />
|
||||||
<div className="text-sm">Input: {action.input}</div>
|
<div className="text-xs text-slate-400">
|
||||||
|
Input: {action.input}
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -135,30 +151,23 @@ function ScrollableActionList({
|
|||||||
return elements;
|
return elements;
|
||||||
}
|
}
|
||||||
|
|
||||||
const actionIndex =
|
|
||||||
typeof activeIndex === "number" ? data.length - activeIndex - 1 : "stream";
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-[40rem] w-1/3 flex-col items-center rounded border">
|
<div className="h-[40rem] w-1/4 rounded border bg-slate-elevation1">
|
||||||
<div className="flex items-center gap-2 p-4 text-sm">
|
<div className="grid grid-cols-3 gap-2 p-4">
|
||||||
<Button
|
<div className="flex h-8 items-center justify-center rounded-sm bg-slate-700 px-3 text-xs text-gray-50">
|
||||||
size="icon"
|
Steps: {taskDetails.steps}
|
||||||
onClick={() => {
|
</div>
|
||||||
onPrevious();
|
<div className="flex h-8 items-center justify-center rounded-sm bg-slate-700 px-3 text-xs text-gray-50">
|
||||||
}}
|
Actions: {taskDetails.actions}
|
||||||
>
|
</div>
|
||||||
<ArrowUpIcon />
|
<div className="flex h-8 items-center justify-center rounded-sm bg-slate-700 px-3 text-xs text-gray-50">
|
||||||
</Button>
|
Cost: {taskDetails.cost}
|
||||||
{typeof actionIndex === "number" &&
|
</div>
|
||||||
`#${actionIndex + 1} of ${data.length} total actions`}
|
|
||||||
{activeIndex === "stream" && "Livestream"}
|
|
||||||
<Button size="icon" onClick={() => onNext()}>
|
|
||||||
<ArrowDownIcon />
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
<ScrollArea className="w-full">
|
<Separator />
|
||||||
<ScrollAreaViewport className="h-full w-full rounded-[inherit]">
|
<ScrollArea className="p-4">
|
||||||
<div className="w-full space-y-4 px-4 pb-4">
|
<ScrollAreaViewport className="max-h-[34rem]">
|
||||||
|
<div className="space-y-4">
|
||||||
{showStreamOption && (
|
{showStreamOption && (
|
||||||
<div
|
<div
|
||||||
key="stream"
|
key="stream"
|
||||||
@@ -166,15 +175,15 @@ function ScrollableActionList({
|
|||||||
refs.current[data.length] = element;
|
refs.current[data.length] = element;
|
||||||
}}
|
}}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex cursor-pointer rounded-lg border p-4 shadow-md hover:border-slate-300",
|
"flex cursor-pointer rounded-lg border-2 bg-slate-elevation3 p-4 hover:border-slate-50",
|
||||||
{
|
{
|
||||||
"border-slate-300": activeIndex === "stream",
|
"border-slate-50": activeIndex === "stream",
|
||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
onClick={() => onActiveIndexChange("stream")}
|
onClick={() => onActiveIndexChange("stream")}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 text-lg">
|
<div className="flex items-center gap-2">
|
||||||
<DotFilledIcon className="h-6 w-6 text-red-500" />
|
<DotFilledIcon className="h-6 w-6 text-destructive" />
|
||||||
Live
|
Live
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -17,6 +17,12 @@ import { toast } from "@/components/ui/use-toast";
|
|||||||
import { envCredential } from "@/util/env";
|
import { envCredential } from "@/util/env";
|
||||||
import { statusIsNotFinalized, statusIsRunningOrQueued } from "../types";
|
import { statusIsNotFinalized, statusIsRunningOrQueued } from "../types";
|
||||||
import { ZoomableImage } from "@/components/ZoomableImage";
|
import { ZoomableImage } from "@/components/ZoomableImage";
|
||||||
|
import { useCostCalculator } from "@/hooks/useCostCalculator";
|
||||||
|
|
||||||
|
const formatter = Intl.NumberFormat("en-US", {
|
||||||
|
style: "currency",
|
||||||
|
currency: "USD",
|
||||||
|
});
|
||||||
|
|
||||||
type StreamMessage = {
|
type StreamMessage = {
|
||||||
task_id: string;
|
task_id: string;
|
||||||
@@ -45,6 +51,7 @@ function TaskActions() {
|
|||||||
const credentialGetter = useCredentialGetter();
|
const credentialGetter = useCredentialGetter();
|
||||||
const [streamImgSrc, setStreamImgSrc] = useState<string>("");
|
const [streamImgSrc, setStreamImgSrc] = useState<string>("");
|
||||||
const [selectedAction, setSelectedAction] = useState<number | "stream">(0);
|
const [selectedAction, setSelectedAction] = useState<number | "stream">(0);
|
||||||
|
const costCalculator = useCostCalculator();
|
||||||
|
|
||||||
const { data: task, isLoading: taskIsLoading } = useQuery<TaskApiResponse>({
|
const { data: task, isLoading: taskIsLoading } = useQuery<TaskApiResponse>({
|
||||||
queryKey: ["task", taskId],
|
queryKey: ["task", taskId],
|
||||||
@@ -202,7 +209,7 @@ function TaskActions() {
|
|||||||
function getStream() {
|
function getStream() {
|
||||||
if (task?.status === Status.Created) {
|
if (task?.status === Status.Created) {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full w-full flex-col items-center justify-center gap-4 bg-slate-900 text-lg">
|
<div className="flex h-full w-full flex-col items-center justify-center gap-4 bg-slate-elevation1 text-lg">
|
||||||
<span>Task has been created.</span>
|
<span>Task has been created.</span>
|
||||||
<span>Stream will start when the task is running.</span>
|
<span>Stream will start when the task is running.</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -210,7 +217,7 @@ function TaskActions() {
|
|||||||
}
|
}
|
||||||
if (task?.status === Status.Queued) {
|
if (task?.status === Status.Queued) {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full w-full flex-col items-center justify-center gap-4 bg-slate-900 text-lg">
|
<div className="flex h-full w-full flex-col items-center justify-center gap-4 bg-slate-elevation1 text-lg">
|
||||||
<span>Your task is queued. Typical queue time is 1-2 minutes.</span>
|
<span>Your task is queued. Typical queue time is 1-2 minutes.</span>
|
||||||
<span>Stream will start when the task is running.</span>
|
<span>Stream will start when the task is running.</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -219,7 +226,7 @@ function TaskActions() {
|
|||||||
|
|
||||||
if (task?.status === Status.Running && streamImgSrc.length === 0) {
|
if (task?.status === Status.Running && streamImgSrc.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full w-full items-center justify-center bg-slate-900 text-lg">
|
<div className="flex h-full w-full items-center justify-center bg-slate-elevation1 text-lg">
|
||||||
Starting the stream...
|
Starting the stream...
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -235,9 +242,12 @@ function TaskActions() {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const showCost = typeof costCalculator === "function";
|
||||||
|
const notRunningSteps = steps?.filter((step) => step.status !== "running");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<div className="w-2/3 rounded border">
|
<div className="w-3/4 rounded border">
|
||||||
<div className="h-full w-full p-4">
|
<div className="h-full w-full p-4">
|
||||||
{selectedAction === "stream" ? getStream() : null}
|
{selectedAction === "stream" ? getStream() : null}
|
||||||
{typeof selectedAction === "number" && activeAction ? (
|
{typeof selectedAction === "number" && activeAction ? (
|
||||||
@@ -253,6 +263,13 @@ function TaskActions() {
|
|||||||
data={actions ?? []}
|
data={actions ?? []}
|
||||||
onActiveIndexChange={setSelectedAction}
|
onActiveIndexChange={setSelectedAction}
|
||||||
showStreamOption={Boolean(taskIsNotFinalized)}
|
showStreamOption={Boolean(taskIsNotFinalized)}
|
||||||
|
taskDetails={{
|
||||||
|
steps: steps?.length ?? 0,
|
||||||
|
actions: actions?.length ?? 0,
|
||||||
|
cost: showCost
|
||||||
|
? formatter.format(costCalculator(notRunningSteps ?? []))
|
||||||
|
: undefined,
|
||||||
|
}}
|
||||||
onNext={() => {
|
onNext={() => {
|
||||||
if (!actions) {
|
if (!actions) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -17,14 +17,12 @@ import {
|
|||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
|
||||||
import { toast } from "@/components/ui/use-toast";
|
import { toast } from "@/components/ui/use-toast";
|
||||||
import { useCredentialGetter } from "@/hooks/useCredentialGetter";
|
import { useCredentialGetter } from "@/hooks/useCredentialGetter";
|
||||||
import { cn } from "@/util/utils";
|
import { cn } from "@/util/utils";
|
||||||
import { CopyIcon, PlayIcon, ReloadIcon } from "@radix-ui/react-icons";
|
import { CopyIcon, PlayIcon, ReloadIcon } from "@radix-ui/react-icons";
|
||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { Link, NavLink, Outlet, useParams } from "react-router-dom";
|
import { Link, NavLink, Outlet, useParams } from "react-router-dom";
|
||||||
import { TaskInfo } from "./TaskInfo";
|
|
||||||
import { useTaskQuery } from "./hooks/useTaskQuery";
|
import { useTaskQuery } from "./hooks/useTaskQuery";
|
||||||
import { taskIsFinalized } from "@/api/utils";
|
import { taskIsFinalized } from "@/api/utils";
|
||||||
import fetchToCurl from "fetch-to-curl";
|
import fetchToCurl from "fetch-to-curl";
|
||||||
@@ -32,6 +30,8 @@ import { apiBaseUrl } from "@/util/env";
|
|||||||
import { useApiCredential } from "@/hooks/useApiCredential";
|
import { useApiCredential } from "@/hooks/useApiCredential";
|
||||||
import { copyText } from "@/util/copyText";
|
import { copyText } from "@/util/copyText";
|
||||||
import { WorkflowApiResponse } from "@/routes/workflows/types/workflowTypes";
|
import { WorkflowApiResponse } from "@/routes/workflows/types/workflowTypes";
|
||||||
|
import { StatusBadge } from "@/components/StatusBadge";
|
||||||
|
import { CodeEditor } from "@/routes/workflows/components/CodeEditor";
|
||||||
|
|
||||||
function createTaskRequestObject(values: TaskApiResponse) {
|
function createTaskRequestObject(values: TaskApiResponse) {
|
||||||
return {
|
return {
|
||||||
@@ -119,12 +119,16 @@ function TaskDetails() {
|
|||||||
const showExtractedInformation =
|
const showExtractedInformation =
|
||||||
task?.status === Status.Completed && task.extracted_information !== null;
|
task?.status === Status.Completed && task.extracted_information !== null;
|
||||||
const extractedInformation = showExtractedInformation ? (
|
const extractedInformation = showExtractedInformation ? (
|
||||||
<div className="flex items-center">
|
<div className="space-y-1">
|
||||||
<Label className="w-32 shrink-0 text-lg">Extracted Information</Label>
|
<Label className="text-lg">Extracted Information</Label>
|
||||||
<Textarea
|
<CodeEditor
|
||||||
rows={5}
|
language="json"
|
||||||
value={JSON.stringify(task.extracted_information, null, 2)}
|
value={JSON.stringify(task.extracted_information, null, 2)}
|
||||||
readOnly
|
disabled
|
||||||
|
fontSize={12}
|
||||||
|
minHeight={"96px"}
|
||||||
|
maxHeight={"500px"}
|
||||||
|
className="w-full"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : null;
|
) : null;
|
||||||
@@ -139,12 +143,16 @@ function TaskDetails() {
|
|||||||
task?.status === Status.Terminated ||
|
task?.status === Status.Terminated ||
|
||||||
task?.status === Status.TimedOut;
|
task?.status === Status.TimedOut;
|
||||||
const failureReason = showFailureReason ? (
|
const failureReason = showFailureReason ? (
|
||||||
<div className="flex items-center">
|
<div className="space-y-1">
|
||||||
<Label className="w-32 shrink-0 text-lg">Failure Reason</Label>
|
<Label className="text-lg">Failure Reason</Label>
|
||||||
<Textarea
|
<CodeEditor
|
||||||
rows={5}
|
language="json"
|
||||||
value={JSON.stringify(task.failure_reason, null, 2)}
|
value={JSON.stringify(task.failure_reason, null, 2)}
|
||||||
readOnly
|
disabled
|
||||||
|
fontSize={12}
|
||||||
|
minHeight={"96px"}
|
||||||
|
maxHeight={"500px"}
|
||||||
|
className="w-full"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : null;
|
) : null;
|
||||||
@@ -153,9 +161,13 @@ function TaskDetails() {
|
|||||||
<div className="flex flex-col gap-8">
|
<div className="flex flex-col gap-8">
|
||||||
<header className="space-y-3">
|
<header className="space-y-3">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-5">
|
||||||
<span className="text-3xl">{taskId}</span>
|
<span className="text-3xl">{taskId}</span>
|
||||||
{taskId && <TaskInfo id={taskId} />}
|
{taskIsLoading ? (
|
||||||
|
<Skeleton className="h-8 w-32" />
|
||||||
|
) : (
|
||||||
|
task && <StatusBadge status={task.status} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Button
|
<Button
|
||||||
@@ -245,10 +257,7 @@ function TaskDetails() {
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
{taskIsLoading ? (
|
{taskIsLoading ? (
|
||||||
<div className="flex items-center gap-2">
|
<Skeleton className="h-32 w-full" />
|
||||||
<Skeleton className="h-32 w-32" />
|
|
||||||
<Skeleton className="h-32 w-full" />
|
|
||||||
</div>
|
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{extractedInformation}
|
{extractedInformation}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ function CodeEditor({
|
|||||||
maxHeight,
|
maxHeight,
|
||||||
language,
|
language,
|
||||||
className,
|
className,
|
||||||
|
disabled,
|
||||||
fontSize = 8,
|
fontSize = 8,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const extensions =
|
const extensions =
|
||||||
@@ -36,6 +37,7 @@ function CodeEditor({
|
|||||||
theme={tokyoNightStorm}
|
theme={tokyoNightStorm}
|
||||||
minHeight={minHeight}
|
minHeight={minHeight}
|
||||||
maxHeight={maxHeight}
|
maxHeight={maxHeight}
|
||||||
|
readOnly={disabled}
|
||||||
className={cn("cursor-auto", className)}
|
className={cn("cursor-auto", className)}
|
||||||
style={{
|
style={{
|
||||||
fontSize: fontSize,
|
fontSize: fontSize,
|
||||||
|
|||||||
Reference in New Issue
Block a user