mirror of
https://github.com/Skyvern-AI/skyvern.git
synced 2025-09-02 02:30:07 +00:00
Changes for signed URL artifacts (#197)
This commit is contained in:
parent
b6f3bc2c41
commit
eb668baba8
6 changed files with 106 additions and 70 deletions
|
@ -31,6 +31,7 @@ export type ArtifactApiResponse = {
|
||||||
step_id: string;
|
step_id: string;
|
||||||
artifact_type: ArtifactType;
|
artifact_type: ArtifactType;
|
||||||
uri: string;
|
uri: string;
|
||||||
|
signed_url?: string | null;
|
||||||
organization_id: string;
|
organization_id: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,25 +1,32 @@
|
||||||
import { artifactApiClient } from "@/api/AxiosClient";
|
import { artifactApiClient } from "@/api/AxiosClient";
|
||||||
|
import { ArtifactApiResponse } from "@/api/types";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
uri: string;
|
artifact: ArtifactApiResponse;
|
||||||
};
|
};
|
||||||
|
|
||||||
function JSONArtifact({ uri }: Props) {
|
function JSONArtifact({ artifact }: Props) {
|
||||||
const { data, isFetching, isError, error } = useQuery<
|
const { data, isFetching, isError, error } = useQuery<
|
||||||
Record<string, unknown>
|
Record<string, unknown>
|
||||||
>({
|
>({
|
||||||
queryKey: ["artifact", uri],
|
queryKey: ["artifact", artifact.artifact_id],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
return artifactApiClient
|
if (artifact.uri.startsWith("file://")) {
|
||||||
.get(`/artifact/json`, {
|
return artifactApiClient
|
||||||
params: {
|
.get(`/artifact/json`, {
|
||||||
path: uri.slice(7),
|
params: {
|
||||||
},
|
path: artifact.uri.slice(7),
|
||||||
})
|
},
|
||||||
.then((response) => response.data);
|
})
|
||||||
|
.then((response) => response.data);
|
||||||
|
}
|
||||||
|
if (artifact.uri.startsWith("s3://") && artifact.signed_url) {
|
||||||
|
return axios.get(artifact.signed_url).then((response) => response.data);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -9,11 +9,11 @@ import { Label } from "@/components/ui/label";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
import { artifactApiBaseUrl } from "@/util/env";
|
|
||||||
import { ZoomableImage } from "@/components/ZoomableImage";
|
import { ZoomableImage } from "@/components/ZoomableImage";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import { JSONArtifact } from "./JSONArtifact";
|
import { JSONArtifact } from "./JSONArtifact";
|
||||||
import { TextArtifact } from "./TextArtifact";
|
import { TextArtifact } from "./TextArtifact";
|
||||||
|
import { getImageURL } from "./artifactUtils";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -40,46 +40,42 @@ function StepArtifacts({ id, stepProps }: Props) {
|
||||||
return <div>Error: {error?.message}</div>;
|
return <div>Error: {error?.message}</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const llmScreenshotUris = artifacts
|
const llmScreenshots = artifacts?.filter(
|
||||||
?.filter(
|
(artifact) => artifact.artifact_type === ArtifactType.LLMScreenshot,
|
||||||
(artifact) => artifact.artifact_type === ArtifactType.LLMScreenshot,
|
);
|
||||||
)
|
|
||||||
.map((artifact) => artifact.uri);
|
|
||||||
|
|
||||||
const actionScreenshotUris = artifacts
|
const actionScreenshots = artifacts?.filter(
|
||||||
?.filter(
|
(artifact) => artifact.artifact_type === ArtifactType.ActionScreenshot,
|
||||||
(artifact) => artifact.artifact_type === ArtifactType.ActionScreenshot,
|
);
|
||||||
)
|
|
||||||
.map((artifact) => artifact.uri);
|
|
||||||
|
|
||||||
const visibleElementsTreeUri = artifacts?.find(
|
const visibleElementsTree = artifacts?.find(
|
||||||
(artifact) => artifact.artifact_type === ArtifactType.VisibleElementsTree,
|
(artifact) => artifact.artifact_type === ArtifactType.VisibleElementsTree,
|
||||||
)?.uri;
|
);
|
||||||
|
|
||||||
const visibleElementsTreeTrimmedUri = artifacts?.find(
|
const visibleElementsTreeTrimmed = artifacts?.find(
|
||||||
(artifact) =>
|
(artifact) =>
|
||||||
artifact.artifact_type === ArtifactType.VisibleElementsTreeTrimmed,
|
artifact.artifact_type === ArtifactType.VisibleElementsTreeTrimmed,
|
||||||
)?.uri;
|
);
|
||||||
|
|
||||||
const llmPromptUri = artifacts?.find(
|
const llmPrompt = artifacts?.find(
|
||||||
(artifact) => artifact.artifact_type === ArtifactType.LLMPrompt,
|
(artifact) => artifact.artifact_type === ArtifactType.LLMPrompt,
|
||||||
)?.uri;
|
);
|
||||||
|
|
||||||
const llmRequestUri = artifacts?.find(
|
const llmRequest = artifacts?.find(
|
||||||
(artifact) => artifact.artifact_type === ArtifactType.LLMRequest,
|
(artifact) => artifact.artifact_type === ArtifactType.LLMRequest,
|
||||||
)?.uri;
|
);
|
||||||
|
|
||||||
const llmResponseRawUri = artifacts?.find(
|
const llmResponseRaw = artifacts?.find(
|
||||||
(artifact) => artifact.artifact_type === ArtifactType.LLMResponseRaw,
|
(artifact) => artifact.artifact_type === ArtifactType.LLMResponseRaw,
|
||||||
)?.uri;
|
);
|
||||||
|
|
||||||
const llmResponseParsedUri = artifacts?.find(
|
const llmResponseParsed = artifacts?.find(
|
||||||
(artifact) => artifact.artifact_type === ArtifactType.LLMResponseParsed,
|
(artifact) => artifact.artifact_type === ArtifactType.LLMResponseParsed,
|
||||||
)?.uri;
|
);
|
||||||
|
|
||||||
const htmlRawUri = artifacts?.find(
|
const htmlRaw = artifacts?.find(
|
||||||
(artifact) => artifact.artifact_type === ArtifactType.HTMLScrape,
|
(artifact) => artifact.artifact_type === ArtifactType.HTMLScrape,
|
||||||
)?.uri;
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tabs defaultValue="info" className="w-full">
|
<Tabs defaultValue="info" className="w-full">
|
||||||
|
@ -128,12 +124,12 @@ function StepArtifacts({ id, stepProps }: Props) {
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="screenshot_llm">
|
<TabsContent value="screenshot_llm">
|
||||||
{llmScreenshotUris && llmScreenshotUris.length > 0 ? (
|
{llmScreenshots && llmScreenshots.length > 0 ? (
|
||||||
<div className="grid grid-cols-3 gap-4 p-4">
|
<div className="grid grid-cols-3 gap-4 p-4">
|
||||||
{llmScreenshotUris.map((uri, index) => (
|
{llmScreenshots.map((artifact, index) => (
|
||||||
<ZoomableImage
|
<ZoomableImage
|
||||||
key={index}
|
key={index}
|
||||||
src={`${artifactApiBaseUrl}/artifact/image?path=${uri.slice(7)}`}
|
src={getImageURL(artifact)}
|
||||||
className="object-cover w-full h-full"
|
className="object-cover w-full h-full"
|
||||||
alt="action-screenshot"
|
alt="action-screenshot"
|
||||||
/>
|
/>
|
||||||
|
@ -150,12 +146,12 @@ function StepArtifacts({ id, stepProps }: Props) {
|
||||||
)}
|
)}
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="screenshot_action">
|
<TabsContent value="screenshot_action">
|
||||||
{actionScreenshotUris && actionScreenshotUris.length > 0 ? (
|
{actionScreenshots && actionScreenshots.length > 0 ? (
|
||||||
<div className="grid grid-cols-3 gap-4 p-4">
|
<div className="grid grid-cols-3 gap-4 p-4">
|
||||||
{actionScreenshotUris.map((uri, index) => (
|
{actionScreenshots.map((artifact, index) => (
|
||||||
<ZoomableImage
|
<ZoomableImage
|
||||||
key={index}
|
key={index}
|
||||||
src={`${artifactApiBaseUrl}/artifact/image?path=${uri.slice(7)}`}
|
src={getImageURL(artifact)}
|
||||||
className="object-cover w-full h-full"
|
className="object-cover w-full h-full"
|
||||||
alt="action-screenshot"
|
alt="action-screenshot"
|
||||||
/>
|
/>
|
||||||
|
@ -172,31 +168,31 @@ function StepArtifacts({ id, stepProps }: Props) {
|
||||||
)}
|
)}
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="element_tree">
|
<TabsContent value="element_tree">
|
||||||
{visibleElementsTreeUri ? (
|
{visibleElementsTree ? (
|
||||||
<JSONArtifact uri={visibleElementsTreeUri} />
|
<JSONArtifact artifact={visibleElementsTree} />
|
||||||
) : null}
|
) : null}
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="element_tree_trimmed">
|
<TabsContent value="element_tree_trimmed">
|
||||||
{visibleElementsTreeTrimmedUri ? (
|
{visibleElementsTreeTrimmed ? (
|
||||||
<JSONArtifact uri={visibleElementsTreeTrimmedUri} />
|
<JSONArtifact artifact={visibleElementsTreeTrimmed} />
|
||||||
) : null}
|
) : null}
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="llm_prompt">
|
<TabsContent value="llm_prompt">
|
||||||
{llmPromptUri ? <TextArtifact uri={llmPromptUri} /> : null}
|
{llmPrompt ? <TextArtifact artifact={llmPrompt} /> : null}
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="llm_request">
|
<TabsContent value="llm_request">
|
||||||
{llmRequestUri ? <JSONArtifact uri={llmRequestUri} /> : null}
|
{llmRequest ? <JSONArtifact artifact={llmRequest} /> : null}
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="llm_response_raw">
|
<TabsContent value="llm_response_raw">
|
||||||
{llmResponseRawUri ? <JSONArtifact uri={llmResponseRawUri} /> : null}
|
{llmResponseRaw ? <JSONArtifact artifact={llmResponseRaw} /> : null}
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="llm_response_parsed">
|
<TabsContent value="llm_response_parsed">
|
||||||
{llmResponseParsedUri ? (
|
{llmResponseParsed ? (
|
||||||
<JSONArtifact uri={llmResponseParsedUri} />
|
<JSONArtifact artifact={llmResponseParsed} />
|
||||||
) : null}
|
) : null}
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="html_raw">
|
<TabsContent value="html_raw">
|
||||||
{htmlRawUri ? <TextArtifact uri={htmlRawUri} /> : null}
|
{htmlRaw ? <TextArtifact artifact={htmlRaw} /> : null}
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
);
|
);
|
||||||
|
|
|
@ -11,13 +11,13 @@ import {
|
||||||
AccordionTrigger,
|
AccordionTrigger,
|
||||||
} from "@/components/ui/accordion";
|
} from "@/components/ui/accordion";
|
||||||
import { StatusBadge } from "@/components/StatusBadge";
|
import { StatusBadge } from "@/components/StatusBadge";
|
||||||
import { artifactApiBaseUrl } from "@/util/env";
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { ReloadIcon } from "@radix-ui/react-icons";
|
import { ReloadIcon } from "@radix-ui/react-icons";
|
||||||
import { basicTimeFormat } from "@/util/timeFormat";
|
import { basicTimeFormat } from "@/util/timeFormat";
|
||||||
import { StepArtifactsLayout } from "./StepArtifactsLayout";
|
import { StepArtifactsLayout } from "./StepArtifactsLayout";
|
||||||
import Zoom from "react-medium-image-zoom";
|
import Zoom from "react-medium-image-zoom";
|
||||||
import { AspectRatio } from "@/components/ui/aspect-ratio";
|
import { AspectRatio } from "@/components/ui/aspect-ratio";
|
||||||
|
import { getRecordingURL, getScreenshotURL } from "./artifactUtils";
|
||||||
|
|
||||||
function TaskDetails() {
|
function TaskDetails() {
|
||||||
const { taskId } = useParams();
|
const { taskId } = useParams();
|
||||||
|
@ -63,10 +63,7 @@ function TaskDetails() {
|
||||||
{task.recording_url ? (
|
{task.recording_url ? (
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<Label className="w-32">Recording</Label>
|
<Label className="w-32">Recording</Label>
|
||||||
<video
|
<video src={getRecordingURL(task)} controls />
|
||||||
src={`${artifactApiBaseUrl}/artifact/recording?path=${task.recording_url.slice(7)}`}
|
|
||||||
controls
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
|
@ -142,10 +139,7 @@ function TaskDetails() {
|
||||||
{task.screenshot_url ? (
|
{task.screenshot_url ? (
|
||||||
<Zoom zoomMargin={16}>
|
<Zoom zoomMargin={16}>
|
||||||
<AspectRatio ratio={16 / 9}>
|
<AspectRatio ratio={16 / 9}>
|
||||||
<img
|
<img src={getScreenshotURL(task)} alt="screenshot" />
|
||||||
src={`${artifactApiBaseUrl}/artifact/image?path=${task.screenshot_url.slice(7)}`}
|
|
||||||
alt="screenshot"
|
|
||||||
/>
|
|
||||||
</AspectRatio>
|
</AspectRatio>
|
||||||
</Zoom>
|
</Zoom>
|
||||||
) : (
|
) : (
|
||||||
|
|
|
@ -1,23 +1,30 @@
|
||||||
import { artifactApiClient } from "@/api/AxiosClient";
|
import { artifactApiClient } from "@/api/AxiosClient";
|
||||||
|
import { ArtifactApiResponse } from "@/api/types";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
uri: string;
|
artifact: ArtifactApiResponse;
|
||||||
};
|
};
|
||||||
|
|
||||||
function TextArtifact({ uri }: Props) {
|
function TextArtifact({ artifact }: Props) {
|
||||||
const { data, isFetching, isError, error } = useQuery<string>({
|
const { data, isFetching, isError, error } = useQuery<string>({
|
||||||
queryKey: ["artifact", uri],
|
queryKey: ["artifact", artifact.artifact_id],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
return artifactApiClient
|
if (artifact.uri.startsWith("file://")) {
|
||||||
.get(`/artifact/text`, {
|
return artifactApiClient
|
||||||
params: {
|
.get(`/artifact/text`, {
|
||||||
path: uri.slice(7),
|
params: {
|
||||||
},
|
path: artifact.uri.slice(7),
|
||||||
})
|
},
|
||||||
.then((response) => response.data);
|
})
|
||||||
|
.then((response) => response.data);
|
||||||
|
}
|
||||||
|
if (artifact.uri.startsWith("s3://") && artifact.signed_url) {
|
||||||
|
return axios.get(artifact.signed_url).then((response) => response.data);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
31
skyvern-frontend/src/routes/tasks/detail/artifactUtils.ts
Normal file
31
skyvern-frontend/src/routes/tasks/detail/artifactUtils.ts
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
import { ArtifactApiResponse, TaskApiResponse } from "@/api/types";
|
||||||
|
import { artifactApiBaseUrl } from "@/util/env";
|
||||||
|
|
||||||
|
export function getImageURL(artifact: ArtifactApiResponse): string {
|
||||||
|
if (artifact.uri.startsWith("file://")) {
|
||||||
|
return `${artifactApiBaseUrl}/artifact/image?path=${artifact.uri.slice(7)}`;
|
||||||
|
} else if (artifact.uri.startsWith("s3://") && artifact.signed_url) {
|
||||||
|
return artifact.signed_url;
|
||||||
|
}
|
||||||
|
return artifact.uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getScreenshotURL(task: TaskApiResponse) {
|
||||||
|
if (!task.screenshot_url) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (task.screenshot_url?.startsWith("file://")) {
|
||||||
|
return `${artifactApiBaseUrl}/artifact/image?path=${task.screenshot_url.slice(7)}`;
|
||||||
|
}
|
||||||
|
return task.screenshot_url;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getRecordingURL(task: TaskApiResponse) {
|
||||||
|
if (!task.recording_url) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (task.recording_url?.startsWith("file://")) {
|
||||||
|
return `${artifactApiBaseUrl}/artifact/recording?path=${task.recording_url.slice(7)}`;
|
||||||
|
}
|
||||||
|
return task.recording_url;
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue