diff --git a/README.md b/README.md index 1365a69..eb9725b 100644 --- a/README.md +++ b/README.md @@ -4,10 +4,13 @@ This is a simple app that converts a screenshot to HTML/Tailwind CSS. It uses GP https://github.com/abi/screenshot-to-code/assets/23818/6cebadae-2fe3-4986-ac6a-8fb9db030045 -See Examples section below for more demos. +See [Examples](#examples) section below for more demos. + +🆕 [Try it here](https://picoapps.xyz/free-tools/screenshot-to-code) (bring your own OpenAI key - **your key must have access to GPT-4 Vision. See [FAQ](#faqs) section below for details**). Or see [Getting Started](#getting-started) below for local install instructions. ## Updates +- Nov 16 - Added a setting to disable DALL-E image generation if you don't need that - Nov 16 - View code directly within the app - Nov 15 - 🔥 You can now instruct the AI to update the code as you wish. Useful if the AI messed up some styles or missed a section. @@ -50,16 +53,28 @@ Application will be up and running at http://localhost:5173 Note that you can't develop the application with this setup as the file changes won't trigger a rebuild. -## Feedback +## FAQs -If you have feature requests, bug reports or other feedback, open an issue or ping me on [Twitter](https://twitter.com/_abi_). +- **I'm running into an error when setting up the backend. How can I fix it?** [Try this](https://github.com/abi/screenshot-to-code/issues/3#issuecomment-1814777959). If that still doesn't work, open an issue. +- **How do I get an OpenAI API key that has the GPT4 Vision model available?** Create an OpenAI account. And then, you need to buy at least $1 worth of credit on the [Billing dashboard](https://platform.openai.com/account/billing/overview). +- **How can I provide feedback?** For feedback, feature requests and bug reports, open an issue or ping me on [Twitter](https://twitter.com/_abi_). ## Examples -Hacker News but it gets the colors wrong at first so we nudge it +**NYTimes** + +| Original | Replica | +| -------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | +| ![Original](https://github.com/abi/screenshot-to-code/assets/23818/01b062c2-f39a-46dd-84ca-bec6c986463a) | ![Replica](https://github.com/abi/screenshot-to-code/assets/23818/e1f662b6-68f7-4578-a64d-ea4be52e31f5) | + +**Instagram page (with not Taylor Swift pics)** + +https://github.com/abi/screenshot-to-code/assets/23818/503eb86a-356e-4dfc-926a-dabdb1ac7ba1 + +**Hacker News** but it gets the colors wrong at first so we nudge it https://github.com/abi/screenshot-to-code/assets/23818/3fec0f77-44e8-4fb3-a769-ac7410315e5d ## Hosted Version -Hosted version coming soon on [Pico](https://picoapps.xyz?ref=github). +🆕 [Try it here](https://picoapps.xyz/free-tools/screenshot-to-code) (bring your own OpenAI key - **your key must have access to GPT-4 Vision. See [FAQ](#faqs) section for details**). Or see [Getting Started](#getting-started) for local install instructions. diff --git a/backend/build.sh b/backend/build.sh new file mode 100644 index 0000000..baab923 --- /dev/null +++ b/backend/build.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +# exit on error +set -o errexit + +echo "Installing the latest version of poetry..." +pip install --upgrade pip +pip install poetry==1.4.1 + +rm poetry.lock +poetry lock +python -m poetry install diff --git a/backend/image_generation.py b/backend/image_generation.py index 5d4b81c..080334f 100644 --- a/backend/image_generation.py +++ b/backend/image_generation.py @@ -5,8 +5,8 @@ from openai import AsyncOpenAI from bs4 import BeautifulSoup -async def process_tasks(prompts): - tasks = [generate_image(prompt) for prompt in prompts] +async def process_tasks(prompts, api_key): + tasks = [generate_image(prompt, api_key) for prompt in prompts] results = await asyncio.gather(*tasks, return_exceptions=True) processed_results = [] @@ -20,8 +20,8 @@ async def process_tasks(prompts): return processed_results -async def generate_image(prompt): - client = AsyncOpenAI(api_key=os.environ.get("OPENAI_API_KEY")) +async def generate_image(prompt, api_key): + client = AsyncOpenAI(api_key=api_key) image_params = { "model": "dall-e-3", "quality": "standard", @@ -60,7 +60,7 @@ def create_alt_url_mapping(code): return mapping -async def generate_images(code, image_cache): +async def generate_images(code, api_key, image_cache): # Find all images soup = BeautifulSoup(code, "html.parser") images = soup.find_all("img") @@ -82,8 +82,12 @@ async def generate_images(code, image_cache): # Remove duplicates prompts = list(set(alts)) + # Return early if there are no images to replace + if len(prompts) == 0: + return code + # Generate images - results = await process_tasks(prompts) + results = await process_tasks(prompts, api_key) # Create a dict mapping alt text to image URL mapped_image_urls = dict(zip(prompts, results)) @@ -110,4 +114,5 @@ async def generate_images(code, image_cache): print("Image generation failed for alt text:" + img.get("alt")) # Return the modified HTML - return str(soup) + # (need to prettify it because BeautifulSoup messes up the formatting) + return soup.prettify() diff --git a/backend/llm.py b/backend/llm.py index 686b008..b52c3c9 100644 --- a/backend/llm.py +++ b/backend/llm.py @@ -4,10 +4,12 @@ from openai import AsyncOpenAI MODEL_GPT_4_VISION = "gpt-4-vision-preview" -client = AsyncOpenAI(api_key=os.environ.get("OPENAI_API_KEY")) +async def stream_openai_response( + messages, api_key: str, callback: Callable[[str], Awaitable[None]] +): + client = AsyncOpenAI(api_key=api_key) -async def stream_openai_response(messages, callback: Callable[[str], Awaitable[None]]): model = MODEL_GPT_4_VISION # Base parameters diff --git a/backend/main.py b/backend/main.py index 5abd333..c6e5556 100644 --- a/backend/main.py +++ b/backend/main.py @@ -11,7 +11,7 @@ from datetime import datetime from fastapi import FastAPI, WebSocket from llm import stream_openai_response -from mock import MOCK_HTML, mock_completion +from mock import mock_completion from image_generation import create_alt_url_mapping, generate_images from prompts import assemble_prompt @@ -23,12 +23,18 @@ SHOULD_MOCK_AI_RESPONSE = False def write_logs(prompt_messages, completion): - # Create run_logs directory if it doesn't exist - if not os.path.exists("run_logs"): - os.makedirs("run_logs") + # Get the logs path from environment, default to the current working directory + logs_path = os.environ.get("LOGS_PATH", os.getcwd()) - # Generate a unique filename using the current timestamp - filename = datetime.now().strftime("run_logs/messages_%Y%m%d_%H%M%S.json") + # Create run_logs directory if it doesn't exist within the specified logs path + logs_directory = os.path.join(logs_path, "run_logs") + if not os.path.exists(logs_directory): + os.makedirs(logs_directory) + + print("Writing to logs directory:", logs_directory) + + # Generate a unique filename using the current timestamp within the logs directory + filename = datetime.now().strftime(f"{logs_directory}/messages_%Y%m%d_%H%M%S.json") # Write the messages dict into a new file for each run with open(filename, "w") as f: @@ -41,6 +47,33 @@ async def stream_code_test(websocket: WebSocket): params = await websocket.receive_json() + # Get the OpenAI API key from the request. Fall back to environment variable if not provided. + # If neither is provided, we throw an error. + if params["openAiApiKey"]: + openai_api_key = params["openAiApiKey"] + print("Using OpenAI API key from client-side settings dialog") + else: + openai_api_key = os.environ.get("OPENAI_API_KEY") + if openai_api_key: + print("Using OpenAI API key from environment variable") + + if not openai_api_key: + print("OpenAI API key not found") + await websocket.send_json( + { + "type": "error", + "value": "No OpenAI API key found. Please add your API key in the settings dialog or add it to backend/.env file.", + } + ) + return + + should_generate_images = ( + params["isImageGenerationEnabled"] + if "isImageGenerationEnabled" in params + else True + ) + + print("generating code...") await websocket.send_json({"type": "status", "value": "Generating code..."}) async def process_chunk(content): @@ -66,17 +99,23 @@ async def stream_code_test(websocket: WebSocket): else: completion = await stream_openai_response( prompt_messages, - lambda x: process_chunk(x), + api_key=openai_api_key, + callback=lambda x: process_chunk(x), ) # Write the messages dict into a log so that we can debug later write_logs(prompt_messages, completion) - # Generate images - await websocket.send_json({"type": "status", "value": "Generating images..."}) - try: - updated_html = await generate_images(completion, image_cache=image_cache) + if should_generate_images: + await websocket.send_json( + {"type": "status", "value": "Generating images..."} + ) + updated_html = await generate_images( + completion, api_key=openai_api_key, image_cache=image_cache + ) + else: + updated_html = completion await websocket.send_json({"type": "setCode", "value": updated_html}) await websocket.send_json( {"type": "status", "value": "Code generation complete."} diff --git a/backend/mock.py b/backend/mock.py index ec26339..90dc7d3 100644 --- a/backend/mock.py +++ b/backend/mock.py @@ -2,7 +2,7 @@ import asyncio async def mock_completion(process_chunk): - code_to_return = MOCK_HTML_2 + code_to_return = NO_IMAGES_NYTIMES_MOCK_CODE for i in range(0, len(code_to_return), 10): await process_chunk(code_to_return[i : i + 10]) @@ -11,7 +11,7 @@ async def mock_completion(process_chunk): return code_to_return -MOCK_HTML = """ +APPLE_MOCK_CODE = """ @@ -68,7 +68,7 @@ MOCK_HTML = """ """ -MOCK_HTML_2 = """ +NYTIMES_MOCK_CODE = """ @@ -138,3 +138,70 @@ MOCK_HTML_2 = """ """ + +NO_IMAGES_NYTIMES_MOCK_CODE = """ + + + + + The New York Times - News + + + + + + +
+
+
+
+ + +
Tuesday, November 14, 2023
Today's Paper
+
+
+ +
Account
+
+
+ +
+
+
+
+
+
+

