diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..d6e2638 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "python.analysis.typeCheckingMode": "strict" +} diff --git a/README.md b/README.md index e4739a4..bc82497 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # screenshot-to-code -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! +This simple app converts a screenshot to code (HTML/Tailwind CSS, or React or Bootstrap or Vue). 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 @@ -12,6 +12,7 @@ See the [Examples](#-examples) section below for more demos. ## 🌟 Recent Updates +- Dec 11 - Start a new project from existing code (allows you to come back to an older project) - Dec 7 - 🔥 🔥 🔥 View a history of your edits, and branch off them - Nov 30 - Dark mode, output code in Ionic (thanks [@dialmedu](https://github.com/dialmedu)), set OpenAI base URL - Nov 28 - 🔥 🔥 🔥 Customize your stack: React or Bootstrap or TailwindCSS @@ -37,6 +38,12 @@ poetry shell poetry run uvicorn main:app --reload --port 7001 ``` +You can also run the backend (when you're in `backend`): + +```bash +poetry run pyright +``` + Run the frontend: ```bash @@ -57,7 +64,7 @@ MOCK=true poetry run uvicorn main:app --reload --port 7001 ## Configuration -* You can configure the OpenAI base URL if you need to use a proxy: Set OPENAI_BASE_URL in the `backend/.env` or directly in the UI in the settings dialog +- You can configure the OpenAI base URL if you need to use a proxy: Set OPENAI_BASE_URL in the `backend/.env` or directly in the UI in the settings dialog ## Docker diff --git a/backend/.gitignore b/backend/.gitignore index d9005f2..a42aad3 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -150,3 +150,7 @@ cython_debug/ # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ + + +# Temporary eval output +evals_data diff --git a/backend/README.md b/backend/README.md index 7c1cad2..155bf46 100644 --- a/backend/README.md +++ b/backend/README.md @@ -1,3 +1,7 @@ -Run tests +# Run the type checker -pytest test_prompts.py +poetry run pyright + +# Run tests + +poetry run pytest diff --git a/backend/config.py b/backend/config.py new file mode 100644 index 0000000..7dd21f9 --- /dev/null +++ b/backend/config.py @@ -0,0 +1,11 @@ +# 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 +# TODO: Should only be set to true when value is 'True', not any abitrary truthy value +import os + + +SHOULD_MOCK_AI_RESPONSE = bool(os.environ.get("MOCK", False)) + +# Set to True when running in production (on the hosted version) +# Used as a feature flag to enable or disable certain features +IS_PROD = os.environ.get("IS_PROD", False) diff --git a/backend/evals/__init__.py b/backend/evals/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/evals/config.py b/backend/evals/config.py new file mode 100644 index 0000000..7643027 --- /dev/null +++ b/backend/evals/config.py @@ -0,0 +1 @@ +EVALS_DIR = "./evals_data" diff --git a/backend/evals/core.py b/backend/evals/core.py new file mode 100644 index 0000000..61db1a3 --- /dev/null +++ b/backend/evals/core.py @@ -0,0 +1,29 @@ +import os + +from llm import stream_openai_response +from prompts import assemble_prompt +from prompts.types import Stack +from utils import pprint_prompt + + +async def generate_code_core(image_url: str, stack: Stack) -> str: + prompt_messages = assemble_prompt(image_url, stack) + openai_api_key = os.environ.get("OPENAI_API_KEY") + openai_base_url = None + + pprint_prompt(prompt_messages) + + async def process_chunk(content: str): + pass + + if not openai_api_key: + raise Exception("OpenAI API key not found") + + completion = await stream_openai_response( + prompt_messages, + api_key=openai_api_key, + base_url=openai_base_url, + callback=lambda x: process_chunk(x), + ) + + return completion diff --git a/backend/evals/utils.py b/backend/evals/utils.py new file mode 100644 index 0000000..6a52f88 --- /dev/null +++ b/backend/evals/utils.py @@ -0,0 +1,7 @@ +import base64 + + +async def image_to_data_url(filepath: str): + with open(filepath, "rb") as image_file: + encoded_string = base64.b64encode(image_file.read()).decode() + return f"data:image/png;base64,{encoded_string}" diff --git a/backend/image_generation.py b/backend/image_generation.py index bb272f8..d3e71b1 100644 --- a/backend/image_generation.py +++ b/backend/image_generation.py @@ -1,15 +1,15 @@ import asyncio -import os import re +from typing import Dict, List, Union from openai import AsyncOpenAI from bs4 import BeautifulSoup -async def process_tasks(prompts, api_key, base_url): +async def process_tasks(prompts: List[str], api_key: str, base_url: str): tasks = [generate_image(prompt, api_key, base_url) for prompt in prompts] results = await asyncio.gather(*tasks, return_exceptions=True) - processed_results = [] + processed_results: List[Union[str, None]] = [] for result in results: if isinstance(result, Exception): print(f"An exception occurred: {result}") @@ -20,9 +20,9 @@ async def process_tasks(prompts, api_key, base_url): return processed_results -async def generate_image(prompt, api_key, base_url): +async def generate_image(prompt: str, api_key: str, base_url: str): client = AsyncOpenAI(api_key=api_key, base_url=base_url) - image_params = { + image_params: Dict[str, Union[str, int]] = { "model": "dall-e-3", "quality": "standard", "style": "natural", @@ -35,7 +35,7 @@ async def generate_image(prompt, api_key, base_url): return res.data[0].url -def extract_dimensions(url): +def extract_dimensions(url: str): # Regular expression to match numbers in the format '300x200' matches = re.findall(r"(\d+)x(\d+)", url) @@ -48,11 +48,11 @@ def extract_dimensions(url): return (100, 100) -def create_alt_url_mapping(code): +def create_alt_url_mapping(code: str) -> Dict[str, str]: soup = BeautifulSoup(code, "html.parser") images = soup.find_all("img") - mapping = {} + mapping: Dict[str, str] = {} for image in images: if not image["src"].startswith("https://placehold.co"): @@ -61,7 +61,9 @@ def create_alt_url_mapping(code): return mapping -async def generate_images(code, api_key, base_url, image_cache): +async def generate_images( + code: str, api_key: str, base_url: Union[str, None], image_cache: Dict[str, str] +): # Find all images soup = BeautifulSoup(code, "html.parser") images = soup.find_all("img") diff --git a/backend/llm.py b/backend/llm.py index e2b41c4..66e3a47 100644 --- a/backend/llm.py +++ b/backend/llm.py @@ -1,16 +1,16 @@ -import os -from typing import Awaitable, Callable +from typing import Awaitable, Callable, List from openai import AsyncOpenAI +from openai.types.chat import ChatCompletionMessageParam, ChatCompletionChunk MODEL_GPT_4_VISION = "gpt-4-vision-preview" async def stream_openai_response( - messages, + messages: List[ChatCompletionMessageParam], api_key: str, base_url: str | None, callback: Callable[[str], Awaitable[None]], -): +) -> str: client = AsyncOpenAI(api_key=api_key, base_url=base_url) model = MODEL_GPT_4_VISION @@ -23,9 +23,10 @@ async def stream_openai_response( params["max_tokens"] = 4096 params["temperature"] = 0 - completion = await client.chat.completions.create(**params) + stream = await client.chat.completions.create(**params) # type: ignore full_response = "" - async for chunk in completion: + async for chunk in stream: # type: ignore + assert isinstance(chunk, ChatCompletionChunk) content = chunk.choices[0].delta.content or "" full_response += content await callback(content) diff --git a/backend/main.py b/backend/main.py index fe80dba..7a1797f 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1,26 +1,12 @@ # Load environment variables first from dotenv import load_dotenv - load_dotenv() -import json -import os -import traceback -from datetime import datetime -from fastapi import FastAPI, WebSocket -from fastapi.staticfiles import StaticFiles +from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware -from fastapi.responses import FileResponse, HTMLResponse -import openai -from llm import stream_openai_response -from mock import mock_completion -from utils import pprint_prompt -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 +from routes import screenshot, generate_code, home, evals app = FastAPI(openapi_url=None, docs_url=None, redoc_url=None) @@ -33,247 +19,8 @@ app.add_middleware( allow_headers=["*"], ) - -# 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 -# 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)) - -# Set to True when running in production (on the hosted version) -# Used as a feature flag to enable or disable certain features -IS_PROD = os.environ.get("IS_PROD", False) - -# if webui folder exists, we are in build mode -IS_BUILD = os.path.exists("webui") - - +# Add routes +app.include_router(generate_code.router) app.include_router(screenshot.router) - - -@app.get("/") -async def get_status(): - if IS_BUILD: - # if in build mode, return webui/index.html - return FileResponse("webui/index.html") - - return HTMLResponse( - content="

