diff --git a/.gitignore b/.gitignore index 39aee32..7b377b1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,13 @@ .aider* +# Project-related files + # Run logs backend/run_logs/* -.env \ No newline at end of file +# Weird Docker setup related files +backend/backend/* + +# Env vars +frontend/.env.local +.env diff --git a/README.md b/README.md index 97bc183..393e027 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # screenshot-to-code -This simple app converts a screenshot to HTML/Tailwind CSS. It uses GPT-4 Vision to generate the code and DALL-E 3 to generate similar-looking images. +This simple app converts a screenshot to code (HTML/Tailwind CSS, or React or Vue or Bootstrap). It uses GPT-4 Vision to generate the code and DALL-E 3 to generate similar-looking images. You can now also enter a URL to clone a live website! https://github.com/abi/screenshot-to-code/assets/23818/6cebadae-2fe3-4986-ac6a-8fb9db030045 @@ -8,15 +8,18 @@ See the [Examples](#examples) section below for more demos. ## 🚀 Try It Out! -🆕 [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](#%EF%B8%8F-faqs) section below for details**). Or see [Getting Started](#-getting-started) below for local install instructions. +🆕 [Try it here](https://screenshottocode.com) (bring your own OpenAI key - **your key must have access to GPT-4 Vision. See [FAQ](#%EF%B8%8F-faqs) section below for details**). Or see [Getting Started](#-getting-started) below for local install instructions. ## 🌟 Recent Updates +- Nov 28 - 🔥 🔥 🔥 Get output code in React or Bootstrap or TailwindCSS +- Nov 23 - Send in a screenshot of the current replicated version (sometimes improves quality of subsequent generations) +- Nov 21 - Edit code in the code editor and preview changes live thanks to [@clean99](https://github.com/clean99) - Nov 20 - Paste in a URL to screenshot and clone (requires [ScreenshotOne free API key](https://screenshotone.com?via=screenshot-to-code)) -- Nov 19 - Support for dark/light code editor theme - thanks https://github.com/kachbit +- Nov 19 - Support for dark/light code editor theme - thanks [@kachbit](https://github.com/kachbit) - 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. It is helpful if the AI messed up some styles or missed a section. +- Nov 15 - You can now instruct the AI to update the code as you wish. It is helpful if the AI messed up some styles or missed a section. ## 🛠 Getting Started @@ -44,6 +47,12 @@ Open http://localhost:5173 to use the app. If you prefer to run the backend on a different port, update VITE_WS_BACKEND_URL in `frontend/.env.local` +For debugging purposes, if you don't want to waste GPT4-Vision credits, you can run the backend in mock mode (which streams a pre-recorded response): + +```bash +MOCK=true poetry run uvicorn main:app --reload --port 7001 +``` + ## Docker If you have Docker installed on your system, in the root directory, run: @@ -58,19 +67,17 @@ The app will be up and running at http://localhost:5173. Note that you can't dev ## 🙋‍♂️ FAQs - **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 do I get an OpenAI API key?** See https://github.com/abi/screenshot-to-code/blob/main/Troubleshooting.md - **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 **NYTimes** -| Original | Replica | -| -------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | +| Original | Replica | +| --------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Screenshot 2023-11-20 at 12 54 03 PM | Screenshot 2023-11-20 at 12 59 56 PM | - - **Instagram page (with not Taylor Swift pics)** https://github.com/abi/screenshot-to-code/assets/23818/503eb86a-356e-4dfc-926a-dabdb1ac7ba1 @@ -81,4 +88,6 @@ https://github.com/abi/screenshot-to-code/assets/23818/3fec0f77-44e8-4fb3-a769-a ## 🌍 Hosted Version -🆕 [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](#%EF%B8%8F-faqs) section for details**). Or see [Getting Started](#-getting-started) for local install instructions. +🆕 [Try it here](https://screenshottocode.com) (bring your own OpenAI key - **your key must have access to GPT-4 Vision. See [FAQ](#%EF%B8%8F-faqs) section for details**). Or see [Getting Started](#-getting-started) for local install instructions. + +[!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/abiraja) diff --git a/Troubleshooting.md b/Troubleshooting.md new file mode 100644 index 0000000..20fa815 --- /dev/null +++ b/Troubleshooting.md @@ -0,0 +1,17 @@ +### Getting an OpenAI API key with GPT4-Vision model access + +You don't need a ChatGPT Pro account. Screenshot to code uses API keys from your OpenAI developer account. In order to get access to the GPT4 Vision model, log into your OpenAI account and then, follow these instructions: + +1. Open [OpenAI Dashboard](https://platform.openai.com/) +1. Go to Settings > Billing +1. Click at the Add payment details +285636868-c80deb92-ab47-45cd-988f-deee67fbd44d +4. You have to buy some credits. The minimum is $5. + +5. Go to Settings > Limits and check at the bottom of the page, your current tier has to be "Tier 1" to have GPT4 access +285636973-da38bd4d-8a78-4904-8027-ca67d729b933 +6. Go to Screenshot to code and paste it in the Settings dialog under OpenAI key (gear icon). Your key is only stored in your browser. Never stored on our servers. + +Some users have also reported that it can take upto 30 minutes after your credit purchase for the GPT4 vision model to be activated. + +If you've followed these steps, and it still doesn't work, feel free to open a Github issue. diff --git a/backend/README.md b/backend/README.md new file mode 100644 index 0000000..7c1cad2 --- /dev/null +++ b/backend/README.md @@ -0,0 +1,3 @@ +Run tests + +pytest test_prompts.py diff --git a/backend/access_token.py b/backend/access_token.py new file mode 100644 index 0000000..e61ef12 --- /dev/null +++ b/backend/access_token.py @@ -0,0 +1,27 @@ +import json +import os +import httpx + + +async def validate_access_token(access_code: str): + async with httpx.AsyncClient() as client: + url = ( + "https://backend.buildpicoapps.com/screenshot_to_code/validate_access_token" + ) + data = json.dumps( + { + "access_code": access_code, + "secret": os.environ.get("PICO_BACKEND_SECRET"), + } + ) + headers = {"Content-Type": "application/json"} + + response = await client.post(url, content=data, headers=headers) + response_data = response.json() + + if response_data["success"]: + print("Access token is valid.") + return True + else: + print(f"Access token validation failed: {response_data['failure_reason']}") + return False diff --git a/backend/main.py b/backend/main.py index bd161c8..fd2e7e2 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1,6 +1,5 @@ # Load environment variables first from dotenv import load_dotenv -from pydantic import BaseModel load_dotenv() @@ -16,6 +15,7 @@ from mock import mock_completion from image_generation import create_alt_url_mapping, generate_images from prompts import assemble_prompt from routes import screenshot +from access_token import validate_access_token app = FastAPI(openapi_url=None, docs_url=None, redoc_url=None) @@ -33,7 +33,8 @@ app.add_middleware( # Useful for debugging purposes when you don't want to waste GPT4-Vision credits # Setting to True will stream a mock response instead of calling the OpenAI API -SHOULD_MOCK_AI_RESPONSE = False +# TODO: Should only be set to true when value is 'True', not any abitrary truthy value +SHOULD_MOCK_AI_RESPONSE = bool(os.environ.get("MOCK", False)) app.include_router(screenshot.router) @@ -59,27 +60,46 @@ def write_logs(prompt_messages, completion): @app.websocket("/generate-code") -async def stream_code_test(websocket: WebSocket): +async def stream_code(websocket: WebSocket): await websocket.accept() + print("Incoming websocket connection...") + params = await websocket.receive_json() + print("Received params") + + # Read the output settings from the request. Fall back to default if not provided. + output_settings = {"css": "tailwind", "js": "vanilla"} + if params["outputSettings"] and params["outputSettings"]["css"]: + output_settings["css"] = params["outputSettings"]["css"] + if params["outputSettings"] and params["outputSettings"]["js"]: + output_settings["js"] = params["outputSettings"]["js"] + print("Using output settings:", output_settings) + # 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") + openai_api_key = None + if "accessCode" in params and params["accessCode"]: + print("Access code - using platform API key") + if await validate_access_token(params["accessCode"]): + openai_api_key = os.environ.get("PLATFORM_OPENAI_API_KEY") + else: + await websocket.send_json( + { + "type": "error", + "value": "Invalid access code or you're out of credits. Please try again.", + } + ) + return else: - openai_api_key = os.environ.get("OPENAI_API_KEY") - if openai_api_key: - print("Using OpenAI API key from environment variable") - if params["openAiBaseURL"]: - openai_base_url = params["openAiBaseURL"] - print("Using OpenAI Base URL from client-side settings dialog") - else: - openai_base_url = os.environ.get("OPENAI_BASE_URL") - if openai_base_url: - print("Using OpenAI Base URL from environment variable") + 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") @@ -90,12 +110,21 @@ async def stream_code_test(websocket: WebSocket): } ) return - # openai_base_url="https://flag.smarttrot.com/v1" + + # Get the OpenAI Base URL from the request. Fall back to environment variable if not provided. + openai_base_url = None + if params["openAiBaseURL"]: + openai_base_url = params["openAiBaseURL"] + print("Using OpenAI Base URL from client-side settings dialog") + else: + openai_base_url = os.environ.get("OPENAI_BASE_URL") + if openai_base_url: + print("Using OpenAI Base URL from environment variable") + if not openai_base_url: - openai_base_url = None print("Using Offical OpenAI Base URL") - + # Get the image generation flag from the request. Fall back to True if not provided. should_generate_images = ( params["isImageGenerationEnabled"] if "isImageGenerationEnabled" in params @@ -108,7 +137,12 @@ async def stream_code_test(websocket: WebSocket): async def process_chunk(content): await websocket.send_json({"type": "chunk", "value": content}) - prompt_messages = assemble_prompt(params["image"]) + if params.get("resultImage") and params["resultImage"]: + prompt_messages = assemble_prompt( + params["image"], output_settings, params["resultImage"] + ) + else: + prompt_messages = assemble_prompt(params["image"], output_settings) # Image cache for updates so that we don't have to regenerate images image_cache = {} @@ -129,7 +163,7 @@ async def stream_code_test(websocket: WebSocket): completion = await stream_openai_response( prompt_messages, api_key=openai_api_key, - base_url = openai_base_url, + base_url=openai_base_url, callback=lambda x: process_chunk(x), ) @@ -142,7 +176,10 @@ async def stream_code_test(websocket: WebSocket): {"type": "status", "value": "Generating images..."} ) updated_html = await generate_images( - completion, api_key=openai_api_key, base_url=openai_base_url, image_cache=image_cache + completion, + api_key=openai_api_key, + base_url=openai_base_url, + image_cache=image_cache, ) else: updated_html = completion diff --git a/backend/prompts.py b/backend/prompts.py index 3761404..f01eb7e 100644 --- a/backend/prompts.py +++ b/backend/prompts.py @@ -1,9 +1,9 @@ -SYSTEM_PROMPT = """ +TAILWIND_SYSTEM_PROMPT = """ You are an expert Tailwind developer You take screenshots of a reference web page from the user, and then build single page apps using Tailwind, HTML and JS. -You might also be given a screenshot of a web page that you have already built, and asked to -update it to look more like the reference image. +You might also be given a screenshot(The second image) of a web page that you have already built, and asked to +update it to look more like the reference image(The first image). - Make sure the app looks exactly like the screenshot. - Pay close attention to background color, text color, font size, font family, @@ -23,25 +23,105 @@ Return only the full code in tags. Do not include markdown "```" or "```html" at the start or end. """ +BOOTSTRAP_SYSTEM_PROMPT = """ +You are an expert Bootstrap developer +You take screenshots of a reference web page from the user, and then build single page apps +using Bootstrap, HTML and JS. +You might also be given a screenshot(The second image) of a web page that you have already built, and asked to +update it to look more like the reference image(The first image). + +- Make sure the app looks exactly like the screenshot. +- Pay close attention to background color, text color, font size, font family, +padding, margin, border, etc. Match the colors and sizes exactly. +- Use the exact text from the screenshot. +- Do not add comments in the code such as "" and "" in place of writing the full code. WRITE THE FULL CODE. +- Repeat elements as needed to match the screenshot. For example, if there are 15 items, the code should have 15 items. DO NOT LEAVE comments like "" or bad things will happen. +- For images, use placeholder images from https://placehold.co and include a detailed description of the image in the alt text so that an image generation AI can generate the image later. + +In terms of libraries, + +- Use this script to include Bootstrap: +- You can use Google Fonts +- Font Awesome for icons: + +Return only the full code in tags. +Do not include markdown "```" or "```html" at the start or end. +""" + +REACT_TAILWIND_SYSTEM_PROMPT = """ +You are an expert React/Tailwind developer +You take screenshots of a reference web page from the user, and then build single page apps +using React and Tailwind CSS. +You might also be given a screenshot(The second image) of a web page that you have already built, and asked to +update it to look more like the reference image(The first image). + +- Make sure the app looks exactly like the screenshot. +- Pay close attention to background color, text color, font size, font family, +padding, margin, border, etc. Match the colors and sizes exactly. +- Use the exact text from the screenshot. +- Do not add comments in the code such as "" and "" in place of writing the full code. WRITE THE FULL CODE. +- Repeat elements as needed to match the screenshot. For example, if there are 15 items, the code should have 15 items. DO NOT LEAVE comments like "" or bad things will happen. +- For images, use placeholder images from https://placehold.co and include a detailed description of the image in the alt text so that an image generation AI can generate the image later. + +In terms of libraries, + +- Use these script to include React so that it can run on a standalone page: + + + +- Use this script to include Tailwind: +- You can use Google Fonts +- Font Awesome for icons: + +Return only the full code in tags. +Do not include markdown "```" or "```html" at the start or end. +""" + USER_PROMPT = """ Generate code for a web page that looks exactly like this. """ -def assemble_prompt(image_data_url): - return [ - {"role": "system", "content": SYSTEM_PROMPT}, +def assemble_prompt(image_data_url, output_settings: dict, result_image_data_url=None): + # Set the system prompt based on the output settings + chosen_prompt_name = "tailwind" + system_content = TAILWIND_SYSTEM_PROMPT + if output_settings["css"] == "bootstrap": + chosen_prompt_name = "bootstrap" + system_content = BOOTSTRAP_SYSTEM_PROMPT + if output_settings["js"] == "react": + chosen_prompt_name = "react-tailwind" + system_content = REACT_TAILWIND_SYSTEM_PROMPT + + print("Using system prompt:", chosen_prompt_name) + + user_content = [ { - "role": "user", - "content": [ - { - "type": "image_url", - "image_url": {"url": image_data_url, "detail": "high"}, - }, - { - "type": "text", - "text": USER_PROMPT, - }, - ], + "type": "image_url", + "image_url": {"url": image_data_url, "detail": "high"}, + }, + { + "type": "text", + "text": USER_PROMPT, + }, + ] + + # Include the result image if it exists + if result_image_data_url: + user_content.insert( + 1, + { + "type": "image_url", + "image_url": {"url": result_image_data_url, "detail": "high"}, + }, + ) + return [ + { + "role": "system", + "content": system_content, + }, + { + "role": "user", + "content": user_content, }, ] diff --git a/backend/test_prompts.py b/backend/test_prompts.py new file mode 100644 index 0000000..2eaaaf4 --- /dev/null +++ b/backend/test_prompts.py @@ -0,0 +1,97 @@ +from prompts import assemble_prompt + +TAILWIND_SYSTEM_PROMPT = """ +You are an expert Tailwind developer +You take screenshots of a reference web page from the user, and then build single page apps +using Tailwind, HTML and JS. +You might also be given a screenshot(The second image) of a web page that you have already built, and asked to +update it to look more like the reference image(The first image). + +- Make sure the app looks exactly like the screenshot. +- Pay close attention to background color, text color, font size, font family, +padding, margin, border, etc. Match the colors and sizes exactly. +- Use the exact text from the screenshot. +- Do not add comments in the code such as "" and "" in place of writing the full code. WRITE THE FULL CODE. +- Repeat elements as needed to match the screenshot. For example, if there are 15 items, the code should have 15 items. DO NOT LEAVE comments like "" or bad things will happen. +- For images, use placeholder images from https://placehold.co and include a detailed description of the image in the alt text so that an image generation AI can generate the image later. + +In terms of libraries, + +- Use this script to include Tailwind: +- You can use Google Fonts +- Font Awesome for icons: + +Return only the full code in tags. +Do not include markdown "```" or "```html" at the start or end. +""" + +BOOTSTRAP_SYSTEM_PROMPT = """ +You are an expert Bootstrap developer +You take screenshots of a reference web page from the user, and then build single page apps +using Bootstrap, HTML and JS. +You might also be given a screenshot(The second image) of a web page that you have already built, and asked to +update it to look more like the reference image(The first image). + +- Make sure the app looks exactly like the screenshot. +- Pay close attention to background color, text color, font size, font family, +padding, margin, border, etc. Match the colors and sizes exactly. +- Use the exact text from the screenshot. +- Do not add comments in the code such as "" and "" in place of writing the full code. WRITE THE FULL CODE. +- Repeat elements as needed to match the screenshot. For example, if there are 15 items, the code should have 15 items. DO NOT LEAVE comments like "" or bad things will happen. +- For images, use placeholder images from https://placehold.co and include a detailed description of the image in the alt text so that an image generation AI can generate the image later. + +In terms of libraries, + +- Use this script to include Bootstrap: +- You can use Google Fonts +- Font Awesome for icons: + +Return only the full code in tags. +Do not include markdown "```" or "```html" at the start or end. +""" + +REACT_TAILWIND_SYSTEM_PROMPT = """ +You are an expert React/Tailwind developer +You take screenshots of a reference web page from the user, and then build single page apps +using React and Tailwind CSS. +You might also be given a screenshot(The second image) of a web page that you have already built, and asked to +update it to look more like the reference image(The first image). + +- Make sure the app looks exactly like the screenshot. +- Pay close attention to background color, text color, font size, font family, +padding, margin, border, etc. Match the colors and sizes exactly. +- Use the exact text from the screenshot. +- Do not add comments in the code such as "" and "" in place of writing the full code. WRITE THE FULL CODE. +- Repeat elements as needed to match the screenshot. For example, if there are 15 items, the code should have 15 items. DO NOT LEAVE comments like "" or bad things will happen. +- For images, use placeholder images from https://placehold.co and include a detailed description of the image in the alt text so that an image generation AI can generate the image later. + +In terms of libraries, + +- Use these script to include React so that it can run on a standalone page: + + + +- Use this script to include Tailwind: +- You can use Google Fonts +- Font Awesome for icons: + +Return only the full code in tags. +Do not include markdown "```" or "```html" at the start or end. +""" + + +def test_prompts(): + tailwind_prompt = assemble_prompt( + "image_data_url", {"css": "tailwind", "js": "vanilla"}, "result_image_data_url" + ) + assert tailwind_prompt[0]["content"] == TAILWIND_SYSTEM_PROMPT + + bootstrap_prompt = assemble_prompt( + "image_data_url", {"css": "bootstrap", "js": "vanilla"}, "result_image_data_url" + ) + assert bootstrap_prompt[0]["content"] == BOOTSTRAP_SYSTEM_PROMPT + + react_tailwind_prompt = assemble_prompt( + "image_data_url", {"css": "tailwind", "js": "react"}, "result_image_data_url" + ) + assert react_tailwind_prompt[0]["content"] == REACT_TAILWIND_SYSTEM_PROMPT diff --git a/frontend/.gitignore b/frontend/.gitignore index a547bf3..17ceca3 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -22,3 +22,6 @@ dist-ssr *.njsproj *.sln *.sw? + +# Env files +.env* diff --git a/frontend/index.html b/frontend/index.html index 0d85bb4..2a7fa0e 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -17,7 +17,38 @@ rel="stylesheet" /> + + <%- injectHead %> + Screenshot to Code + + + + + + + + + + + + + + + +
diff --git a/frontend/package.json b/frontend/package.json index 7a34ab2..476a72d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -5,15 +5,22 @@ "type": "module", "scripts": { "dev": "vite", + "dev-hosted": "vite --mode prod", "build": "tsc && vite build", + "build-hosted": "tsc && vite build --mode prod", "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "preview": "vite preview" }, "dependencies": { "@codemirror/lang-html": "^6.4.6", + "@radix-ui/react-accordion": "^1.1.2", + "@radix-ui/react-alert-dialog": "^1.0.5", + "@radix-ui/react-checkbox": "^1.0.4", "@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-label": "^2.0.2", + "@radix-ui/react-popover": "^1.0.7", + "@radix-ui/react-select": "^2.0.0", "@radix-ui/react-separator": "^1.0.3", "@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-switch": "^1.0.3", @@ -22,6 +29,8 @@ "classnames": "^2.3.2", "clsx": "^2.0.0", "codemirror": "^6.0.1", + "copy-to-clipboard": "^3.3.3", + "html2canvas": "^1.4.1", "react": "^18.2.0", "react-dom": "^18.2.0", "react-dropzone": "^14.2.3", @@ -46,7 +55,8 @@ "postcss": "^8.4.31", "tailwindcss": "^3.3.5", "typescript": "^5.0.2", - "vite": "^4.4.5" + "vite": "^4.4.5", + "vite-plugin-html": "^3.2.0" }, "engines": { "node": ">=14.18.0" diff --git a/frontend/public/brand/twitter-summary-card.png b/frontend/public/brand/twitter-summary-card.png new file mode 100644 index 0000000..6fcedbf Binary files /dev/null and b/frontend/public/brand/twitter-summary-card.png differ diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 7e89041..ed416da 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useRef, useState } from "react"; import ImageUpload from "./components/ImageUpload"; import CodePreview from "./components/CodePreview"; import Preview from "./components/Preview"; @@ -12,23 +12,33 @@ import { FaMobile, FaUndo, } from "react-icons/fa"; + +import { Switch } from "./components/ui/switch"; 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 { + Settings, + EditorTheme, + AppState, + CSSOption, + OutputSettings, + JSFrameworkOption, +} from "./types"; import { IS_RUNNING_ON_CLOUD } from "./config"; import { PicoBadge } from "./components/PicoBadge"; import { OnboardingNote } from "./components/OnboardingNote"; import { usePersistedState } from "./hooks/usePersistedState"; import { UrlInputSection } from "./components/UrlInputSection"; import TermsOfServiceDialog from "./components/TermsOfServiceDialog"; +import html2canvas from "html2canvas"; +import { USER_CLOSE_WEB_SOCKET_CODE } from "./constants"; +import CodeTab from "./components/CodeTab"; +import OutputSettingsSection from "./components/OutputSettingsSection"; function App() { - const [appState, setAppState] = useState<"INITIAL" | "CODING" | "CODE_READY">( - "INITIAL" - ); + const [appState, setAppState] = useState(AppState.INITIAL); const [generatedCode, setGeneratedCode] = useState(""); const [referenceImages, setReferenceImages] = useState([]); const [executionConsole, setExecutionConsole] = useState([]); @@ -40,10 +50,33 @@ function App() { openAiBaseURL: null, screenshotOneApiKey: null, isImageGenerationEnabled: true, - editorTheme: "cobalt", + editorTheme: EditorTheme.COBALT, + isTermOfServiceAccepted: false, + accessCode: null, }, "setting" ); + const [outputSettings, setOutputSettings] = useState({ + css: CSSOption.TAILWIND, + js: JSFrameworkOption.NO_FRAMEWORK, + }); + const [shouldIncludeResultImage, setShouldIncludeResultImage] = + useState(false); + + const wsRef = useRef(null); + + const takeScreenshot = async (): Promise => { + const iframeElement = document.querySelector( + "#preview-desktop" + ) as HTMLIFrameElement; + if (!iframeElement?.contentWindow?.document.body) { + return ""; + } + + const canvas = await html2canvas(iframeElement.contentWindow.document.body); + const png = canvas.toDataURL("image/png"); + return png; + }; const downloadCode = () => { // Create a blob from the generated code @@ -63,31 +96,41 @@ function App() { }; const reset = () => { - setAppState("INITIAL"); + setAppState(AppState.INITIAL); setGeneratedCode(""); setReferenceImages([]); setExecutionConsole([]); setHistory([]); }; + const stop = () => { + wsRef.current?.close?.(USER_CLOSE_WEB_SOCKET_CODE); + // make sure stop can correct the state even if the websocket is already closed + setAppState(AppState.CODE_READY); + }; + function doGenerateCode(params: CodeGenerationParams) { setExecutionConsole([]); - setAppState("CODING"); + setAppState(AppState.CODING); // Merge settings with params - const updatedParams = { ...params, ...settings }; + const updatedParams = { ...params, ...settings, outputSettings }; generateCode( + wsRef, updatedParams, (token) => setGeneratedCode((prev) => prev + token), (code) => setGeneratedCode(code), (line) => setExecutionConsole((prev) => [...prev, line]), - () => setAppState("CODE_READY") + () => setAppState(AppState.CODE_READY) ); } // Initial version creation function doCreate(referenceImages: string[]) { + // Reset any existing state + reset(); + setReferenceImages(referenceImages); if (referenceImages.length > 0) { doGenerateCode({ @@ -98,53 +141,88 @@ function App() { } // Subsequent updates - function doUpdate() { + async function doUpdate() { const updatedHistory = [...history, generatedCode, updateInstruction]; - - doGenerateCode({ - generationType: "update", - image: referenceImages[0], - history: updatedHistory, - }); + if (shouldIncludeResultImage) { + const resultImage = await takeScreenshot(); + doGenerateCode({ + generationType: "update", + image: referenceImages[0], + resultImage: resultImage, + history: updatedHistory, + }); + } else { + doGenerateCode({ + generationType: "update", + image: referenceImages[0], + history: updatedHistory, + }); + } setHistory(updatedHistory); setGeneratedCode(""); setUpdateInstruction(""); } - return ( -
- {IS_RUNNING_ON_CLOUD && } - {IS_RUNNING_ON_CLOUD && } + const handleTermDialogOpenChange = (open: boolean) => { + setSettings((s) => ({ + ...s, + isTermOfServiceAccepted: !open, + })); + }; -
-
-
+ return ( +
+ {IS_RUNNING_ON_CLOUD && } + {IS_RUNNING_ON_CLOUD && ( + + )} +
+
+

Screenshot to Code

- {appState === "INITIAL" && ( -

- Drag & drop a screenshot to get started. -

- )} - {IS_RUNNING_ON_CLOUD && !settings.openAiApiKey && } + - {(appState === "CODING" || appState === "CODE_READY") && ( + {IS_RUNNING_ON_CLOUD && + !(settings.openAiApiKey || settings.accessCode) && ( + + )} + + {(appState === AppState.CODING || + appState === AppState.CODE_READY) && ( <> {/* Show code preview only when coding */} - {appState === "CODING" && ( + {appState === AppState.CODING && (
{executionConsole.slice(-1)[0]}
+
+ +
)} - {appState === "CODE_READY" && ( + {appState === AppState.CODE_READY && (