Israeli Military Raids Gaza’s Largest Hospital

+

Israeli troops have entered the Al-Shifa Hospital complex, where conditions have grown dire and Israel says Hamas fighters are embedded.

+ See more updates +
+ +
+
+
+

From Elvis to Elopements, the Evolution of the Las Vegas Wedding

+

The glittering city that attracts thousands of couples seeking unconventional nuptials has grown beyond the drive-through wedding.

+ 8 MIN READ +
+ +
+
+
+
+
+ + +""" diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 9983a61..9aa721d 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -4,7 +4,6 @@ version = "0.1.0" description = "" authors = ["Abi Raja "] license = "MIT" -readme = "README.md" [tool.poetry.dependencies] python = "^3.10" diff --git a/frontend/package.json b/frontend/package.json index 2b37f02..7a34ab2 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -11,9 +11,12 @@ }, "dependencies": { "@codemirror/lang-html": "^6.4.6", + "@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-icons": "^1.3.0", + "@radix-ui/react-label": "^2.0.2", "@radix-ui/react-separator": "^1.0.3", "@radix-ui/react-slot": "^1.0.2", + "@radix-ui/react-switch": "^1.0.3", "@radix-ui/react-tabs": "^1.0.4", "class-variance-authority": "^0.7.0", "classnames": "^2.3.2", @@ -44,5 +47,8 @@ "tailwindcss": "^3.3.5", "typescript": "^5.0.2", "vite": "^4.4.5" + }, + "engines": { + "node": ">=14.18.0" } } diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 452e0be..654acf2 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -5,11 +5,22 @@ import Preview from "./components/Preview"; import { CodeGenerationParams, generateCode } from "./generateCode"; import Spinner from "./components/Spinner"; import classNames from "classnames"; -import { FaCode, FaDownload, FaEye, FaUndo } from "react-icons/fa"; +import { + FaCode, + FaDesktop, + FaDownload, + FaMobile, + FaUndo, +} from "react-icons/fa"; import { Button } from "@/components/ui/button"; import { Textarea } from "@/components/ui/textarea"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "./components/ui/tabs"; import CodeMirror from "./components/CodeMirror"; +import SettingsDialog from "./components/SettingsDialog"; +import { Settings } from "./types"; +import { IS_RUNNING_ON_CLOUD } from "./config"; +import { PicoBadge } from "./components/PicoBadge"; +import { OnboardingNote } from "./components/OnboardingNote"; function App() { const [appState, setAppState] = useState<"INITIAL" | "CODING" | "CODE_READY">( @@ -20,6 +31,10 @@ function App() { const [executionConsole, setExecutionConsole] = useState([]); const [updateInstruction, setUpdateInstruction] = useState(""); const [history, setHistory] = useState([]); + const [settings, setSettings] = useState({ + openAiApiKey: null, + isImageGenerationEnabled: true, + }); const downloadCode = () => { // Create a blob from the generated code @@ -49,8 +64,12 @@ function App() { function doGenerateCode(params: CodeGenerationParams) { setExecutionConsole([]); setAppState("CODING"); + + // Merge settings with params + const updatedParams = { ...params, ...settings }; + generateCode( - params, + updatedParams, (token) => setGeneratedCode((prev) => prev + token), (code) => setGeneratedCode(code), (line) => setExecutionConsole((prev) => [...prev, line]), @@ -61,10 +80,12 @@ function App() { // Initial version creation function doCreate(referenceImages: string[]) { setReferenceImages(referenceImages); - doGenerateCode({ - generationType: "create", - image: referenceImages[0], - }); + if (referenceImages.length > 0) { + doGenerateCode({ + generationType: "create", + image: referenceImages[0], + }); + } } // Subsequent updates @@ -83,16 +104,23 @@ function App() { } return ( -
+
+ {IS_RUNNING_ON_CLOUD && } +
-

Screenshot to Code

+
+

Screenshot to Code

+ +
{appState === "INITIAL" && (

Drag & drop a screenshot to get started.

)} + {IS_RUNNING_ON_CLOUD && !settings.openAiApiKey && } + {(appState === "CODING" || appState === "CODE_READY") && ( <> {/* Show code preview only when coding */} @@ -181,11 +209,14 @@ function App() { {(appState === "CODING" || appState === "CODE_READY") && (
- -
+ +
- - Preview + + Desktop + + + Mobile @@ -193,8 +224,11 @@ function App() {
- - + + + + + diff --git a/frontend/src/components/CodeMirror.tsx b/frontend/src/components/CodeMirror.tsx index 7c6d9df..3ba1c51 100644 --- a/frontend/src/components/CodeMirror.tsx +++ b/frontend/src/components/CodeMirror.tsx @@ -60,6 +60,6 @@ function CodeMirror({ code }: Props) { } }, [code]); - return
; + return
; } export default CodeMirror; diff --git a/frontend/src/components/ImageUpload.tsx b/frontend/src/components/ImageUpload.tsx index 16b5373..4a46d99 100644 --- a/frontend/src/components/ImageUpload.tsx +++ b/frontend/src/components/ImageUpload.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useMemo } from "react"; +import { useState, useEffect, useMemo, useCallback } from "react"; import { useDropzone } from "react-dropzone"; // import { PromptImage } from "../../../types"; import { toast } from "react-hot-toast"; @@ -35,7 +35,7 @@ const rejectStyle = { borderColor: "#ff1744", }; -// TODO: Move to a seperate file +// TODO: Move to a separate file function fileToDataURL(file: File) { return new Promise((resolve, reject) => { const reader = new FileReader(); @@ -80,7 +80,7 @@ function ImageUpload({ setReferenceImages }: Props) { setReferenceImages(dataUrls.map((dataUrl) => dataUrl as string)); }) .catch((error) => { - // TODO: Display error to user + toast.error("Error reading files" + error); console.error("Error reading files:", error); }); }, @@ -89,6 +89,38 @@ function ImageUpload({ setReferenceImages }: Props) { }, }); + const pasteEvent = useCallback( + (event: ClipboardEvent) => { + const clipboardData = event.clipboardData; + if (!clipboardData) return; + + const items = clipboardData.items; + const files = []; + for (let i = 0; i < items.length; i++) { + const file = items[i].getAsFile(); + if (file && file.type.startsWith("image/")) { + files.push(file); + } + } + + // Convert images to data URLs and set the prompt images state + Promise.all(files.map((file) => fileToDataURL(file))) + .then((dataUrls) => { + setReferenceImages(dataUrls.map((dataUrl) => dataUrl as string)); + }) + .catch((error) => { + // TODO: Display error to user + console.error("Error reading files:", error); + }); + }, + [setReferenceImages] + ); + + // TODO: Make sure we don't listen to paste events in text input components + useEffect(() => { + window.addEventListener("paste", pasteEvent); + }, [pasteEvent]); + useEffect(() => { return () => files.forEach((file) => URL.revokeObjectURL(file.preview)); }, [files]); // Added files as a dependency @@ -108,7 +140,7 @@ function ImageUpload({ setReferenceImages }: Props) { {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
-

Drop a screenshot here, or click to select

+

Drop a screenshot here, paste from clipboard, or click to select

); diff --git a/frontend/src/components/OnboardingNote.tsx b/frontend/src/components/OnboardingNote.tsx new file mode 100644 index 0000000..437bc1e --- /dev/null +++ b/frontend/src/components/OnboardingNote.tsx @@ -0,0 +1,20 @@ +export function OnboardingNote() { + return ( +
+ Please add your OpenAI API key (must have GPT4 vision access) in the + settings dialog (gear icon above). +
+
+ How do you get an OpenAI API key that has the GPT4 Vision model available? + Create an OpenAI account. And then, you need to buy at least $1 worth of + credit on the Billing dashboard. +
+ + This key is never stored. This app is open source. You can{" "} + + check the code to confirm. + + +
+ ); +} diff --git a/frontend/src/components/PicoBadge.tsx b/frontend/src/components/PicoBadge.tsx new file mode 100644 index 0000000..077a99d --- /dev/null +++ b/frontend/src/components/PicoBadge.tsx @@ -0,0 +1,12 @@ +export function PicoBadge() { + return ( + +
+ an open source project by Pico +
+
+ ); +} diff --git a/frontend/src/components/Preview.tsx b/frontend/src/components/Preview.tsx index 4728a73..de0134f 100644 --- a/frontend/src/components/Preview.tsx +++ b/frontend/src/components/Preview.tsx @@ -1,19 +1,27 @@ +import classNames from "classnames"; import useThrottle from "../hooks/useThrottle"; interface Props { code: string; + device: "mobile" | "desktop"; } -function Preview({ code }: Props) { +function Preview({ code, device }: Props) { const throttledCode = useThrottle(code, 200); return ( -
+
); diff --git a/frontend/src/components/SettingsDialog.tsx b/frontend/src/components/SettingsDialog.tsx new file mode 100644 index 0000000..512894f --- /dev/null +++ b/frontend/src/components/SettingsDialog.tsx @@ -0,0 +1,77 @@ +import { + Dialog, + DialogClose, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { FaCog } from "react-icons/fa"; +import { Settings } from "../types"; +import { Switch } from "./ui/switch"; +import { Label } from "./ui/label"; +import { Input } from "./ui/input"; + +interface Props { + settings: Settings; + setSettings: React.Dispatch>; +} + +function SettingsDialog({ settings, setSettings }: Props) { + return ( + + + + + + + Settings + +
+ + + setSettings((s) => ({ + ...s, + isImageGenerationEnabled: !s.isImageGenerationEnabled, + })) + } + /> +
+
+ + + + setSettings((s) => ({ + ...s, + openAiApiKey: e.target.value, + })) + } + /> +
+ + Save + +
+
+ ); +} + +export default SettingsDialog; diff --git a/frontend/src/components/ui/dialog.tsx b/frontend/src/components/ui/dialog.tsx new file mode 100644 index 0000000..776ca68 --- /dev/null +++ b/frontend/src/components/ui/dialog.tsx @@ -0,0 +1,120 @@ +import * as React from "react" +import * as DialogPrimitive from "@radix-ui/react-dialog" +import { Cross2Icon } from "@radix-ui/react-icons" + +import { cn } from "@/lib/utils" + +const Dialog = DialogPrimitive.Root + +const DialogTrigger = DialogPrimitive.Trigger + +const DialogPortal = DialogPrimitive.Portal + +const DialogClose = DialogPrimitive.Close + +const DialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName + +const DialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + {children} + + + Close + + + +)) +DialogContent.displayName = DialogPrimitive.Content.displayName + +const DialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogHeader.displayName = "DialogHeader" + +const DialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogFooter.displayName = "DialogFooter" + +const DialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogTitle.displayName = DialogPrimitive.Title.displayName + +const DialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogDescription.displayName = DialogPrimitive.Description.displayName + +export { + Dialog, + DialogPortal, + DialogOverlay, + DialogTrigger, + DialogClose, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, +} diff --git a/frontend/src/components/ui/input.tsx b/frontend/src/components/ui/input.tsx new file mode 100644 index 0000000..a92b8e0 --- /dev/null +++ b/frontend/src/components/ui/input.tsx @@ -0,0 +1,25 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +export interface InputProps + extends React.InputHTMLAttributes {} + +const Input = React.forwardRef( + ({ className, type, ...props }, ref) => { + return ( + + ) + } +) +Input.displayName = "Input" + +export { Input } diff --git a/frontend/src/components/ui/label.tsx b/frontend/src/components/ui/label.tsx new file mode 100644 index 0000000..683faa7 --- /dev/null +++ b/frontend/src/components/ui/label.tsx @@ -0,0 +1,24 @@ +import * as React from "react" +import * as LabelPrimitive from "@radix-ui/react-label" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const labelVariants = cva( + "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" +) + +const Label = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & + VariantProps +>(({ className, ...props }, ref) => ( + +)) +Label.displayName = LabelPrimitive.Root.displayName + +export { Label } diff --git a/frontend/src/components/ui/switch.tsx b/frontend/src/components/ui/switch.tsx new file mode 100644 index 0000000..f15014b --- /dev/null +++ b/frontend/src/components/ui/switch.tsx @@ -0,0 +1,27 @@ +import * as React from "react"; +import * as SwitchPrimitives from "@radix-ui/react-switch"; + +import { cn } from "@/lib/utils"; + +const Switch = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)); +Switch.displayName = SwitchPrimitives.Root.displayName; + +export { Switch }; diff --git a/frontend/src/config.ts b/frontend/src/config.ts new file mode 100644 index 0000000..054f6ed --- /dev/null +++ b/frontend/src/config.ts @@ -0,0 +1,3 @@ +// Default to false if set to anything other than "true" or unset +export const IS_RUNNING_ON_CLOUD = + import.meta.env.VITE_IS_DEPLOYED === "true" || false; diff --git a/frontend/src/generateCode.ts b/frontend/src/generateCode.ts index 77edf07..3b0f3d6 100644 --- a/frontend/src/generateCode.ts +++ b/frontend/src/generateCode.ts @@ -3,12 +3,13 @@ import toast from "react-hot-toast"; const WS_BACKEND_URL = import.meta.env.VITE_WS_BACKEND_URL || "ws://127.0.0.1:7001"; const ERROR_MESSAGE = - "Error generating code. Check the Developer Console for details. Feel free to open a Github ticket"; + "Error generating code. Check the Developer Console for details. Feel free to open a Github issue"; export interface CodeGenerationParams { generationType: "create" | "update"; image: string; history?: string[]; + // isImageGenerationEnabled: boolean; // TODO: Merge with Settings type in types.ts } export function generateCode( @@ -35,6 +36,9 @@ export function generateCode( onStatusUpdate(response.value); } else if (response.type === "setCode") { onSetCode(response.value); + } else if (response.type === "error") { + console.error("Error generating code", response.value); + toast.error(response.value); } }); diff --git a/frontend/src/types.ts b/frontend/src/types.ts new file mode 100644 index 0000000..b2c6305 --- /dev/null +++ b/frontend/src/types.ts @@ -0,0 +1,4 @@ +export interface Settings { + openAiApiKey: string | null; + isImageGenerationEnabled: boolean; +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 8b0c365..3e7414d 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -5,6 +5,7 @@ import react from "@vitejs/plugin-react"; // https://vitejs.dev/config/ export default defineConfig({ + base: process.env.VITE_IS_DEPLOYED ? "/free-tools/screenshot-to-code/" : "", plugins: [react(), checker({ typescript: true })], resolve: { alias: { diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 20ade74..2109c60 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -618,6 +618,27 @@ dependencies: "@babel/runtime" "^7.13.10" +"@radix-ui/react-dialog@^1.0.5": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@radix-ui/react-dialog/-/react-dialog-1.0.5.tgz#71657b1b116de6c7a0b03242d7d43e01062c7300" + integrity sha512-GjWJX/AUpB703eEBanuBnIWdIXg6NvJFCXcNlSZk4xdszCdhrJgBoUd1cGk67vFO+WdA2pfI/plOpqz/5GUP6Q== + 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-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-portal" "1.0.4" + "@radix-ui/react-presence" "1.0.1" + "@radix-ui/react-primitive" "1.0.3" + "@radix-ui/react-slot" "1.0.2" + "@radix-ui/react-use-controllable-state" "1.0.1" + aria-hidden "^1.1.1" + react-remove-scroll "2.5.5" + "@radix-ui/react-direction@1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@radix-ui/react-direction/-/react-direction-1.0.1.tgz#9cb61bf2ccf568f3421422d182637b7f47596c9b" @@ -625,6 +646,35 @@ dependencies: "@babel/runtime" "^7.13.10" +"@radix-ui/react-dismissable-layer@1.0.5": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.0.5.tgz#3f98425b82b9068dfbab5db5fff3df6ebf48b9d4" + integrity sha512-aJeDjQhywg9LBu2t/At58hCvr7pEm0o2Ke1x33B+MhjNmmZ17sy4KImo0KPLgsnc/zN7GPdce8Cnn0SWvwZO7g== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/primitive" "1.0.1" + "@radix-ui/react-compose-refs" "1.0.1" + "@radix-ui/react-primitive" "1.0.3" + "@radix-ui/react-use-callback-ref" "1.0.1" + "@radix-ui/react-use-escape-keydown" "1.0.3" + +"@radix-ui/react-focus-guards@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-guards/-/react-focus-guards-1.0.1.tgz#1ea7e32092216b946397866199d892f71f7f98ad" + integrity sha512-Rect2dWbQ8waGzhMavsIbmSVCgYxkXLxxR3ZvCX79JOglzdEy4JXMb98lq4hPxUbLr77nP0UOGf4rcMU+s1pUA== + dependencies: + "@babel/runtime" "^7.13.10" + +"@radix-ui/react-focus-scope@1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-scope/-/react-focus-scope-1.0.4.tgz#2ac45fce8c5bb33eb18419cdc1905ef4f1906525" + integrity sha512-sL04Mgvf+FmyvZeYfNu1EPAaaxD+aw7cYeIB9L9Fvq8+urhltTRaEo5ysKOpHuKPclsZcSUMKlN05x4u+CINpA== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-compose-refs" "1.0.1" + "@radix-ui/react-primitive" "1.0.3" + "@radix-ui/react-use-callback-ref" "1.0.1" + "@radix-ui/react-icons@^1.3.0": version "1.3.0" resolved "https://registry.yarnpkg.com/@radix-ui/react-icons/-/react-icons-1.3.0.tgz#c61af8f323d87682c5ca76b856d60c2312dbcb69" @@ -638,6 +688,22 @@ "@babel/runtime" "^7.13.10" "@radix-ui/react-use-layout-effect" "1.0.1" +"@radix-ui/react-label@^2.0.2": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@radix-ui/react-label/-/react-label-2.0.2.tgz#9c72f1d334aac996fdc27b48a8bdddd82108fb6d" + integrity sha512-N5ehvlM7qoTLx7nWPodsPYPgMzA5WM8zZChQg8nyFJKnDO5WHdba1vv5/H6IO5LtJMfD2Q3wh1qHFGNtK0w3bQ== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-primitive" "1.0.3" + +"@radix-ui/react-portal@1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@radix-ui/react-portal/-/react-portal-1.0.4.tgz#df4bfd353db3b1e84e639e9c63a5f2565fb00e15" + integrity sha512-Qki+C/EuGUVCQTOTD5vzJzJuMUlewbzuKyUy+/iHM2uwGiru9gZeBJtHAPKAEkB5KWGi9mP/CHKcY0wt1aW45Q== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-primitive" "1.0.3" + "@radix-ui/react-presence@1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@radix-ui/react-presence/-/react-presence-1.0.1.tgz#491990ba913b8e2a5db1b06b203cb24b5cdef9ba" @@ -687,6 +753,20 @@ "@babel/runtime" "^7.13.10" "@radix-ui/react-compose-refs" "1.0.1" +"@radix-ui/react-switch@^1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@radix-ui/react-switch/-/react-switch-1.0.3.tgz#6119f16656a9eafb4424c600fdb36efa5ec5837e" + integrity sha512-mxm87F88HyHztsI7N+ZUmEoARGkC22YVW5CaC+Byc+HRpuvCrOBPTAnXgf+tZ/7i0Sg/eOePGdMhUKhPaQEqow== + 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-primitive" "1.0.3" + "@radix-ui/react-use-controllable-state" "1.0.1" + "@radix-ui/react-use-previous" "1.0.1" + "@radix-ui/react-use-size" "1.0.1" + "@radix-ui/react-tabs@^1.0.4": version "1.0.4" resolved "https://registry.yarnpkg.com/@radix-ui/react-tabs/-/react-tabs-1.0.4.tgz#993608eec55a5d1deddd446fa9978d2bc1053da2" @@ -717,6 +797,14 @@ "@babel/runtime" "^7.13.10" "@radix-ui/react-use-callback-ref" "1.0.1" +"@radix-ui/react-use-escape-keydown@1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.0.3.tgz#217b840c250541609c66f67ed7bab2b733620755" + integrity sha512-vyL82j40hcFicA+M4Ex7hVkB9vHgSse1ZWomAqV2Je3RleKGO5iM8KMOEtfoSB0PnIelMd2lATjTGMYqN5ylTg== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-use-callback-ref" "1.0.1" + "@radix-ui/react-use-layout-effect@1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.1.tgz#be8c7bc809b0c8934acf6657b577daf948a75399" @@ -724,6 +812,21 @@ dependencies: "@babel/runtime" "^7.13.10" +"@radix-ui/react-use-previous@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-previous/-/react-use-previous-1.0.1.tgz#b595c087b07317a4f143696c6a01de43b0d0ec66" + integrity sha512-cV5La9DPwiQ7S0gf/0qiD6YgNqM5Fk97Kdrlc5yBcrF3jyEZQwm7vYFqMo4IfeHgJXsRaMvLABFtd0OVEmZhDw== + dependencies: + "@babel/runtime" "^7.13.10" + +"@radix-ui/react-use-size@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-size/-/react-use-size-1.0.1.tgz#1c5f5fea940a7d7ade77694bb98116fb49f870b2" + integrity sha512-ibay+VqrgcaI6veAojjofPATwledXiSmX+C0KrBk/xgpX9rBzPV3OsfwlhQdUOFbh+LKQorLYT+xTXW9V8yd0g== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-use-layout-effect" "1.0.1" + "@types/babel__core@^7.20.3": version "7.20.4" resolved "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.4.tgz" @@ -970,6 +1073,13 @@ argparse@^2.0.1: resolved "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz" integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== +aria-hidden@^1.1.1: + version "1.2.3" + resolved "https://registry.yarnpkg.com/aria-hidden/-/aria-hidden-1.2.3.tgz#14aeb7fb692bbb72d69bebfa47279c1fd725e954" + integrity sha512-xcLxITLe2HYa1cnYnwCjkOO1PqUHQpozB8x9AR0OgWN2woOBi5kSDVxKfd0b7sb1hw5qFeJhXm9H1nu3xSfLeQ== + dependencies: + tslib "^2.0.0" + array-union@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz" @@ -1184,6 +1294,11 @@ deep-is@^0.1.3: resolved "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz" integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== +detect-node-es@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/detect-node-es/-/detect-node-es-1.1.0.tgz#163acdf643330caa0b4cd7c21e7ee7755d6fa493" + integrity sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ== + didyoumean@^1.2.2: version "1.2.2" resolved "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz" @@ -1466,6 +1581,11 @@ gensync@^1.0.0-beta.2: resolved "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz" integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== +get-nonce@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/get-nonce/-/get-nonce-1.0.1.tgz#fdf3f0278073820d2ce9426c18f07481b1e0cdf3" + integrity sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q== + glob-parent@^5.1.2, glob-parent@~5.1.2: version "5.1.2" resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz" @@ -1591,6 +1711,13 @@ inherits@2: resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== +invariant@^2.2.4: + version "2.2.4" + resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" + integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA== + dependencies: + loose-envify "^1.0.0" + is-binary-path@~2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz" @@ -1730,7 +1857,7 @@ lodash.pick@^4.4.0: resolved "https://registry.npmjs.org/lodash.pick/-/lodash.pick-4.4.0.tgz" integrity sha512-hXt6Ul/5yWjfklSGvLQl8vM//l3FtyHZeuelpzK6mm99pNvN9yTDruNZPEJZD1oWrqo+izBmB7oUfWgcCX7s4Q== -loose-envify@^1.1.0, loose-envify@^1.4.0: +loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz" integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== @@ -2028,6 +2155,34 @@ react-refresh@^0.14.0: resolved "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz" integrity sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ== +react-remove-scroll-bar@^2.3.3: + version "2.3.4" + resolved "https://registry.yarnpkg.com/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.4.tgz#53e272d7a5cb8242990c7f144c44d8bd8ab5afd9" + integrity sha512-63C4YQBUt0m6ALadE9XV56hV8BgJWDmmTPY758iIJjfQKt2nYwoUrPk0LXRXcB/yIj82T1/Ixfdpdk68LwIB0A== + dependencies: + react-style-singleton "^2.2.1" + tslib "^2.0.0" + +react-remove-scroll@2.5.5: + version "2.5.5" + resolved "https://registry.yarnpkg.com/react-remove-scroll/-/react-remove-scroll-2.5.5.tgz#1e31a1260df08887a8a0e46d09271b52b3a37e77" + integrity sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw== + dependencies: + react-remove-scroll-bar "^2.3.3" + react-style-singleton "^2.2.1" + tslib "^2.1.0" + use-callback-ref "^1.3.0" + use-sidecar "^1.1.2" + +react-style-singleton@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/react-style-singleton/-/react-style-singleton-2.2.1.tgz#f99e420492b2d8f34d38308ff660b60d0b1205b4" + integrity sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g== + dependencies: + get-nonce "^1.0.0" + invariant "^2.2.4" + tslib "^2.0.0" + react@^18.2.0: version "18.2.0" resolved "https://registry.npmjs.org/react/-/react-18.2.0.tgz" @@ -2275,7 +2430,7 @@ ts-interface-checker@^0.1.9: resolved "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz" integrity sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA== -tslib@^2.4.0: +tslib@^2.0.0, tslib@^2.1.0, tslib@^2.4.0: version "2.6.2" resolved "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz" integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== @@ -2327,6 +2482,21 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" +use-callback-ref@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/use-callback-ref/-/use-callback-ref-1.3.0.tgz#772199899b9c9a50526fedc4993fc7fa1f7e32d5" + integrity sha512-3FT9PRuRdbB9HfXhEq35u4oZkvpJ5kuYbpqhCfmiZyReuRgpnhDlbr2ZEnnuS0RrJAPn6l23xjFg9kpDM+Ms7w== + dependencies: + tslib "^2.0.0" + +use-sidecar@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/use-sidecar/-/use-sidecar-1.1.2.tgz#2f43126ba2d7d7e117aa5855e5d8f0276dfe73c2" + integrity sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw== + dependencies: + detect-node-es "^1.1.0" + tslib "^2.0.0" + util-deprecate@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz"