diff --git a/skyvern-frontend/server.js b/skyvern-frontend/artifactServer.js similarity index 54% rename from skyvern-frontend/server.js rename to skyvern-frontend/artifactServer.js index c2337779..5fa0d545 100644 --- a/skyvern-frontend/server.js +++ b/skyvern-frontend/artifactServer.js @@ -1,9 +1,12 @@ import express from "express"; import fs from "fs"; +import cors from "cors"; const app = express(); -app.get("/artifact", (req, res) => { +app.use(cors()); + +app.get("/artifact/recording", (req, res) => { const range = req.headers.range; const path = req.query.path; const videoSize = fs.statSync(path).size; @@ -25,4 +28,26 @@ app.get("/artifact", (req, res) => { stream.pipe(res); }); +app.get("/artifact/image", (req, res) => { + const path = req.query.path; + res.sendFile(path); +}); + +app.get("/artifact/json", (req, res) => { + const path = req.query.path; + const contents = fs.readFileSync(path); + try { + const data = JSON.parse(contents); + res.json(data); + } catch (err) { + res.status(500).send(err); + } +}); + +app.get("/artifact/text", (req, res) => { + const path = req.query.path; + const contents = fs.readFileSync(path); + res.send(contents); +}); + app.listen(9090); diff --git a/skyvern-frontend/index.html b/skyvern-frontend/index.html index 7dbc1a9f..99d0f255 100644 --- a/skyvern-frontend/index.html +++ b/skyvern-frontend/index.html @@ -2,7 +2,7 @@ - + Skyvern diff --git a/skyvern-frontend/package-lock.json b/skyvern-frontend/package-lock.json index 7a69fcc0..720348dd 100644 --- a/skyvern-frontend/package-lock.json +++ b/skyvern-frontend/package-lock.json @@ -10,22 +10,28 @@ "dependencies": { "@hookform/resolvers": "^3.3.4", "@radix-ui/react-accordion": "^1.1.2", + "@radix-ui/react-aspect-ratio": "^1.0.3", "@radix-ui/react-dialog": "^1.0.5", + "@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-label": "^2.0.2", "@radix-ui/react-radio-group": "^1.1.3", "@radix-ui/react-select": "^2.0.0", "@radix-ui/react-slot": "^1.0.2", + "@radix-ui/react-tabs": "^1.0.4", "@radix-ui/react-toast": "^1.1.5", "@radix-ui/react-tooltip": "^1.0.7", "@tanstack/react-query": "^5.28.6", "axios": "^1.6.8", "class-variance-authority": "^0.7.0", "clsx": "^2.1.0", + "cors": "^2.8.5", + "embla-carousel-react": "^8.0.0", "express": "^4.19.2", "react": "^18.2.0", "react-dom": "^18.2.0", "react-hook-form": "^7.51.1", + "react-medium-image-zoom": "^5.1.11", "react-router-dom": "^6.22.3", "tailwind-merge": "^2.2.2", "tailwindcss-animate": "^1.0.7", @@ -883,6 +889,29 @@ } } }, + "node_modules/@radix-ui/react-aspect-ratio": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-aspect-ratio/-/react-aspect-ratio-1.0.3.tgz", + "integrity": "sha512-fXR5kbMan9oQqMuacfzlGG/SQMcmMlZ4wrvpckv8SgUulD0MMpspxJrxg/Gp/ISV3JfV1AeSWTYK9GvxA4ySwA==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-primitive": "1.0.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-collapsible": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.0.3.tgz", @@ -1053,6 +1082,35 @@ } } }, + "node_modules/@radix-ui/react-dropdown-menu": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.0.6.tgz", + "integrity": "sha512-i6TuFOoWmLWq+M/eCLGd/bQ2HfAX1RJgvrBQ6AQLmzfvsLdefxbWu8G9zczcPFfcSPehz9GcpF6K9QYreFV8hA==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-id": "1.0.1", + "@radix-ui/react-menu": "2.0.6", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-controllable-state": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-focus-guards": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.0.1.tgz", @@ -1144,6 +1202,46 @@ } } }, + "node_modules/@radix-ui/react-menu": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.0.6.tgz", + "integrity": "sha512-BVkFLS+bUC8HcImkRKPSiVumA1VPOOEC5WBMiT+QAVsPzW1FJzI9KnqgGxVDPBcql5xXrHkD3JOVoXWEXD8SYA==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-collection": "1.0.3", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-direction": "1.0.1", + "@radix-ui/react-dismissable-layer": "1.0.5", + "@radix-ui/react-focus-guards": "1.0.1", + "@radix-ui/react-focus-scope": "1.0.4", + "@radix-ui/react-id": "1.0.1", + "@radix-ui/react-popper": "1.1.3", + "@radix-ui/react-portal": "1.0.4", + "@radix-ui/react-presence": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-roving-focus": "1.0.4", + "@radix-ui/react-slot": "1.0.2", + "@radix-ui/react-use-callback-ref": "1.0.1", + "aria-hidden": "^1.1.1", + "react-remove-scroll": "2.5.5" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-popper": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.1.3.tgz", @@ -1370,6 +1468,36 @@ } } }, + "node_modules/@radix-ui/react-tabs": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.0.4.tgz", + "integrity": "sha512-egZfYY/+wRNCflXNHx+dePvnz9FbmssDTJBtgRfDY7e8SE5oIo3Py2eCB1ckAbh1Q7cQ/6yJZThJ++sgbxibog==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-direction": "1.0.1", + "@radix-ui/react-id": "1.0.1", + "@radix-ui/react-presence": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-roving-focus": "1.0.4", + "@radix-ui/react-use-controllable-state": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-toast": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.1.5.tgz", @@ -2661,6 +2789,18 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -2810,6 +2950,31 @@ "integrity": "sha512-ncfPC8UnGIyGFrPE03J5Xn6yTZ6R+clkcZbuG1PJbjAaZBFS4Kn3UKfzu8eilzru6SfC8TPsHuwv0p0eYVu+ww==", "dev": true }, + "node_modules/embla-carousel": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.0.0.tgz", + "integrity": "sha512-ecixcyqS6oKD2nh5Nj5MObcgoSILWNI/GtBxkidn5ytFaCCmwVHo2SecksaQZHcARMMpIR2dWOlSIdA1LkZFUA==" + }, + "node_modules/embla-carousel-react": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/embla-carousel-react/-/embla-carousel-react-8.0.0.tgz", + "integrity": "sha512-qT0dii8ZwoCtEIBE6ogjqU2+5IwnGfdt2teKjCzW88JRErflhlCpz8KjWnW8xoRZOP8g0clRtsMEFoAgS/elfA==", + "dependencies": { + "embla-carousel": "8.0.0", + "embla-carousel-reactive-utils": "8.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.1 || ^18.0.0" + } + }, + "node_modules/embla-carousel-reactive-utils": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/embla-carousel-reactive-utils/-/embla-carousel-reactive-utils-8.0.0.tgz", + "integrity": "sha512-JCw0CqCXI7tbHDRogBb9PoeMLyjEC1vpN0lDOzUjmlfVgtfF+ffLaOK8bVtXVUEbNs/3guGe3NSzA5J5aYzLzw==", + "peerDependencies": { + "embla-carousel": "8.0.0" + } + }, "node_modules/emoji-regex": { "version": "10.3.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz", @@ -4900,6 +5065,21 @@ "react": "^16.8.0 || ^17 || ^18" } }, + "node_modules/react-medium-image-zoom": { + "version": "5.1.11", + "resolved": "https://registry.npmjs.org/react-medium-image-zoom/-/react-medium-image-zoom-5.1.11.tgz", + "integrity": "sha512-7rHECk0nRY+Uwmd02HKPt0L0Lxv2/km24ztcxgUaGR8TD0ikK+OOJyGZAMg/SeQzJDzmNv0yW6U1K8jqnta5MQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/rpearce" + } + ], + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-remove-scroll": { "version": "2.5.5", "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.5.tgz", diff --git a/skyvern-frontend/package.json b/skyvern-frontend/package.json index 7cb8d6b2..acd89727 100644 --- a/skyvern-frontend/package.json +++ b/skyvern-frontend/package.json @@ -4,34 +4,40 @@ "version": "0.0.0", "type": "module", "scripts": { - "dev": "vite & npm run run:server", + "dev": "vite & npm run run-artifact-server", "build": "tsc --noEmit && vite build", "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "format": "prettier --write .", "preview": "vite preview", "prepare": "cd .. && husky skyvern-frontend/.husky", "precommit": "lint-staged", - "run:server": "node server.js" + "run-artifact-server": "node artifactServer.js" }, "dependencies": { "@hookform/resolvers": "^3.3.4", "@radix-ui/react-accordion": "^1.1.2", + "@radix-ui/react-aspect-ratio": "^1.0.3", "@radix-ui/react-dialog": "^1.0.5", + "@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-label": "^2.0.2", "@radix-ui/react-radio-group": "^1.1.3", "@radix-ui/react-select": "^2.0.0", "@radix-ui/react-slot": "^1.0.2", + "@radix-ui/react-tabs": "^1.0.4", "@radix-ui/react-toast": "^1.1.5", "@radix-ui/react-tooltip": "^1.0.7", "@tanstack/react-query": "^5.28.6", "axios": "^1.6.8", "class-variance-authority": "^0.7.0", "clsx": "^2.1.0", + "cors": "^2.8.5", + "embla-carousel-react": "^8.0.0", "express": "^4.19.2", "react": "^18.2.0", "react-dom": "^18.2.0", "react-hook-form": "^7.51.1", + "react-medium-image-zoom": "^5.1.11", "react-router-dom": "^6.22.3", "tailwind-merge": "^2.2.2", "tailwindcss-animate": "^1.0.7", diff --git a/skyvern-frontend/public/favicon.png b/skyvern-frontend/public/favicon.png new file mode 100644 index 00000000..fe3d46ca Binary files /dev/null and b/skyvern-frontend/public/favicon.png differ diff --git a/skyvern-frontend/public/logo.png b/skyvern-frontend/public/logo.png new file mode 100644 index 00000000..c93ed9c5 Binary files /dev/null and b/skyvern-frontend/public/logo.png differ diff --git a/skyvern-frontend/public/skyvern-logo-text.png b/skyvern-frontend/public/skyvern-logo-text.png deleted file mode 100644 index c7b5ab29..00000000 Binary files a/skyvern-frontend/public/skyvern-logo-text.png and /dev/null differ diff --git a/skyvern-frontend/public/skyvern-logo.png b/skyvern-frontend/public/skyvern-logo.png deleted file mode 100644 index de7d6de7..00000000 Binary files a/skyvern-frontend/public/skyvern-logo.png and /dev/null differ diff --git a/skyvern-frontend/src/api/AxiosClient.ts b/skyvern-frontend/src/api/AxiosClient.ts index c332cab2..2df24272 100644 --- a/skyvern-frontend/src/api/AxiosClient.ts +++ b/skyvern-frontend/src/api/AxiosClient.ts @@ -1,4 +1,4 @@ -import { apiBaseUrl, credential } from "@/util/env"; +import { apiBaseUrl, artifactApiBaseUrl, credential } from "@/util/env"; import axios from "axios"; const client = axios.create({ @@ -9,4 +9,8 @@ const client = axios.create({ }, }); -export { client }; +const artifactApiClient = axios.create({ + baseURL: artifactApiBaseUrl, +}); + +export { client, artifactApiClient }; diff --git a/skyvern-frontend/src/api/types.ts b/skyvern-frontend/src/api/types.ts index 6c4a59be..8f82fef3 100644 --- a/skyvern-frontend/src/api/types.ts +++ b/skyvern-frontend/src/api/types.ts @@ -1,6 +1,14 @@ export const ArtifactType = { Recording: "recording", ActionScreenshot: "screenshot_action", + LLMScreenshot: "screenshot_llm", + LLMResponseRaw: "llm_response", + LLMResponseParsed: "llm_response_parsed", + VisibleElementsTree: "visible_elements_tree", + VisibleElementsTreeTrimmed: "visible_elements_tree_trimmed", + LLMPrompt: "llm_prompt", + LLMRequest: "llm_request", + HTMLScrape: "html_scrape", } as const; export const Status = { diff --git a/skyvern-frontend/src/components/Logo.tsx b/skyvern-frontend/src/components/Logo.tsx new file mode 100644 index 00000000..ff3234d0 --- /dev/null +++ b/skyvern-frontend/src/components/Logo.tsx @@ -0,0 +1,6 @@ +function Logo() { + const src = "/logo.png"; + return ; +} + +export { Logo }; diff --git a/skyvern-frontend/src/components/TaskStatusBadge.tsx b/skyvern-frontend/src/components/StatusBadge.tsx similarity index 86% rename from skyvern-frontend/src/components/TaskStatusBadge.tsx rename to skyvern-frontend/src/components/StatusBadge.tsx index 8021bf1f..b3c78a0f 100644 --- a/skyvern-frontend/src/components/TaskStatusBadge.tsx +++ b/skyvern-frontend/src/components/StatusBadge.tsx @@ -5,7 +5,7 @@ type Props = { status: Status; }; -function TaskStatusBadge({ status }: Props) { +function StatusBadge({ status }: Props) { let variant: "default" | "success" | "destructive" | "warning" = "default"; if (status === "completed") { variant = "success"; @@ -18,4 +18,4 @@ function TaskStatusBadge({ status }: Props) { return {status}; } -export { TaskStatusBadge }; +export { StatusBadge }; diff --git a/skyvern-frontend/src/components/ThemeSwitch.tsx b/skyvern-frontend/src/components/ThemeSwitch.tsx new file mode 100644 index 00000000..05e9d20c --- /dev/null +++ b/skyvern-frontend/src/components/ThemeSwitch.tsx @@ -0,0 +1,39 @@ +import { MoonIcon, SunIcon } from "@radix-ui/react-icons"; + +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { useTheme } from "@/components/useTheme"; + +function ThemeToggle() { + const { setTheme } = useTheme(); + + return ( + + + + + + setTheme("light")}> + Light + + setTheme("dark")}> + Dark + + setTheme("system")}> + System + + + + ); +} + +export { ThemeToggle }; diff --git a/skyvern-frontend/src/components/ZoomableImage.tsx b/skyvern-frontend/src/components/ZoomableImage.tsx new file mode 100644 index 00000000..3d0d3ebb --- /dev/null +++ b/skyvern-frontend/src/components/ZoomableImage.tsx @@ -0,0 +1,16 @@ +import Zoom from "react-medium-image-zoom"; +import { AspectRatio } from "@/components/ui/aspect-ratio"; + +type HTMLImageElementProps = React.ComponentProps<"img">; + +function ZoomableImage(props: HTMLImageElementProps) { + return ( + + + + + + ); +} + +export { ZoomableImage }; diff --git a/skyvern-frontend/src/components/ui/aspect-ratio.tsx b/skyvern-frontend/src/components/ui/aspect-ratio.tsx new file mode 100644 index 00000000..c9e6f4bf --- /dev/null +++ b/skyvern-frontend/src/components/ui/aspect-ratio.tsx @@ -0,0 +1,5 @@ +import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"; + +const AspectRatio = AspectRatioPrimitive.Root; + +export { AspectRatio }; diff --git a/skyvern-frontend/src/components/ui/carousel.tsx b/skyvern-frontend/src/components/ui/carousel.tsx new file mode 100644 index 00000000..b1b8c4b8 --- /dev/null +++ b/skyvern-frontend/src/components/ui/carousel.tsx @@ -0,0 +1,260 @@ +import * as React from "react"; +import { ArrowLeftIcon, ArrowRightIcon } from "@radix-ui/react-icons"; +import useEmblaCarousel, { + type UseEmblaCarouselType, +} from "embla-carousel-react"; + +import { cn } from "@/util/utils"; +import { Button } from "@/components/ui/button"; + +type CarouselApi = UseEmblaCarouselType[1]; +type UseCarouselParameters = Parameters; +type CarouselOptions = UseCarouselParameters[0]; +type CarouselPlugin = UseCarouselParameters[1]; + +type CarouselProps = { + opts?: CarouselOptions; + plugins?: CarouselPlugin; + orientation?: "horizontal" | "vertical"; + setApi?: (api: CarouselApi) => void; +}; + +type CarouselContextProps = { + carouselRef: ReturnType[0]; + api: ReturnType[1]; + scrollPrev: () => void; + scrollNext: () => void; + canScrollPrev: boolean; + canScrollNext: boolean; +} & CarouselProps; + +const CarouselContext = React.createContext(null); + +function useCarousel() { + const context = React.useContext(CarouselContext); + + if (!context) { + throw new Error("useCarousel must be used within a "); + } + + return context; +} + +const Carousel = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes & CarouselProps +>( + ( + { + orientation = "horizontal", + opts, + setApi, + plugins, + className, + children, + ...props + }, + ref, + ) => { + const [carouselRef, api] = useEmblaCarousel( + { + ...opts, + axis: orientation === "horizontal" ? "x" : "y", + }, + plugins, + ); + const [canScrollPrev, setCanScrollPrev] = React.useState(false); + const [canScrollNext, setCanScrollNext] = React.useState(false); + + const onSelect = React.useCallback((api: CarouselApi) => { + if (!api) { + return; + } + + setCanScrollPrev(api.canScrollPrev()); + setCanScrollNext(api.canScrollNext()); + }, []); + + const scrollPrev = React.useCallback(() => { + api?.scrollPrev(); + }, [api]); + + const scrollNext = React.useCallback(() => { + api?.scrollNext(); + }, [api]); + + const handleKeyDown = React.useCallback( + (event: React.KeyboardEvent) => { + if (event.key === "ArrowLeft") { + event.preventDefault(); + scrollPrev(); + } else if (event.key === "ArrowRight") { + event.preventDefault(); + scrollNext(); + } + }, + [scrollPrev, scrollNext], + ); + + React.useEffect(() => { + if (!api || !setApi) { + return; + } + + setApi(api); + }, [api, setApi]); + + React.useEffect(() => { + if (!api) { + return; + } + + onSelect(api); + api.on("reInit", onSelect); + api.on("select", onSelect); + + return () => { + api?.off("select", onSelect); + }; + }, [api, onSelect]); + + return ( + +
+ {children} +
+
+ ); + }, +); +Carousel.displayName = "Carousel"; + +const CarouselContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => { + const { carouselRef, orientation } = useCarousel(); + + return ( +
+
+
+ ); +}); +CarouselContent.displayName = "CarouselContent"; + +const CarouselItem = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => { + const { orientation } = useCarousel(); + + return ( +
+ ); +}); +CarouselItem.displayName = "CarouselItem"; + +const CarouselPrevious = React.forwardRef< + HTMLButtonElement, + React.ComponentProps +>(({ className, variant = "outline", size = "icon", ...props }, ref) => { + const { orientation, scrollPrev, canScrollPrev } = useCarousel(); + + return ( + + ); +}); +CarouselPrevious.displayName = "CarouselPrevious"; + +const CarouselNext = React.forwardRef< + HTMLButtonElement, + React.ComponentProps +>(({ className, variant = "outline", size = "icon", ...props }, ref) => { + const { orientation, scrollNext, canScrollNext } = useCarousel(); + + return ( + + ); +}); +CarouselNext.displayName = "CarouselNext"; + +export { + type CarouselApi, + Carousel, + CarouselContent, + CarouselItem, + CarouselPrevious, + CarouselNext, +}; diff --git a/skyvern-frontend/src/components/ui/dropdown-menu.tsx b/skyvern-frontend/src/components/ui/dropdown-menu.tsx new file mode 100644 index 00000000..60e73e35 --- /dev/null +++ b/skyvern-frontend/src/components/ui/dropdown-menu.tsx @@ -0,0 +1,203 @@ +import * as React from "react"; +import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"; +import { + CheckIcon, + ChevronRightIcon, + DotFilledIcon, +} from "@radix-ui/react-icons"; + +import { cn } from "@/util/utils"; + +const DropdownMenu = DropdownMenuPrimitive.Root; + +const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger; + +const DropdownMenuGroup = DropdownMenuPrimitive.Group; + +const DropdownMenuPortal = DropdownMenuPrimitive.Portal; + +const DropdownMenuSub = DropdownMenuPrimitive.Sub; + +const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup; + +const DropdownMenuSubTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean; + } +>(({ className, inset, children, ...props }, ref) => ( + + {children} + + +)); +DropdownMenuSubTrigger.displayName = + DropdownMenuPrimitive.SubTrigger.displayName; + +const DropdownMenuSubContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DropdownMenuSubContent.displayName = + DropdownMenuPrimitive.SubContent.displayName; + +const DropdownMenuContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, sideOffset = 4, ...props }, ref) => ( + + + +)); +DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName; + +const DropdownMenuItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean; + } +>(({ className, inset, ...props }, ref) => ( + +)); +DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName; + +const DropdownMenuCheckboxItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, checked, ...props }, ref) => ( + + + + + + + {children} + +)); +DropdownMenuCheckboxItem.displayName = + DropdownMenuPrimitive.CheckboxItem.displayName; + +const DropdownMenuRadioItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + {children} + +)); +DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName; + +const DropdownMenuLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean; + } +>(({ className, inset, ...props }, ref) => ( + +)); +DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName; + +const DropdownMenuSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName; + +const DropdownMenuShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( + + ); +}; +DropdownMenuShortcut.displayName = "DropdownMenuShortcut"; + +export { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuGroup, + DropdownMenuPortal, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuRadioGroup, +}; diff --git a/skyvern-frontend/src/components/ui/tabs.tsx b/skyvern-frontend/src/components/ui/tabs.tsx new file mode 100644 index 00000000..3b34c87e --- /dev/null +++ b/skyvern-frontend/src/components/ui/tabs.tsx @@ -0,0 +1,53 @@ +import * as React from "react"; +import * as TabsPrimitive from "@radix-ui/react-tabs"; + +import { cn } from "@/util/utils"; + +const Tabs = TabsPrimitive.Root; + +const TabsList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +TabsList.displayName = TabsPrimitive.List.displayName; + +const TabsTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +TabsTrigger.displayName = TabsPrimitive.Trigger.displayName; + +const TabsContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +TabsContent.displayName = TabsPrimitive.Content.displayName; + +export { Tabs, TabsList, TabsTrigger, TabsContent }; diff --git a/skyvern-frontend/src/index.css b/skyvern-frontend/src/index.css index b16ead87..2c2aead2 100644 --- a/skyvern-frontend/src/index.css +++ b/skyvern-frontend/src/index.css @@ -73,6 +73,104 @@ body { @apply bg-background text-foreground; } + + [data-rmiz] { + position: relative; + } + [data-rmiz-ghost] { + position: absolute; + pointer-events: none; + } + [data-rmiz-btn-zoom], + [data-rmiz-btn-unzoom] { + background-color: rgba(0, 0, 0, 0.7); + border-radius: 50%; + border: none; + box-shadow: 0 0 1px rgba(255, 255, 255, 0.5); + color: #fff; + height: 40px; + margin: 0; + outline-offset: 2px; + padding: 9px; + touch-action: manipulation; + width: 40px; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + } + [data-rmiz-btn-zoom]:not(:focus):not(:active) { + position: absolute; + clip: rect(0 0 0 0); + clip-path: inset(50%); + height: 1px; + overflow: hidden; + pointer-events: none; + white-space: nowrap; + width: 1px; + } + [data-rmiz-btn-zoom] { + position: absolute; + inset: 10px 10px auto auto; + cursor: zoom-in; + } + [data-rmiz-btn-unzoom] { + position: absolute; + inset: 20px 20px auto auto; + cursor: zoom-out; + z-index: 1; + } + [data-rmiz-content="found"] img, + [data-rmiz-content="found"] svg, + [data-rmiz-content="found"] [role="img"], + [data-rmiz-content="found"] [data-zoom] { + cursor: zoom-in; + } + [data-rmiz-modal]::backdrop { + display: none; + } + [data-rmiz-modal][open] { + position: fixed; + width: 100vw; + width: 100dvw; + height: 100vh; + height: 100dvh; + max-width: none; + max-height: none; + margin: 0; + padding: 0; + border: 0; + background: transparent; + overflow: hidden; + } + [data-rmiz-modal-overlay] { + position: absolute; + inset: 0; + transition: background-color 0.3s; + } + [data-rmiz-modal-overlay="hidden"] { + background-color: rgba(255, 255, 255, 0); + } + [data-rmiz-modal-overlay="visible"] { + @apply bg-background; + } + [data-rmiz-modal-content] { + position: relative; + width: 100%; + height: 100%; + } + [data-rmiz-modal-img] { + position: absolute; + cursor: zoom-out; + image-rendering: high-quality; + transform-origin: top left; + transition: transform 0.3s; + } + @media (prefers-reduced-motion: reduce) { + [data-rmiz-modal-overlay], + [data-rmiz-modal-img] { + transition-duration: 0.01ms !important; + } + } } body, diff --git a/skyvern-frontend/src/routes/root/RootLayout.tsx b/skyvern-frontend/src/routes/root/RootLayout.tsx index 9f053a12..68409967 100644 --- a/skyvern-frontend/src/routes/root/RootLayout.tsx +++ b/skyvern-frontend/src/routes/root/RootLayout.tsx @@ -2,20 +2,21 @@ import { Link, Outlet } from "react-router-dom"; import { Toaster } from "@/components/ui/toaster"; import { SideNav } from "./SideNav"; import { DiscordLogoIcon, GitHubLogoIcon } from "@radix-ui/react-icons"; +import { Logo } from "@/components/Logo"; +import { ThemeToggle } from "@/components/ThemeSwitch"; function RootLayout() { return ( <> -
+
diff --git a/skyvern-frontend/src/routes/tasks/detail/JSONArtifact.tsx b/skyvern-frontend/src/routes/tasks/detail/JSONArtifact.tsx new file mode 100644 index 00000000..128f5fc9 --- /dev/null +++ b/skyvern-frontend/src/routes/tasks/detail/JSONArtifact.tsx @@ -0,0 +1,40 @@ +import { artifactApiClient } from "@/api/AxiosClient"; +import { Skeleton } from "@/components/ui/skeleton"; +import { Textarea } from "@/components/ui/textarea"; +import { useQuery } from "@tanstack/react-query"; + +type Props = { + uri: string; +}; + +function JSONArtifact({ uri }: Props) { + const { data, isFetching, isError, error } = useQuery< + Record + >({ + queryKey: ["artifact", uri], + queryFn: async () => { + return artifactApiClient + .get(`/artifact/json`, { + params: { + path: uri.slice(7), + }, + }) + .then((response) => response.data); + }, + }); + + if (isFetching) { + return ; + } + + return ( +