Your backend is running correctly. Please open the front-end URL (default is http://localhost:5173) to use screenshot-to-code.

" - ) - - -def write_logs(prompt_messages, completion): - # Get the logs path from environment, default to the current working directory - logs_path = os.environ.get("LOGS_PATH", os.getcwd()) - - # 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: - f.write(json.dumps({"prompt": prompt_messages, "completion": completion})) - - -@app.websocket("/generate-code") -async def stream_code(websocket: WebSocket): - await websocket.accept() - - print("Incoming websocket connection...") - - async def throw_error( - message: str, - ): - await websocket.send_json({"type": "error", "value": message}) - await websocket.close() - - params = await websocket.receive_json() - - print("Received params") - - # Read the code config settings from the request. Fall back to default if not provided. - generated_code_config = "" - if "generatedCodeConfig" in params and params["generatedCodeConfig"]: - generated_code_config = params["generatedCodeConfig"] - print(f"Generating {generated_code_config} code") - - # Get the OpenAI API key from the request. Fall back to environment variable if not provided. - # If neither is provided, we throw an error. - openai_api_key = None - if "accessCode" in params and params["accessCode"]: - print("Access code - using platform API key") - res = await validate_access_token(params["accessCode"]) - if res["success"]: - openai_api_key = os.environ.get("PLATFORM_OPENAI_API_KEY") - else: - await websocket.send_json( - { - "type": "error", - "value": res["failure_reason"], - } - ) - return - else: - 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 - - # Get the OpenAI Base URL from the request. Fall back to environment variable if not provided. - openai_base_url = None - # Disable user-specified OpenAI Base URL in prod - if not os.environ.get("IS_PROD"): - if "openAiBaseURL" in params and 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: - print("Using official OpenAI 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 - else True - ) - - print("generating code...") - await websocket.send_json({"type": "status", "value": "Generating code..."}) - - async def process_chunk(content): - await websocket.send_json({"type": "chunk", "value": content}) - - # Assemble the prompt - try: - if params.get("resultImage") and params["resultImage"]: - prompt_messages = assemble_prompt( - params["image"], generated_code_config, params["resultImage"] - ) - else: - prompt_messages = assemble_prompt(params["image"], generated_code_config) - except: - await websocket.send_json( - { - "type": "error", - "value": "Error assembling prompt. Contact support at support@picoapps.xyz", - } - ) - await websocket.close() - return - - # Image cache for updates so that we don't have to regenerate images - image_cache = {} - - if params["generationType"] == "update": - # Transform into message format - # TODO: Move this to frontend - for index, text in enumerate(params["history"]): - prompt_messages += [ - {"role": "assistant" if index % 2 == 0 else "user", "content": text} - ] - image_cache = create_alt_url_mapping(params["history"][-2]) - - if SHOULD_MOCK_AI_RESPONSE: - completion = await mock_completion(process_chunk) - else: - try: - completion = await stream_openai_response( - prompt_messages, - api_key=openai_api_key, - base_url=openai_base_url, - callback=lambda x: process_chunk(x), - ) - except openai.AuthenticationError as e: - print("[GENERATE_CODE] Authentication failed", e) - error_message = ( - "Incorrect OpenAI key. Please make sure your OpenAI API key is correct, or create a new OpenAI API key on your OpenAI dashboard." - + ( - " Alternatively, you can purchase code generation credits directly on this website." - if IS_PROD - else "" - ) - ) - return await throw_error(error_message) - except openai.NotFoundError as e: - print("[GENERATE_CODE] Model not found", e) - error_message = ( - e.message - + ". Please make sure you have followed the instructions correctly to obtain an OpenAI key with GPT vision access: https://github.com/abi/screenshot-to-code/blob/main/Troubleshooting.md" - + ( - " Alternatively, you can purchase code generation credits directly on this website." - if IS_PROD - else "" - ) - ) - return await throw_error(error_message) - except openai.RateLimitError as e: - print("[GENERATE_CODE] Rate limit exceeded", e) - error_message = ( - "OpenAI error - 'You exceeded your current quota, please check your plan and billing details.'" - + ( - " Alternatively, you can purchase code generation credits directly on this website." - if IS_PROD - else "" - ) - ) - return await throw_error(error_message) - - # Write the messages dict into a log so that we can debug later - write_logs(prompt_messages, completion) - - try: - if should_generate_images: - await websocket.send_json( - {"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, - ) - else: - updated_html = completion - await websocket.send_json({"type": "setCode", "value": updated_html}) - await websocket.send_json( - {"type": "status", "value": "Code generation complete."} - ) - except Exception as e: - traceback.print_exc() - print("Image generation failed", e) - await websocket.send_json( - {"type": "status", "value": "Image generation failed but code is complete."} - ) - - await websocket.close() - - -# Serve the webui folder as static files -if IS_BUILD: - app.mount("/", StaticFiles(directory="webui", html=True), name="webui") - -if __name__ == "__main__": - import uvicorn - uvicorn.run(app, host="0.0.0.0", port=8000) \ No newline at end of file +app.include_router(home.router) +app.include_router(evals.router) \ No newline at end of file diff --git a/backend/mock.py b/backend/mock_llm.py similarity index 98% rename from backend/mock.py rename to backend/mock_llm.py index 90dc7d3..0102bad 100644 --- a/backend/mock.py +++ b/backend/mock_llm.py @@ -1,7 +1,8 @@ import asyncio +from typing import Awaitable, Callable -async def mock_completion(process_chunk): +async def mock_completion(process_chunk: Callable[[str], Awaitable[None]]) -> str: code_to_return = NO_IMAGES_NYTIMES_MOCK_CODE for i in range(0, len(code_to_return), 10): diff --git a/backend/poetry.lock b/backend/poetry.lock index dbf2122..0086f7d 100644 --- a/backend/poetry.lock +++ b/backend/poetry.lock @@ -200,19 +200,31 @@ files = [ ] [[package]] -name = "macholib" -version = "1.16.3" -description = "Mach-O header analysis and editing" +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +category = "dev" optional = false -python-versions = "*" +python-versions = ">=3.7" files = [ - {file = "macholib-1.16.3-py2.py3-none-any.whl", hash = "sha256:0e315d7583d38b8c77e815b1ecbdbf504a8258d8b3e17b61165c6feb60d18f2c"}, - {file = "macholib-1.16.3.tar.gz", hash = "sha256:07ae9e15e8e4cd9a788013d81f5908b3609aa76f9b1421bae9c4d7606ec86a30"}, + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "nodeenv" +version = "1.8.0" +description = "Node.js virtual environment builder" +category = "dev" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" +files = [ + {file = "nodeenv-1.8.0-py2.py3-none-any.whl", hash = "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"}, + {file = "nodeenv-1.8.0.tar.gz", hash = "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2"}, ] [package.dependencies] -altgraph = ">=0.17" - +setuptools = "*" [[package]] name = "openai" version = "1.3.7" @@ -240,6 +252,7 @@ datalib = ["numpy (>=1)", "pandas (>=1.2.3)", "pandas-stubs (>=1.1.0.11)"] name = "packaging" version = "23.2" description = "Core utilities for Python packages" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -248,16 +261,21 @@ files = [ ] [[package]] -name = "pefile" -version = "2023.2.7" -description = "Python PE parsing module" +name = "pluggy" +version = "1.3.0" +description = "plugin and hook calling mechanisms for python" +category = "dev" optional = false -python-versions = ">=3.6.0" +python-versions = ">=3.8" files = [ - {file = "pefile-2023.2.7-py3-none-any.whl", hash = "sha256:da185cd2af68c08a6cd4481f7325ed600a88f6a813bad9dea07ab3ef73d8d8d6"}, - {file = "pefile-2023.2.7.tar.gz", hash = "sha256:82e6114004b3d6911c77c3953e3838654b04511b8b66e8583db70c65998017dc"}, + {file = "pluggy-1.3.0-py3-none-any.whl", hash = "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"}, + {file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"}, ] +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + [[package]] name = "pydantic" version = "1.10.13" @@ -354,7 +372,6 @@ files = [ {file = "pyinstaller-hooks-contrib-2023.10.tar.gz", hash = "sha256:4b4a998036abb713774cb26534ca06b7e6e09e4c628196017a10deb11a48747f"}, {file = "pyinstaller_hooks_contrib-2023.10-py2.py3-none-any.whl", hash = "sha256:6dc1786a8f452941245d5bb85893e2a33632ebdcbc4c23eea41f2ee08281b0c0"}, ] - [[package]] name = "python-dotenv" version = "1.0.0" @@ -435,6 +452,18 @@ anyio = ">=3.4.0,<5" [package.extras] full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart", "pyyaml"] +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] + [[package]] name = "tqdm" version = "4.66.1" @@ -468,13 +497,13 @@ files = [ [[package]] name = "uvicorn" -version = "0.24.0.post1" +version = "0.25.0" description = "The lightning-fast ASGI server." optional = false python-versions = ">=3.8" files = [ - {file = "uvicorn-0.24.0.post1-py3-none-any.whl", hash = "sha256:7c84fea70c619d4a710153482c0d230929af7bcf76c7bfa6de151f0a3a80121e"}, - {file = "uvicorn-0.24.0.post1.tar.gz", hash = "sha256:09c8e5a79dc466bdf28dead50093957db184de356fcdc48697bad3bde4c2588e"}, + {file = "uvicorn-0.25.0-py3-none-any.whl", hash = "sha256:ce107f5d9bd02b4636001a77a4e74aab5e1e2b146868ebbad565237145af444c"}, + {file = "uvicorn-0.25.0.tar.gz", hash = "sha256:6dddbad1d7ee0f5140aba5ec138ddc9612c5109399903828b4874c9937f009c2"}, ] [package.dependencies] diff --git a/backend/prompts/__init__.py b/backend/prompts/__init__.py new file mode 100644 index 0000000..4f2e329 --- /dev/null +++ b/backend/prompts/__init__.py @@ -0,0 +1,79 @@ +from typing import List, NoReturn, Union + +from openai.types.chat import ChatCompletionMessageParam, ChatCompletionContentPartParam + +from prompts.imported_code_prompts import IMPORTED_CODE_SYSTEM_PROMPTS +from prompts.screenshot_system_prompts import SYSTEM_PROMPTS +from prompts.types import Stack + + +USER_PROMPT = """ +Generate code for a web page that looks exactly like this. +""" + +SVG_USER_PROMPT = """ +Generate code for a SVG that looks exactly like this. +""" + + +def assemble_imported_code_prompt( + code: str, stack: Stack, result_image_data_url: Union[str, None] = None +) -> List[ChatCompletionMessageParam]: + system_content = IMPORTED_CODE_SYSTEM_PROMPTS[stack] + + user_content = ( + "Here is the code of the app: " + code + if stack != "svg" + else "Here is the code of the SVG: " + code + ) + return [ + { + "role": "system", + "content": system_content, + }, + { + "role": "user", + "content": user_content, + }, + ] + # TODO: Use result_image_data_url + + +def assemble_prompt( + image_data_url: str, + stack: Stack, + result_image_data_url: Union[str, None] = None, +) -> List[ChatCompletionMessageParam]: + system_content = SYSTEM_PROMPTS[stack] + user_prompt = USER_PROMPT if stack != "svg" else SVG_USER_PROMPT + + user_content: List[ChatCompletionContentPartParam] = [ + { + "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/prompts/imported_code_prompts.py similarity index 54% rename from backend/test_prompts.py rename to backend/prompts/imported_code_prompts.py index 5d8cd88..8babf78 100644 --- a/backend/test_prompts.py +++ b/backend/prompts/imported_code_prompts.py @@ -1,18 +1,11 @@ -from prompts import assemble_prompt +from prompts.types import SystemPrompts -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. +IMPORTED_CODE_TAILWIND_SYSTEM_PROMPT = """ +You are an expert Tailwind developer. + - 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. +- Repeat elements as needed. 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, @@ -25,44 +18,11 @@ 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 = """ +IMPORTED_CODE_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. +- Repeat elements as needed. 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, @@ -79,19 +39,28 @@ Return only the full code in tags. Do not include markdown "```" or "```html" at the start or end. """ -IONIC_TAILWIND_SYSTEM_PROMPT = """ -You are an expert Ionic/Tailwind developer -You take screenshots of a reference web page from the user, and then build single page apps -using Ionic 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). +IMPORTED_CODE_BOOTSTRAP_SYSTEM_PROMPT = """ +You are an expert Bootstrap developer. -- 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. +- Repeat elements as needed. 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. +""" + +IMPORTED_CODE_IONIC_TAILWIND_SYSTEM_PROMPT = """ +You are an expert Ionic/Tailwind developer. + +- 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. 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, @@ -113,24 +82,55 @@ Return only the full code in tags. Do not include markdown "```" or "```html" at the start or end. """ +IMPORTED_CODE_VUE_TAILWIND_SYSTEM_PROMPT = """ +You are an expert Vue/Tailwind developer. -def test_prompts(): - tailwind_prompt = assemble_prompt( - "image_data_url", "html_tailwind", "result_image_data_url" - ) - assert tailwind_prompt[0]["content"] == TAILWIND_SYSTEM_PROMPT +- 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. 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. - react_tailwind_prompt = assemble_prompt( - "image_data_url", "react_tailwind", "result_image_data_url" - ) - assert react_tailwind_prompt[0]["content"] == REACT_TAILWIND_SYSTEM_PROMPT +In terms of libraries, - bootstrap_prompt = assemble_prompt( - "image_data_url", "bootstrap", "result_image_data_url" - ) - assert bootstrap_prompt[0]["content"] == BOOTSTRAP_SYSTEM_PROMPT +- Use these script to include Vue so that it can run on a standalone page: + +- Use Vue using the global build like so: +
{{ message }}
+ +- Use this script to include Tailwind: +- You can use Google Fonts +- Font Awesome for icons: - ionic_tailwind = assemble_prompt( - "image_data_url", "ionic_tailwind", "result_image_data_url" - ) - assert ionic_tailwind[0]["content"] == IONIC_TAILWIND_SYSTEM_PROMPT +Return only the full code in tags. +Do not include markdown "```" or "```html" at the start or end. +The return result must only include the code.""" + +IMPORTED_CODE_SVG_SYSTEM_PROMPT = """ +You are an expert at building SVGs. + +- 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. +- You can use Google Fonts + +Return only the full code in tags. +Do not include markdown "```" or "```svg" at the start or end. +""" + +IMPORTED_CODE_SYSTEM_PROMPTS = SystemPrompts( + html_tailwind=IMPORTED_CODE_TAILWIND_SYSTEM_PROMPT, + react_tailwind=IMPORTED_CODE_REACT_TAILWIND_SYSTEM_PROMPT, + bootstrap=IMPORTED_CODE_BOOTSTRAP_SYSTEM_PROMPT, + ionic_tailwind=IMPORTED_CODE_IONIC_TAILWIND_SYSTEM_PROMPT, + vue_tailwind=IMPORTED_CODE_VUE_TAILWIND_SYSTEM_PROMPT, + svg=IMPORTED_CODE_SVG_SYSTEM_PROMPT, +) diff --git a/backend/prompts.py b/backend/prompts/screenshot_system_prompts.py similarity index 67% rename from backend/prompts.py rename to backend/prompts/screenshot_system_prompts.py index c9e48cb..fca91ba 100644 --- a/backend/prompts.py +++ b/backend/prompts/screenshot_system_prompts.py @@ -1,4 +1,7 @@ -TAILWIND_SYSTEM_PROMPT = """ +from prompts.types import SystemPrompts + + +HTML_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. @@ -111,54 +114,72 @@ 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. +VUE_TAILWIND_SYSTEM_PROMPT = """ +You are an expert Vue/Tailwind developer +You take screenshots of a reference web page from the user, and then build single page apps +using Vue 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. +- Use Vue using the global build like so: + +
{{ message }}
+ + +In terms of libraries, + +- Use these script to include Vue 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. +The return result must only include the code. """ -def assemble_prompt( - image_data_url, generated_code_config: str, result_image_data_url=None -): - # Set the system prompt based on the output settings - system_content = TAILWIND_SYSTEM_PROMPT - if generated_code_config == "html_tailwind": - system_content = TAILWIND_SYSTEM_PROMPT - elif generated_code_config == "react_tailwind": - system_content = REACT_TAILWIND_SYSTEM_PROMPT - elif generated_code_config == "bootstrap": - system_content = BOOTSTRAP_SYSTEM_PROMPT - elif generated_code_config == "ionic_tailwind": - system_content = IONIC_TAILWIND_SYSTEM_PROMPT - else: - raise Exception("Code config is not one of available options") +SVG_SYSTEM_PROMPT = """ +You are an expert at building SVGs. +You take screenshots of a reference web page from the user, and then build a SVG that looks exactly like the screenshot. - user_content = [ - { - "type": "image_url", - "image_url": {"url": image_data_url, "detail": "high"}, - }, - { - "type": "text", - "text": USER_PROMPT, - }, - ] +- Make sure the SVG 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. +- You can use Google Fonts - # 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, - }, - ] +Return only the full code in tags. +Do not include markdown "```" or "```svg" at the start or end. +""" + + +SYSTEM_PROMPTS = SystemPrompts( + html_tailwind=HTML_TAILWIND_SYSTEM_PROMPT, + react_tailwind=REACT_TAILWIND_SYSTEM_PROMPT, + bootstrap=BOOTSTRAP_SYSTEM_PROMPT, + ionic_tailwind=IONIC_TAILWIND_SYSTEM_PROMPT, + vue_tailwind=VUE_TAILWIND_SYSTEM_PROMPT, + svg=SVG_SYSTEM_PROMPT, +) diff --git a/backend/prompts/test_prompts.py b/backend/prompts/test_prompts.py new file mode 100644 index 0000000..9e60410 --- /dev/null +++ b/backend/prompts/test_prompts.py @@ -0,0 +1,397 @@ +from prompts import assemble_imported_code_prompt, 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. +""" + +IONIC_TAILWIND_SYSTEM_PROMPT = """ +You are an expert Ionic/Tailwind developer +You take screenshots of a reference web page from the user, and then build single page apps +using Ionic 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 Ionic so that it can run on a standalone page: + + + +- Use this script to include Tailwind: +- You can use Google Fonts +- ionicons for icons, add the following + + + +Return only the full code in tags. +Do not include markdown "```" or "```html" at the start or end. +""" + +VUE_TAILWIND_SYSTEM_PROMPT = """ +You are an expert Vue/Tailwind developer +You take screenshots of a reference web page from the user, and then build single page apps +using Vue 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. +- Use Vue using the global build like so: + +
{{ message }}
+ + +In terms of libraries, + +- Use these script to include Vue 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. +The return result must only include the code. +""" + +SVG_SYSTEM_PROMPT = """ +You are an expert at building SVGs. +You take screenshots of a reference web page from the user, and then build a SVG that looks exactly like the screenshot. + +- Make sure the SVG 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. +- You can use Google Fonts + +Return only the full code in tags. +Do not include markdown "```" or "```svg" at the start or end. +""" + +IMPORTED_CODE_TAILWIND_SYSTEM_PROMPT = """ +You are an expert Tailwind developer. + +- 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. 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. +""" + +IMPORTED_CODE_REACT_TAILWIND_SYSTEM_PROMPT = """ +You are an expert React/Tailwind developer + +- 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. 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. +""" + +IMPORTED_CODE_BOOTSTRAP_SYSTEM_PROMPT = """ +You are an expert Bootstrap developer. + +- 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. 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. +""" + +IMPORTED_CODE_IONIC_TAILWIND_SYSTEM_PROMPT = """ +You are an expert Ionic/Tailwind developer. + +- 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. 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 Ionic so that it can run on a standalone page: + + + +- Use this script to include Tailwind: +- You can use Google Fonts +- ionicons for icons, add the following + + + +Return only the full code in tags. +Do not include markdown "```" or "```html" at the start or end. +""" + + +IMPORTED_CODE_VUE_TAILWIND_PROMPT = """ +You are an expert Vue/Tailwind developer. + +- 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. 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 Vue so that it can run on a standalone page: + +- Use Vue using the global build like so: +
{{ message }}
+ +- 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. +The return result must only include the code.""" + +IMPORTED_CODE_SVG_SYSTEM_PROMPT = """ +You are an expert at building SVGs. + +- 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. +- You can use Google Fonts + +Return only the full code in tags. +Do not include markdown "```" or "```svg" at the start or end. +""" + +USER_PROMPT = """ +Generate code for a web page that looks exactly like this. +""" + +SVG_USER_PROMPT = """ +Generate code for a SVG that looks exactly like this. +""" + + +def test_prompts(): + tailwind_prompt = assemble_prompt( + "image_data_url", "html_tailwind", "result_image_data_url" + ) + assert tailwind_prompt[0]["content"] == TAILWIND_SYSTEM_PROMPT + assert tailwind_prompt[1]["content"][2]["text"] == USER_PROMPT # type: ignore + + react_tailwind_prompt = assemble_prompt( + "image_data_url", "react_tailwind", "result_image_data_url" + ) + assert react_tailwind_prompt[0]["content"] == REACT_TAILWIND_SYSTEM_PROMPT + assert react_tailwind_prompt[1]["content"][2]["text"] == USER_PROMPT # type: ignore + + bootstrap_prompt = assemble_prompt( + "image_data_url", "bootstrap", "result_image_data_url" + ) + assert bootstrap_prompt[0]["content"] == BOOTSTRAP_SYSTEM_PROMPT + assert bootstrap_prompt[1]["content"][2]["text"] == USER_PROMPT # type: ignore + + ionic_tailwind = assemble_prompt( + "image_data_url", "ionic_tailwind", "result_image_data_url" + ) + assert ionic_tailwind[0]["content"] == IONIC_TAILWIND_SYSTEM_PROMPT + assert ionic_tailwind[1]["content"][2]["text"] == USER_PROMPT # type: ignore + + vue_tailwind = assemble_prompt( + "image_data_url", "vue_tailwind", "result_image_data_url" + ) + assert vue_tailwind[0]["content"] == VUE_TAILWIND_SYSTEM_PROMPT + assert vue_tailwind[1]["content"][2]["text"] == USER_PROMPT # type: ignore + + svg_prompt = assemble_prompt("image_data_url", "svg", "result_image_data_url") + assert svg_prompt[0]["content"] == SVG_SYSTEM_PROMPT + assert svg_prompt[1]["content"][2]["text"] == SVG_USER_PROMPT # type: ignore + + +def test_imported_code_prompts(): + tailwind_prompt = assemble_imported_code_prompt( + "code", "html_tailwind", "result_image_data_url" + ) + expected_tailwind_prompt = [ + {"role": "system", "content": IMPORTED_CODE_TAILWIND_SYSTEM_PROMPT}, + {"role": "user", "content": "Here is the code of the app: code"}, + ] + assert tailwind_prompt == expected_tailwind_prompt + + react_tailwind_prompt = assemble_imported_code_prompt( + "code", "react_tailwind", "result_image_data_url" + ) + expected_react_tailwind_prompt = [ + {"role": "system", "content": IMPORTED_CODE_REACT_TAILWIND_SYSTEM_PROMPT}, + {"role": "user", "content": "Here is the code of the app: code"}, + ] + assert react_tailwind_prompt == expected_react_tailwind_prompt + + bootstrap_prompt = assemble_imported_code_prompt( + "code", "bootstrap", "result_image_data_url" + ) + expected_bootstrap_prompt = [ + {"role": "system", "content": IMPORTED_CODE_BOOTSTRAP_SYSTEM_PROMPT}, + {"role": "user", "content": "Here is the code of the app: code"}, + ] + assert bootstrap_prompt == expected_bootstrap_prompt + + ionic_tailwind = assemble_imported_code_prompt( + "code", "ionic_tailwind", "result_image_data_url" + ) + expected_ionic_tailwind = [ + {"role": "system", "content": IMPORTED_CODE_IONIC_TAILWIND_SYSTEM_PROMPT}, + {"role": "user", "content": "Here is the code of the app: code"}, + ] + assert ionic_tailwind == expected_ionic_tailwind + + vue_tailwind = assemble_imported_code_prompt( + "code", "vue_tailwind", "result_image_data_url" + ) + expected_vue_tailwind = [ + {"role": "system", "content": IMPORTED_CODE_VUE_TAILWIND_PROMPT}, + {"role": "user", "content": "Here is the code of the app: code"}, + ] + assert vue_tailwind == expected_vue_tailwind + + svg = assemble_imported_code_prompt("code", "svg", "result_image_data_url") + expected_svg = [ + {"role": "system", "content": IMPORTED_CODE_SVG_SYSTEM_PROMPT}, + {"role": "user", "content": "Here is the code of the SVG: code"}, + ] + assert svg == expected_svg diff --git a/backend/prompts/types.py b/backend/prompts/types.py new file mode 100644 index 0000000..9068443 --- /dev/null +++ b/backend/prompts/types.py @@ -0,0 +1,20 @@ +from typing import Literal, TypedDict + + +class SystemPrompts(TypedDict): + html_tailwind: str + react_tailwind: str + bootstrap: str + ionic_tailwind: str + vue_tailwind: str + svg: str + + +Stack = Literal[ + "html_tailwind", + "react_tailwind", + "bootstrap", + "ionic_tailwind", + "vue_tailwind", + "svg", +] diff --git a/backend/pyproject.toml b/backend/pyproject.toml index d954787..f896606 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -8,7 +8,7 @@ license = "MIT" [tool.poetry.dependencies] python = "^3.10, <3.13" fastapi = "^0.95.0" -uvicorn = "^0.24.0.post1" +uvicorn = "^0.25.0" websockets = "^12.0" openai = "^1.2.4" python-dotenv = "^1.0.0" @@ -16,6 +16,8 @@ beautifulsoup4 = "^4.12.2" httpx = "^0.25.1" [tool.poetry.group.dev.dependencies] +pytest = "^7.4.3" +pyright = "^1.1.345" pyinstaller = "^6.2.0" [build-system] diff --git a/backend/pyrightconfig.json b/backend/pyrightconfig.json new file mode 100644 index 0000000..6e475af --- /dev/null +++ b/backend/pyrightconfig.json @@ -0,0 +1,3 @@ +{ + "exclude": ["image_generation.py"] +} diff --git a/backend/routes/evals.py b/backend/routes/evals.py new file mode 100644 index 0000000..798a9d8 --- /dev/null +++ b/backend/routes/evals.py @@ -0,0 +1,46 @@ +import os +from fastapi import APIRouter +from pydantic import BaseModel +from evals.utils import image_to_data_url +from evals.config import EVALS_DIR + + +router = APIRouter() + + +class Eval(BaseModel): + input: str + output: str + + +@router.get("/evals") +async def get_evals(): + # Get all evals from EVALS_DIR + input_dir = EVALS_DIR + "/inputs" + output_dir = EVALS_DIR + "/outputs" + + evals: list[Eval] = [] + for file in os.listdir(input_dir): + if file.endswith(".png"): + input_file_path = os.path.join(input_dir, file) + input_file = await image_to_data_url(input_file_path) + + # Construct the corresponding output file name + output_file_name = file.replace(".png", ".html") + output_file_path = os.path.join(output_dir, output_file_name) + + # Check if the output file exists + if os.path.exists(output_file_path): + with open(output_file_path, "r") as f: + output_file_data = f.read() + else: + output_file_data = "Output file not found." + + evals.append( + Eval( + input=input_file, + output=output_file_data, + ) + ) + + return evals diff --git a/backend/routes/generate_code.py b/backend/routes/generate_code.py new file mode 100644 index 0000000..092289c --- /dev/null +++ b/backend/routes/generate_code.py @@ -0,0 +1,270 @@ +import os +import traceback +from fastapi import APIRouter, WebSocket +import openai +from config import IS_PROD, SHOULD_MOCK_AI_RESPONSE +from llm import stream_openai_response +from openai.types.chat import ChatCompletionMessageParam +from mock_llm import mock_completion +from typing import Dict, List, cast, get_args +from image_generation import create_alt_url_mapping, generate_images +from prompts import assemble_imported_code_prompt, assemble_prompt +from access_token import validate_access_token +from datetime import datetime +import json +from prompts.types import Stack + +from utils import pprint_prompt # type: ignore + + +router = APIRouter() + + +def write_logs(prompt_messages: List[ChatCompletionMessageParam], completion: str): + # Get the logs path from environment, default to the current working directory + logs_path = os.environ.get("LOGS_PATH", os.getcwd()) + + # 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: + f.write(json.dumps({"prompt": prompt_messages, "completion": completion})) + + +@router.websocket("/generate-code") +async def stream_code(websocket: WebSocket): + await websocket.accept() + + print("Incoming websocket connection...") + + async def throw_error( + message: str, + ): + await websocket.send_json({"type": "error", "value": message}) + await websocket.close() + + # TODO: Are the values always strings? + params: Dict[str, str] = await websocket.receive_json() + + print("Received params") + + # Read the code config settings from the request. Fall back to default if not provided. + generated_code_config = "" + if "generatedCodeConfig" in params and params["generatedCodeConfig"]: + generated_code_config = params["generatedCodeConfig"] + print(f"Generating {generated_code_config} code") + + # Get the OpenAI API key from the request. Fall back to environment variable if not provided. + # If neither is provided, we throw an error. + openai_api_key = None + if "accessCode" in params and params["accessCode"]: + print("Access code - using platform API key") + res = await validate_access_token(params["accessCode"]) + if res["success"]: + openai_api_key = os.environ.get("PLATFORM_OPENAI_API_KEY") + else: + await websocket.send_json( + { + "type": "error", + "value": res["failure_reason"], + } + ) + return + else: + 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. If you add it to .env, make sure to restart the backend server.", + } + ) + return + + # Validate the generated code config + if not generated_code_config in get_args(Stack): + await throw_error(f"Invalid generated code config: {generated_code_config}") + return + # Cast the variable to the Stack type + valid_stack = cast(Stack, generated_code_config) + + # Get the OpenAI Base URL from the request. Fall back to environment variable if not provided. + openai_base_url = None + # Disable user-specified OpenAI Base URL in prod + if not os.environ.get("IS_PROD"): + if "openAiBaseURL" in params and 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: + print("Using official OpenAI 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 + else True + ) + + print("generating code...") + await websocket.send_json({"type": "status", "value": "Generating code..."}) + + async def process_chunk(content: str): + await websocket.send_json({"type": "chunk", "value": content}) + + # Image cache for updates so that we don't have to regenerate images + image_cache: Dict[str, str] = {} + + # If this generation started off with imported code, we need to assemble the prompt differently + if params.get("isImportedFromCode") and params["isImportedFromCode"]: + original_imported_code = params["history"][0] + prompt_messages = assemble_imported_code_prompt( + original_imported_code, valid_stack + ) + for index, text in enumerate(params["history"][1:]): + if index % 2 == 0: + message: ChatCompletionMessageParam = { + "role": "user", + "content": text, + } + else: + message: ChatCompletionMessageParam = { + "role": "assistant", + "content": text, + } + prompt_messages.append(message) + else: + # Assemble the prompt + try: + if params.get("resultImage") and params["resultImage"]: + prompt_messages = assemble_prompt( + params["image"], valid_stack, params["resultImage"] + ) + else: + prompt_messages = assemble_prompt(params["image"], valid_stack) + except: + await websocket.send_json( + { + "type": "error", + "value": "Error assembling prompt. Contact support at support@picoapps.xyz", + } + ) + await websocket.close() + return + + if params["generationType"] == "update": + # Transform the history tree into message format + # TODO: Move this to frontend + for index, text in enumerate(params["history"]): + if index % 2 == 0: + message: ChatCompletionMessageParam = { + "role": "assistant", + "content": text, + } + else: + message: ChatCompletionMessageParam = { + "role": "user", + "content": text, + } + prompt_messages.append(message) + + image_cache = create_alt_url_mapping(params["history"][-2]) + + pprint_prompt(prompt_messages) + + if SHOULD_MOCK_AI_RESPONSE: + completion = await mock_completion(process_chunk) + else: + try: + completion = await stream_openai_response( + prompt_messages, + api_key=openai_api_key, + base_url=openai_base_url, + callback=lambda x: process_chunk(x), + ) + except openai.AuthenticationError as e: + print("[GENERATE_CODE] Authentication failed", e) + error_message = ( + "Incorrect OpenAI key. Please make sure your OpenAI API key is correct, or create a new OpenAI API key on your OpenAI dashboard." + + ( + " Alternatively, you can purchase code generation credits directly on this website." + if IS_PROD + else "" + ) + ) + return await throw_error(error_message) + except openai.NotFoundError as e: + print("[GENERATE_CODE] Model not found", e) + error_message = ( + e.message + + ". Please make sure you have followed the instructions correctly to obtain an OpenAI key with GPT vision access: https://github.com/abi/screenshot-to-code/blob/main/Troubleshooting.md" + + ( + " Alternatively, you can purchase code generation credits directly on this website." + if IS_PROD + else "" + ) + ) + return await throw_error(error_message) + except openai.RateLimitError as e: + print("[GENERATE_CODE] Rate limit exceeded", e) + error_message = ( + "OpenAI error - 'You exceeded your current quota, please check your plan and billing details.'" + + ( + " Alternatively, you can purchase code generation credits directly on this website." + if IS_PROD + else "" + ) + ) + return await throw_error(error_message) + + # Write the messages dict into a log so that we can debug later + write_logs(prompt_messages, completion) + + try: + if should_generate_images: + await websocket.send_json( + {"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, + ) + else: + updated_html = completion + await websocket.send_json({"type": "setCode", "value": updated_html}) + await websocket.send_json( + {"type": "status", "value": "Code generation complete."} + ) + except Exception as e: + traceback.print_exc() + print("Image generation failed", e) + # Send set code even if image generation fails since that triggers + # the frontend to update history + await websocket.send_json({"type": "setCode", "value": completion}) + await websocket.send_json( + {"type": "status", "value": "Image generation failed but code is complete."} + ) + + await websocket.close() diff --git a/backend/routes/home.py b/backend/routes/home.py new file mode 100644 index 0000000..c9f66b4 --- /dev/null +++ b/backend/routes/home.py @@ -0,0 +1,12 @@ +from fastapi import APIRouter +from fastapi.responses import HTMLResponse + + +router = APIRouter() + + +@router.get("/") +async def get_status(): + return HTMLResponse( + content="

Your backend is running correctly. Please open the front-end URL (default is http://localhost:5173) to use screenshot-to-code.

" + ) diff --git a/backend/routes/screenshot.py b/backend/routes/screenshot.py index 7efcfb8..258cd7e 100644 --- a/backend/routes/screenshot.py +++ b/backend/routes/screenshot.py @@ -11,7 +11,9 @@ def bytes_to_data_url(image_bytes: bytes, mime_type: str) -> str: return f"data:{mime_type};base64,{base64_image}" -async def capture_screenshot(target_url, api_key, device="desktop") -> bytes: +async def capture_screenshot( + target_url: str, api_key: str, device: str = "desktop" +) -> bytes: api_base_url = "https://api.screenshotone.com/take" params = { diff --git a/backend/run_evals.py b/backend/run_evals.py new file mode 100644 index 0000000..ddb7eaa --- /dev/null +++ b/backend/run_evals.py @@ -0,0 +1,41 @@ +# Load environment variables first +from dotenv import load_dotenv + +load_dotenv() + +import os +from typing import Any, Coroutine +import asyncio + +from evals.config import EVALS_DIR +from evals.core import generate_code_core +from evals.utils import image_to_data_url + + +async def main(): + INPUT_DIR = EVALS_DIR + "/inputs" + OUTPUT_DIR = EVALS_DIR + "/outputs" + + # Get all the files in the directory (only grab pngs) + evals = [f for f in os.listdir(INPUT_DIR) if f.endswith(".png")] + + tasks: list[Coroutine[Any, Any, str]] = [] + for filename in evals: + filepath = os.path.join(INPUT_DIR, filename) + data_url = await image_to_data_url(filepath) + task = generate_code_core(data_url, "vue_tailwind") + tasks.append(task) + + results = await asyncio.gather(*tasks) + + os.makedirs(OUTPUT_DIR, exist_ok=True) + + for filename, content in zip(evals, results): + # File name is derived from the original filename in evals + output_filename = f"{os.path.splitext(filename)[0]}.html" + output_filepath = os.path.join(OUTPUT_DIR, output_filename) + with open(output_filepath, "w") as file: + file.write(content) + + +asyncio.run(main()) diff --git a/backend/start.py b/backend/start.py new file mode 100644 index 0000000..a126468 --- /dev/null +++ b/backend/start.py @@ -0,0 +1,4 @@ +import uvicorn + +if __name__ == "__main__": + uvicorn.run("main:app", port=7001, reload=True) diff --git a/backend/utils.py b/backend/utils.py index 17d6423..6c28e14 100644 --- a/backend/utils.py +++ b/backend/utils.py @@ -1,28 +1,30 @@ import copy import json +from typing import List +from openai.types.chat import ChatCompletionMessageParam -def pprint_prompt(prompt_messages): +def pprint_prompt(prompt_messages: List[ChatCompletionMessageParam]): print(json.dumps(truncate_data_strings(prompt_messages), indent=4)) -def truncate_data_strings(data): +def truncate_data_strings(data: List[ChatCompletionMessageParam]): # type: ignore # Deep clone the data to avoid modifying the original object cloned_data = copy.deepcopy(data) if isinstance(cloned_data, dict): - for key, value in cloned_data.items(): + for key, value in cloned_data.items(): # type: ignore # Recursively call the function if the value is a dictionary or a list if isinstance(value, (dict, list)): - cloned_data[key] = truncate_data_strings(value) + cloned_data[key] = truncate_data_strings(value) # type: ignore # Truncate the string if it it's long and add ellipsis and length elif isinstance(value, str): - cloned_data[key] = value[:40] + cloned_data[key] = value[:40] # type: ignore if len(value) > 40: - cloned_data[key] += "..." + f" ({len(value)} chars)" + cloned_data[key] += "..." + f" ({len(value)} chars)" # type: ignore - elif isinstance(cloned_data, list): + elif isinstance(cloned_data, list): # type: ignore # Process each item in the list - cloned_data = [truncate_data_strings(item) for item in cloned_data] + cloned_data = [truncate_data_strings(item) for item in cloned_data] # type: ignore - return cloned_data + return cloned_data # type: ignore diff --git a/design-docs.md b/design-docs.md new file mode 100644 index 0000000..eb38119 --- /dev/null +++ b/design-docs.md @@ -0,0 +1,5 @@ +## Version History + +Version history is stored as a tree on the client-side. + +![Screenshot to Code](https://github.com/abi/screenshot-to-code/assets/23818/e35644aa-b90a-4aa7-8027-b8732796fd7c) diff --git a/frontend/package.json b/frontend/package.json index 44c8813..8b4f0ee 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -17,6 +17,7 @@ "@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-collapsible": "^1.0.3", "@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-hover-card": "^1.0.7", "@radix-ui/react-icons": "^1.3.0", @@ -40,6 +41,7 @@ "react-dropzone": "^14.2.3", "react-hot-toast": "^2.4.1", "react-icons": "^4.12.0", + "react-router-dom": "^6.20.1", "tailwind-merge": "^2.0.0", "tailwindcss-animate": "^1.0.7", "thememirror": "^2.0.1", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index f1301e0..3f7a025 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from "react"; import ImageUpload from "./components/ImageUpload"; import CodePreview from "./components/CodePreview"; import Preview from "./components/Preview"; -import { CodeGenerationParams, generateCode } from "./generateCode"; +import { generateCode } from "./generateCode"; import Spinner from "./components/Spinner"; import classNames from "classnames"; import { @@ -18,7 +18,7 @@ import { Button } from "@/components/ui/button"; import { Textarea } from "@/components/ui/textarea"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "./components/ui/tabs"; import SettingsDialog from "./components/SettingsDialog"; -import { Settings, EditorTheme, AppState, GeneratedCodeConfig } from "./types"; +import { AppState, CodeGenerationParams, EditorTheme, Settings } from "./types"; import { IS_RUNNING_ON_CLOUD } from "./config"; import { PicoBadge } from "./components/PicoBadge"; import { OnboardingNote } from "./components/OnboardingNote"; @@ -33,6 +33,8 @@ import { History } from "./components/history/history_types"; import HistoryDisplay from "./components/history/HistoryDisplay"; import { extractHistoryTree } from "./components/history/utils"; import toast from "react-hot-toast"; +import ImportCodeSection from "./components/ImportCodeSection"; +import { Stack } from "./lib/stacks"; const IS_OPENAI_DOWN = false; @@ -43,6 +45,7 @@ function App() { const [referenceImages, setReferenceImages] = useState([]); const [executionConsole, setExecutionConsole] = useState([]); const [updateInstruction, setUpdateInstruction] = useState(""); + const [isImportedFromCode, setIsImportedFromCode] = useState(false); // Settings const [settings, setSettings] = usePersistedState( @@ -52,7 +55,7 @@ function App() { screenshotOneApiKey: null, isImageGenerationEnabled: true, editorTheme: EditorTheme.COBALT, - generatedCodeConfig: GeneratedCodeConfig.HTML_TAILWIND, + generatedCodeConfig: Stack.HTML_TAILWIND, // Only relevant for hosted version isTermOfServiceAccepted: false, accessCode: null, @@ -77,7 +80,7 @@ function App() { if (!settings.generatedCodeConfig) { setSettings((prev) => ({ ...prev, - generatedCodeConfig: GeneratedCodeConfig.HTML_TAILWIND, + generatedCodeConfig: Stack.HTML_TAILWIND, })); } }, [settings.generatedCodeConfig, setSettings]); @@ -117,13 +120,28 @@ function App() { setGeneratedCode(""); setReferenceImages([]); setExecutionConsole([]); + setUpdateInstruction(""); + setIsImportedFromCode(false); setAppHistory([]); + setCurrentVersion(null); + setShouldIncludeResultImage(false); }; - const stop = () => { + const cancelCodeGeneration = () => { 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); + cancelCodeGenerationAndReset(); + }; + + const cancelCodeGenerationAndReset = () => { + // When this is the first version, reset the entire app state + if (currentVersion === null) { + reset(); + } else { + // Otherwise, revert to the last version + setGeneratedCode(appHistory[currentVersion].code); + setAppState(AppState.CODE_READY); + } }; function doGenerateCode( @@ -139,7 +157,9 @@ function App() { generateCode( wsRef, updatedParams, + // On change (token) => setGeneratedCode((prev) => prev + token), + // On set code (code) => { setGeneratedCode(code); if (params.generationType === "create") { @@ -178,7 +198,13 @@ function App() { }); } }, + // On status update (line) => setExecutionConsole((prev) => [...prev, line]), + // On cancel + () => { + cancelCodeGenerationAndReset(); + }, + // On complete () => { setAppState(AppState.CODE_READY); } @@ -211,10 +237,17 @@ function App() { return; } - const updatedHistory = [ - ...extractHistoryTree(appHistory, currentVersion), - updateInstruction, - ]; + let historyTree; + try { + historyTree = extractHistoryTree(appHistory, currentVersion); + } catch { + toast.error( + "Version history is invalid. This shouldn't happen. Please contact support or open a Github issue." + ); + return; + } + + const updatedHistory = [...historyTree, updateInstruction]; if (shouldIncludeResultImage) { const resultImage = await takeScreenshot(); @@ -224,6 +257,7 @@ function App() { image: referenceImages[0], resultImage: resultImage, history: updatedHistory, + isImportedFromCode, }, currentVersion ); @@ -233,6 +267,7 @@ function App() { generationType: "update", image: referenceImages[0], history: updatedHistory, + isImportedFromCode, }, currentVersion ); @@ -249,6 +284,32 @@ function App() { })); }; + function setStack(stack: Stack) { + setSettings((prev) => ({ + ...prev, + generatedCodeConfig: stack, + })); + } + + function importFromCode(code: string, stack: Stack) { + setIsImportedFromCode(true); + + // Set up this project + setGeneratedCode(code); + setStack(stack); + setAppHistory([ + { + type: "code_create", + parentIndex: null, + code, + inputs: { code }, + }, + ]); + setCurrentVersion(0); + + setAppState(AppState.CODE_READY); + } + return (
{IS_RUNNING_ON_CLOUD && } @@ -266,13 +327,8 @@ function App() {
- setSettings((prev) => ({ - ...prev, - generatedCodeConfig: config, - })) - } + stack={settings.generatedCodeConfig} + setStack={(config) => setStack(config)} shouldDisableUpdates={ appState === AppState.CODING || appState === AppState.CODE_READY } @@ -302,10 +358,10 @@ function App() {
@@ -357,22 +413,24 @@ function App() { {/* Reference image display */}
-
-
- Reference + {referenceImages.length > 0 && ( +
+
+ Reference +
+
+ Original Screenshot +
-
- Original Screenshot -
-
+ )}

Console @@ -417,6 +475,7 @@ function App() { doCreate={doCreate} screenshotOneApiKey={settings.screenshotOneApiKey} /> +

)} diff --git a/frontend/src/components/ImageUpload.tsx b/frontend/src/components/ImageUpload.tsx index 6cd45b7..ff97466 100644 --- a/frontend/src/components/ImageUpload.tsx +++ b/frontend/src/components/ImageUpload.tsx @@ -1,4 +1,5 @@ -import { useState, useEffect, useMemo, useCallback } from "react"; +import { useState, useEffect, useMemo } from "react"; +// useCallback import { useDropzone } from "react-dropzone"; // import { PromptImage } from "../../../types"; import { toast } from "react-hot-toast"; @@ -89,39 +90,39 @@ function ImageUpload({ setReferenceImages }: Props) { }, }); - const pasteEvent = useCallback( - (event: ClipboardEvent) => { - const clipboardData = event.clipboardData; - if (!clipboardData) return; + // 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); - } - } + // 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) => { - if (dataUrls.length > 0) { - setReferenceImages(dataUrls.map((dataUrl) => dataUrl as string)); - } - }) - .catch((error) => { - // TODO: Display error to user - console.error("Error reading files:", error); - }); - }, - [setReferenceImages] - ); + // // Convert images to data URLs and set the prompt images state + // Promise.all(files.map((file) => fileToDataURL(file))) + // .then((dataUrls) => { + // if (dataUrls.length > 0) { + // 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(() => { + // window.addEventListener("paste", pasteEvent); + // }, [pasteEvent]); useEffect(() => { return () => files.forEach((file) => URL.revokeObjectURL(file.preview)); diff --git a/frontend/src/components/ImportCodeSection.tsx b/frontend/src/components/ImportCodeSection.tsx new file mode 100644 index 0000000..b320a97 --- /dev/null +++ b/frontend/src/components/ImportCodeSection.tsx @@ -0,0 +1,74 @@ +import { useState } from "react"; +import { Button } from "./ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "./ui/dialog"; +import { Textarea } from "./ui/textarea"; +import OutputSettingsSection from "./OutputSettingsSection"; +import toast from "react-hot-toast"; +import { Stack } from "../lib/stacks"; + +interface Props { + importFromCode: (code: string, stack: Stack) => void; +} + +function ImportCodeSection({ importFromCode }: Props) { + const [code, setCode] = useState(""); + const [stack, setStack] = useState(undefined); + + const doImport = () => { + if (code === "") { + toast.error("Please paste in some code"); + return; + } + + if (stack === undefined) { + toast.error("Please select your stack"); + return; + } + + importFromCode(code, stack); + }; + return ( + + + + + + + Paste in your HTML code + + Make sure that the code you're importing is valid HTML. + + + +