From 5912957514aaf87722aef31f64b738cf705d5b57 Mon Sep 17 00:00:00 2001 From: Abi Raja Date: Mon, 8 Jan 2024 16:52:38 -0500 Subject: [PATCH 01/12] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ad42423..3e28988 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 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 From 0080a5e2c49e94c6328f3c163b75c4926a2f341f Mon Sep 17 00:00:00 2001 From: Abi Raja Date: Mon, 8 Jan 2024 14:25:00 -0800 Subject: [PATCH 02/12] better organization of prompts directory --- backend/{prompts.py => prompts/__init__.py} | 4 ++-- backend/{ => prompts}/imported_code_prompts.py | 0 backend/{ => prompts}/screenshot_system_prompts.py | 0 backend/{ => prompts}/test_prompts.py | 0 4 files changed, 2 insertions(+), 2 deletions(-) rename backend/{prompts.py => prompts/__init__.py} (97%) rename backend/{ => prompts}/imported_code_prompts.py (100%) rename backend/{ => prompts}/screenshot_system_prompts.py (100%) rename backend/{ => prompts}/test_prompts.py (100%) diff --git a/backend/prompts.py b/backend/prompts/__init__.py similarity index 97% rename from backend/prompts.py rename to backend/prompts/__init__.py index d3d3b18..3a063ac 100644 --- a/backend/prompts.py +++ b/backend/prompts/__init__.py @@ -2,14 +2,14 @@ from typing import List, Union from openai.types.chat import ChatCompletionMessageParam, ChatCompletionContentPartParam -from imported_code_prompts import ( +from prompts.imported_code_prompts import ( IMPORTED_CODE_BOOTSTRAP_SYSTEM_PROMPT, IMPORTED_CODE_IONIC_TAILWIND_SYSTEM_PROMPT, IMPORTED_CODE_REACT_TAILWIND_SYSTEM_PROMPT, IMPORTED_CODE_TAILWIND_SYSTEM_PROMPT, IMPORTED_CODE_SVG_SYSTEM_PROMPT, ) -from screenshot_system_prompts import ( +from prompts.screenshot_system_prompts import ( BOOTSTRAP_SYSTEM_PROMPT, IONIC_TAILWIND_SYSTEM_PROMPT, REACT_TAILWIND_SYSTEM_PROMPT, diff --git a/backend/imported_code_prompts.py b/backend/prompts/imported_code_prompts.py similarity index 100% rename from backend/imported_code_prompts.py rename to backend/prompts/imported_code_prompts.py diff --git a/backend/screenshot_system_prompts.py b/backend/prompts/screenshot_system_prompts.py similarity index 100% rename from backend/screenshot_system_prompts.py rename to backend/prompts/screenshot_system_prompts.py diff --git a/backend/test_prompts.py b/backend/prompts/test_prompts.py similarity index 100% rename from backend/test_prompts.py rename to backend/prompts/test_prompts.py From adda6852f36fdc277ce867fb0e03808787340e21 Mon Sep 17 00:00:00 2001 From: Abi Raja Date: Mon, 8 Jan 2024 14:35:26 -0800 Subject: [PATCH 03/12] update gitignore --- backend/.gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/.gitignore b/backend/.gitignore index 128eab6..a42aad3 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -153,4 +153,4 @@ cython_debug/ # Temporary eval output -evals +evals_data From 15dc74a328ac2f8af020f82a207e03984734b769 Mon Sep 17 00:00:00 2001 From: Abi Raja Date: Mon, 8 Jan 2024 14:55:41 -0800 Subject: [PATCH 04/12] improve type checking for stack on backend --- backend/prompts/__init__.py | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/backend/prompts/__init__.py b/backend/prompts/__init__.py index 3a063ac..fbfa5ab 100644 --- a/backend/prompts/__init__.py +++ b/backend/prompts/__init__.py @@ -1,4 +1,4 @@ -from typing import List, Union +from typing import List, Literal, NoReturn, Union from openai.types.chat import ChatCompletionMessageParam, ChatCompletionContentPartParam @@ -27,9 +27,22 @@ SVG_USER_PROMPT = """ Generate code for a SVG that looks exactly like this. """ +Stack = Literal[ + "html_tailwind", + "react_tailwind", + "bootstrap", + "ionic_tailwind", + "vue_tailwind", + "svg", +] + + +def assert_never(x: NoReturn) -> NoReturn: + raise AssertionError(f"Stack is not one of available options: {x}") + def assemble_imported_code_prompt( - code: str, stack: str, result_image_data_url: Union[str, None] = None + code: str, stack: Stack, result_image_data_url: Union[str, None] = None ) -> List[ChatCompletionMessageParam]: system_content = IMPORTED_CODE_TAILWIND_SYSTEM_PROMPT if stack == "html_tailwind": @@ -40,10 +53,13 @@ def assemble_imported_code_prompt( system_content = IMPORTED_CODE_BOOTSTRAP_SYSTEM_PROMPT elif stack == "ionic_tailwind": system_content = IMPORTED_CODE_IONIC_TAILWIND_SYSTEM_PROMPT + elif stack == "vue_tailwind": + # TODO: Fix this prompt to be vue tailwind + system_content = IMPORTED_CODE_IONIC_TAILWIND_SYSTEM_PROMPT elif stack == "svg": system_content = IMPORTED_CODE_SVG_SYSTEM_PROMPT else: - raise Exception("Code config is not one of available options") + assert_never(stack) user_content = ( "Here is the code of the app: " + code @@ -65,7 +81,7 @@ def assemble_imported_code_prompt( def assemble_prompt( image_data_url: str, - generated_code_config: str, + generated_code_config: Stack, result_image_data_url: Union[str, None] = None, ) -> List[ChatCompletionMessageParam]: # Set the system prompt based on the output settings @@ -83,7 +99,7 @@ def assemble_prompt( elif generated_code_config == "svg": system_content = SVG_SYSTEM_PROMPT else: - raise Exception("Code config is not one of available options") + assert_never(generated_code_config) user_prompt = USER_PROMPT if generated_code_config != "svg" else SVG_USER_PROMPT From 1aeb8c4e1448288f8546cc688a854f85e07b3a83 Mon Sep 17 00:00:00 2001 From: Abi Raja Date: Mon, 8 Jan 2024 15:30:53 -0800 Subject: [PATCH 05/12] Type the backend properly to avoid code duplication and ensure type errors when a stack configuration is not properly added --- backend/prompts/__init__.py | 72 +++----------------- backend/prompts/imported_code_prompts.py | 13 ++++ backend/prompts/screenshot_system_prompts.py | 15 +++- backend/prompts/types.py | 20 ++++++ backend/routes/generate_code.py | 18 +++-- 5 files changed, 67 insertions(+), 71 deletions(-) create mode 100644 backend/prompts/types.py diff --git a/backend/prompts/__init__.py b/backend/prompts/__init__.py index fbfa5ab..4f2e329 100644 --- a/backend/prompts/__init__.py +++ b/backend/prompts/__init__.py @@ -1,22 +1,10 @@ -from typing import List, Literal, NoReturn, Union +from typing import List, NoReturn, Union from openai.types.chat import ChatCompletionMessageParam, ChatCompletionContentPartParam -from prompts.imported_code_prompts import ( - IMPORTED_CODE_BOOTSTRAP_SYSTEM_PROMPT, - IMPORTED_CODE_IONIC_TAILWIND_SYSTEM_PROMPT, - IMPORTED_CODE_REACT_TAILWIND_SYSTEM_PROMPT, - IMPORTED_CODE_TAILWIND_SYSTEM_PROMPT, - IMPORTED_CODE_SVG_SYSTEM_PROMPT, -) -from prompts.screenshot_system_prompts import ( - BOOTSTRAP_SYSTEM_PROMPT, - IONIC_TAILWIND_SYSTEM_PROMPT, - REACT_TAILWIND_SYSTEM_PROMPT, - TAILWIND_SYSTEM_PROMPT, - SVG_SYSTEM_PROMPT, - VUE_TAILWIND_SYSTEM_PROMPT, -) +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 = """ @@ -27,39 +15,11 @@ SVG_USER_PROMPT = """ Generate code for a SVG that looks exactly like this. """ -Stack = Literal[ - "html_tailwind", - "react_tailwind", - "bootstrap", - "ionic_tailwind", - "vue_tailwind", - "svg", -] - - -def assert_never(x: NoReturn) -> NoReturn: - raise AssertionError(f"Stack is not one of available options: {x}") - def assemble_imported_code_prompt( code: str, stack: Stack, result_image_data_url: Union[str, None] = None ) -> List[ChatCompletionMessageParam]: - system_content = IMPORTED_CODE_TAILWIND_SYSTEM_PROMPT - if stack == "html_tailwind": - system_content = IMPORTED_CODE_TAILWIND_SYSTEM_PROMPT - elif stack == "react_tailwind": - system_content = IMPORTED_CODE_REACT_TAILWIND_SYSTEM_PROMPT - elif stack == "bootstrap": - system_content = IMPORTED_CODE_BOOTSTRAP_SYSTEM_PROMPT - elif stack == "ionic_tailwind": - system_content = IMPORTED_CODE_IONIC_TAILWIND_SYSTEM_PROMPT - elif stack == "vue_tailwind": - # TODO: Fix this prompt to be vue tailwind - system_content = IMPORTED_CODE_IONIC_TAILWIND_SYSTEM_PROMPT - elif stack == "svg": - system_content = IMPORTED_CODE_SVG_SYSTEM_PROMPT - else: - assert_never(stack) + system_content = IMPORTED_CODE_SYSTEM_PROMPTS[stack] user_content = ( "Here is the code of the app: " + code @@ -81,27 +41,11 @@ def assemble_imported_code_prompt( def assemble_prompt( image_data_url: str, - generated_code_config: Stack, + stack: Stack, result_image_data_url: Union[str, None] = None, ) -> List[ChatCompletionMessageParam]: - # 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 - elif generated_code_config == "vue_tailwind": - system_content = VUE_TAILWIND_SYSTEM_PROMPT - elif generated_code_config == "svg": - system_content = SVG_SYSTEM_PROMPT - else: - assert_never(generated_code_config) - - user_prompt = USER_PROMPT if generated_code_config != "svg" else SVG_USER_PROMPT + system_content = SYSTEM_PROMPTS[stack] + user_prompt = USER_PROMPT if stack != "svg" else SVG_USER_PROMPT user_content: List[ChatCompletionContentPartParam] = [ { diff --git a/backend/prompts/imported_code_prompts.py b/backend/prompts/imported_code_prompts.py index a8bfa6a..4cda821 100644 --- a/backend/prompts/imported_code_prompts.py +++ b/backend/prompts/imported_code_prompts.py @@ -1,3 +1,6 @@ +from prompts.types import SystemPrompts + + IMPORTED_CODE_TAILWIND_SYSTEM_PROMPT = """ You are an expert Tailwind developer. @@ -90,3 +93,13 @@ You are an expert at building SVGs. 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, + # TODO: Fix this prompt to actually be Vue + vue_tailwind=IMPORTED_CODE_TAILWIND_SYSTEM_PROMPT, + svg=IMPORTED_CODE_SVG_SYSTEM_PROMPT, +) diff --git a/backend/prompts/screenshot_system_prompts.py b/backend/prompts/screenshot_system_prompts.py index 1dfcca4..fca91ba 100644 --- a/backend/prompts/screenshot_system_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. @@ -170,3 +173,13 @@ padding, margin, border, etc. Match the colors and sizes exactly. 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/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/routes/generate_code.py b/backend/routes/generate_code.py index b260cc0..bd2e0c2 100644 --- a/backend/routes/generate_code.py +++ b/backend/routes/generate_code.py @@ -6,12 +6,13 @@ 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 +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 @@ -96,6 +97,13 @@ async def stream_code(websocket: WebSocket): ) 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 @@ -131,7 +139,7 @@ async def stream_code(websocket: WebSocket): if params.get("isImportedFromCode") and params["isImportedFromCode"]: original_imported_code = params["history"][0] prompt_messages = assemble_imported_code_prompt( - original_imported_code, generated_code_config + original_imported_code, valid_stack ) for index, text in enumerate(params["history"][1:]): if index % 2 == 0: @@ -150,12 +158,10 @@ async def stream_code(websocket: WebSocket): try: if params.get("resultImage") and params["resultImage"]: prompt_messages = assemble_prompt( - params["image"], generated_code_config, params["resultImage"] + params["image"], valid_stack, params["resultImage"] ) else: - prompt_messages = assemble_prompt( - params["image"], generated_code_config - ) + prompt_messages = assemble_prompt(params["image"], valid_stack) except: await websocket.send_json( { From 7073879e6e4957df31049e8c5cb30b9b5f18710f Mon Sep 17 00:00:00 2001 From: Abi Raja Date: Mon, 8 Jan 2024 17:04:59 -0800 Subject: [PATCH 06/12] include type checker --- backend/README.md | 4 +++ backend/eval.py | 3 ++- backend/poetry.lock | 53 +++++++++++++++++++++++++++++++++++++- backend/pyproject.toml | 1 + backend/pyrightconfig.json | 3 +++ 5 files changed, 62 insertions(+), 2 deletions(-) create mode 100644 backend/pyrightconfig.json diff --git a/backend/README.md b/backend/README.md index ee55816..155bf46 100644 --- a/backend/README.md +++ b/backend/README.md @@ -1,3 +1,7 @@ +# Run the type checker + +poetry run pyright + # Run tests poetry run pytest diff --git a/backend/eval.py b/backend/eval.py index 60ef409..75641a3 100644 --- a/backend/eval.py +++ b/backend/eval.py @@ -4,6 +4,7 @@ from dotenv import load_dotenv from eval_config import EVALS_DIR from eval_utils import image_to_data_url +from prompts.types import Stack load_dotenv() @@ -15,7 +16,7 @@ import asyncio from utils import pprint_prompt -async def generate_code_core(image_url: str, stack: str) -> str: +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 diff --git a/backend/poetry.lock b/backend/poetry.lock index fbd0691..42a44ee 100644 --- a/backend/poetry.lock +++ b/backend/poetry.lock @@ -212,6 +212,21 @@ files = [ {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] +setuptools = "*" + [[package]] name = "openai" version = "1.3.7" @@ -317,6 +332,25 @@ typing-extensions = ">=4.2.0" dotenv = ["python-dotenv (>=0.10.4)"] email = ["email-validator (>=1.0.3)"] +[[package]] +name = "pyright" +version = "1.1.345" +description = "Command line wrapper for pyright" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pyright-1.1.345-py3-none-any.whl", hash = "sha256:00891361baf58698aa660d9374823d65782823ceb4a65515ff5dd159b0d4d2b1"}, + {file = "pyright-1.1.345.tar.gz", hash = "sha256:bb8c80671cdaeb913142b49642a741959f3fcd728c99814631c2bde3a7864938"}, +] + +[package.dependencies] +nodeenv = ">=1.6.0" + +[package.extras] +all = ["twine (>=3.4.1)"] +dev = ["twine (>=3.4.1)"] + [[package]] name = "pytest" version = "7.4.3" @@ -355,6 +389,23 @@ files = [ [package.extras] cli = ["click (>=5.0)"] +[[package]] +name = "setuptools" +version = "69.0.3" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +category = "dev" +optional = false +python-versions = ">=3.8" +files = [ + {file = "setuptools-69.0.3-py3-none-any.whl", hash = "sha256:385eb4edd9c9d5c17540511303e39a147ce2fc04bc55289c322b9e5904fe2c05"}, + {file = "setuptools-69.0.3.tar.gz", hash = "sha256:be1af57fc409f93647f2e8e4573a142ed38724b8cdd389706a867bb4efcf1e78"}, +] + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.1)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] + [[package]] name = "sniffio" version = "1.3.0" @@ -547,4 +598,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "92aaa379ec66ff6146122266c895f277835b23254e59c5ae21f0d0cae87d3a11" +content-hash = "00950515c8d7f2061811ce196e4aa47204eb98ea078bbeb875548072dbfa38b1" diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 216a020..bdaaed1 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -17,6 +17,7 @@ httpx = "^0.25.1" [tool.poetry.group.dev.dependencies] pytest = "^7.4.3" +pyright = "^1.1.345" [build-system] requires = ["poetry-core"] 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"] +} From aff0ad0b919e0adc983e42d83b667760ae0caa65 Mon Sep 17 00:00:00 2001 From: Abi Raja Date: Mon, 8 Jan 2024 17:06:21 -0800 Subject: [PATCH 07/12] Update README.md --- README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 3e28988..bc82497 100644 --- a/README.md +++ b/README.md @@ -38,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 @@ -58,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 From b8bce72d23bd730a5ed068311f3ae5e7b4965396 Mon Sep 17 00:00:00 2001 From: Abi Raja Date: Mon, 8 Jan 2024 17:38:34 -0800 Subject: [PATCH 08/12] organize evals code into the evals dir --- backend/eval_config.py | 1 - backend/evals/__init__.py | 0 backend/evals/config.py | 1 + backend/evals/core.py | 29 +++++++++++++++++++ backend/{eval_utils.py => evals/utils.py} | 0 backend/routes/evals.py | 4 +-- backend/{eval.py => run_evals.py} | 35 +++-------------------- 7 files changed, 36 insertions(+), 34 deletions(-) delete mode 100644 backend/eval_config.py create mode 100644 backend/evals/__init__.py create mode 100644 backend/evals/config.py create mode 100644 backend/evals/core.py rename backend/{eval_utils.py => evals/utils.py} (100%) rename backend/{eval.py => run_evals.py} (57%) diff --git a/backend/eval_config.py b/backend/eval_config.py deleted file mode 100644 index 62a3b8f..0000000 --- a/backend/eval_config.py +++ /dev/null @@ -1 +0,0 @@ -EVALS_DIR = "./evals" 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/eval_utils.py b/backend/evals/utils.py similarity index 100% rename from backend/eval_utils.py rename to backend/evals/utils.py diff --git a/backend/routes/evals.py b/backend/routes/evals.py index 48a3a95..798a9d8 100644 --- a/backend/routes/evals.py +++ b/backend/routes/evals.py @@ -1,8 +1,8 @@ import os from fastapi import APIRouter from pydantic import BaseModel -from eval_utils import image_to_data_url -from eval_config import EVALS_DIR +from evals.utils import image_to_data_url +from evals.config import EVALS_DIR router = APIRouter() diff --git a/backend/eval.py b/backend/run_evals.py similarity index 57% rename from backend/eval.py rename to backend/run_evals.py index 75641a3..ddb7eaa 100644 --- a/backend/eval.py +++ b/backend/run_evals.py @@ -1,42 +1,15 @@ # Load environment variables first -from typing import Any, Coroutine from dotenv import load_dotenv -from eval_config import EVALS_DIR -from eval_utils import image_to_data_url -from prompts.types import Stack - load_dotenv() import os -from llm import stream_openai_response -from prompts import assemble_prompt +from typing import Any, Coroutine import asyncio -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 +from evals.config import EVALS_DIR +from evals.core import generate_code_core +from evals.utils import image_to_data_url async def main(): From a8b562e392a76d87b663b713981abcda510e8bb0 Mon Sep 17 00:00:00 2001 From: Abi Raja Date: Tue, 9 Jan 2024 06:35:17 -0800 Subject: [PATCH 09/12] simplify Stack dropdown generation significantly --- .../src/components/OutputSettingsSection.tsx | 108 +++++------------- frontend/src/types.ts | 15 ++- 2 files changed, 40 insertions(+), 83 deletions(-) diff --git a/frontend/src/components/OutputSettingsSection.tsx b/frontend/src/components/OutputSettingsSection.tsx index 4398f1b..06ae6ef 100644 --- a/frontend/src/components/OutputSettingsSection.tsx +++ b/frontend/src/components/OutputSettingsSection.tsx @@ -1,3 +1,4 @@ +import React from "react"; import { Select, SelectContent, @@ -5,56 +6,22 @@ import { SelectItem, SelectTrigger, } from "./ui/select"; -import { GeneratedCodeConfig } from "../types"; +import { GeneratedCodeConfig, STACK_DESCRIPTION } from "../types"; import { Badge } from "./ui/badge"; -function generateDisplayComponent(config: GeneratedCodeConfig) { - switch (config) { - case GeneratedCodeConfig.HTML_TAILWIND: - return ( -
- HTML +{" "} - Tailwind -
- ); - case GeneratedCodeConfig.REACT_TAILWIND: - return ( -
- React +{" "} - Tailwind -
- ); - case GeneratedCodeConfig.BOOTSTRAP: - return ( -
- Bootstrap -
- ); - case GeneratedCodeConfig.IONIC_TAILWIND: - return ( -
- Ionic +{" "} - Tailwind -
- ); - case GeneratedCodeConfig.VUE_TAILWIND: - return ( -
- Vue +{" "} - Tailwind -
- ); - case GeneratedCodeConfig.SVG: - return ( -
- SVG -
- ); - default: { - const exhaustiveCheck: never = config; - throw new Error(`Unhandled case: ${exhaustiveCheck}`); - } - } +function generateDisplayComponent(stack: GeneratedCodeConfig) { + const stackComponents = STACK_DESCRIPTION[stack].components; + + return ( +
+ {stackComponents.map((component, index) => ( + + {component} + {index < stackComponents.length - 1 && " + "} + + ))} +
+ ); } interface Props { @@ -88,39 +55,18 @@ function OutputSettingsSection({ - - {generateDisplayComponent(GeneratedCodeConfig.HTML_TAILWIND)} - - - {generateDisplayComponent(GeneratedCodeConfig.REACT_TAILWIND)} - - - {generateDisplayComponent(GeneratedCodeConfig.BOOTSTRAP)} - - -
- {generateDisplayComponent(GeneratedCodeConfig.VUE_TAILWIND)} - - Beta - -
-
- -
- {generateDisplayComponent(GeneratedCodeConfig.IONIC_TAILWIND)} - - Beta - -
-
- -
- {generateDisplayComponent(GeneratedCodeConfig.SVG)} - - Beta - -
-
+ {Object.values(GeneratedCodeConfig).map((stack) => ( + +
+ {generateDisplayComponent(stack)} + {STACK_DESCRIPTION[stack].inBeta && ( + + Beta + + )} +
+
+ ))}
diff --git a/frontend/src/types.ts b/frontend/src/types.ts index e0c1026..31459cb 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -3,16 +3,27 @@ export enum EditorTheme { COBALT = "cobalt", } -// Keep in sync with backend (prompts.py) +// Keep in sync with backend (prompts/types.py) export enum GeneratedCodeConfig { HTML_TAILWIND = "html_tailwind", REACT_TAILWIND = "react_tailwind", - VUE_TAILWIND = "vue_tailwind", BOOTSTRAP = "bootstrap", + VUE_TAILWIND = "vue_tailwind", IONIC_TAILWIND = "ionic_tailwind", SVG = "svg", } +export const STACK_DESCRIPTION: { + [key in GeneratedCodeConfig]: { components: string[]; inBeta: boolean }; +} = { + html_tailwind: { components: ["HTML", "Tailwind"], inBeta: false }, + react_tailwind: { components: ["React", "Tailwind"], inBeta: false }, + bootstrap: { components: ["Bootstrap"], inBeta: false }, + vue_tailwind: { components: ["Vue", "Tailwind"], inBeta: true }, + ionic_tailwind: { components: ["Ionic", "Tailwind"], inBeta: true }, + svg: { components: ["SVG"], inBeta: true }, +}; + export interface Settings { openAiApiKey: string | null; openAiBaseURL: string | null; From 3723c81a047a6741153699b50cdf767e9768ea3e Mon Sep 17 00:00:00 2001 From: Abi Raja Date: Tue, 9 Jan 2024 06:43:32 -0800 Subject: [PATCH 10/12] move stack info to a separate file --- .../src/components/OutputSettingsSection.tsx | 7 +++--- frontend/src/lib/stacks/descriptions.ts | 12 ++++++++++ frontend/src/lib/stacks/types.ts | 9 +++++++ frontend/src/types.ts | 24 +++---------------- 4 files changed, 28 insertions(+), 24 deletions(-) create mode 100644 frontend/src/lib/stacks/descriptions.ts create mode 100644 frontend/src/lib/stacks/types.ts diff --git a/frontend/src/components/OutputSettingsSection.tsx b/frontend/src/components/OutputSettingsSection.tsx index 06ae6ef..99de309 100644 --- a/frontend/src/components/OutputSettingsSection.tsx +++ b/frontend/src/components/OutputSettingsSection.tsx @@ -6,11 +6,12 @@ import { SelectItem, SelectTrigger, } from "./ui/select"; -import { GeneratedCodeConfig, STACK_DESCRIPTION } from "../types"; import { Badge } from "./ui/badge"; +import { GeneratedCodeConfig } from "../lib/stacks/types"; +import { STACK_DESCRIPTIONS } from "../lib/stacks/descriptions"; function generateDisplayComponent(stack: GeneratedCodeConfig) { - const stackComponents = STACK_DESCRIPTION[stack].components; + const stackComponents = STACK_DESCRIPTIONS[stack].components; return (
@@ -59,7 +60,7 @@ function OutputSettingsSection({
{generateDisplayComponent(stack)} - {STACK_DESCRIPTION[stack].inBeta && ( + {STACK_DESCRIPTIONS[stack].inBeta && ( Beta diff --git a/frontend/src/lib/stacks/descriptions.ts b/frontend/src/lib/stacks/descriptions.ts new file mode 100644 index 0000000..abd75c3 --- /dev/null +++ b/frontend/src/lib/stacks/descriptions.ts @@ -0,0 +1,12 @@ +import { GeneratedCodeConfig } from "./types"; + +export const STACK_DESCRIPTIONS: { + [key in GeneratedCodeConfig]: { components: string[]; inBeta: boolean }; +} = { + html_tailwind: { components: ["HTML", "Tailwind"], inBeta: false }, + react_tailwind: { components: ["React", "Tailwind"], inBeta: false }, + bootstrap: { components: ["Bootstrap"], inBeta: false }, + vue_tailwind: { components: ["Vue", "Tailwind"], inBeta: true }, + ionic_tailwind: { components: ["Ionic", "Tailwind"], inBeta: true }, + svg: { components: ["SVG"], inBeta: true }, +}; diff --git a/frontend/src/lib/stacks/types.ts b/frontend/src/lib/stacks/types.ts new file mode 100644 index 0000000..1344836 --- /dev/null +++ b/frontend/src/lib/stacks/types.ts @@ -0,0 +1,9 @@ +// Keep in sync with backend (prompts/types.py) +export enum GeneratedCodeConfig { + HTML_TAILWIND = "html_tailwind", + REACT_TAILWIND = "react_tailwind", + BOOTSTRAP = "bootstrap", + VUE_TAILWIND = "vue_tailwind", + IONIC_TAILWIND = "ionic_tailwind", + SVG = "svg", +} diff --git a/frontend/src/types.ts b/frontend/src/types.ts index 31459cb..5a5ced7 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -1,29 +1,10 @@ +import { GeneratedCodeConfig } from "./lib/stacks/types"; + export enum EditorTheme { ESPRESSO = "espresso", COBALT = "cobalt", } -// Keep in sync with backend (prompts/types.py) -export enum GeneratedCodeConfig { - HTML_TAILWIND = "html_tailwind", - REACT_TAILWIND = "react_tailwind", - BOOTSTRAP = "bootstrap", - VUE_TAILWIND = "vue_tailwind", - IONIC_TAILWIND = "ionic_tailwind", - SVG = "svg", -} - -export const STACK_DESCRIPTION: { - [key in GeneratedCodeConfig]: { components: string[]; inBeta: boolean }; -} = { - html_tailwind: { components: ["HTML", "Tailwind"], inBeta: false }, - react_tailwind: { components: ["React", "Tailwind"], inBeta: false }, - bootstrap: { components: ["Bootstrap"], inBeta: false }, - vue_tailwind: { components: ["Vue", "Tailwind"], inBeta: true }, - ionic_tailwind: { components: ["Ionic", "Tailwind"], inBeta: true }, - svg: { components: ["SVG"], inBeta: true }, -}; - export interface Settings { openAiApiKey: string | null; openAiBaseURL: string | null; @@ -51,3 +32,4 @@ export interface CodeGenerationParams { } export type FullGenerationSettings = CodeGenerationParams & Settings; +export { GeneratedCodeConfig }; From 7bc368d9bf35e425d508ca1d307fab8c2651ef94 Mon Sep 17 00:00:00 2001 From: Abi Raja Date: Tue, 9 Jan 2024 08:12:33 -0800 Subject: [PATCH 11/12] update remaining variable names for GeneratedCodeConfig --- frontend/src/App.tsx | 22 +++++++---------- frontend/src/components/ImportCodeSection.tsx | 14 ++++------- .../src/components/OutputSettingsSection.tsx | 24 ++++++++----------- frontend/src/lib/stacks/descriptions.ts | 4 ++-- frontend/src/lib/stacks/types.ts | 2 +- frontend/src/types.ts | 5 ++-- 6 files changed, 28 insertions(+), 43 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index d0e7ee6..cab77b7 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -18,13 +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 { - AppState, - CodeGenerationParams, - EditorTheme, - GeneratedCodeConfig, - Settings, -} 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"; @@ -40,6 +34,7 @@ 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/types"; const IS_OPENAI_DOWN = false; @@ -60,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, @@ -85,7 +80,7 @@ function App() { if (!settings.generatedCodeConfig) { setSettings((prev) => ({ ...prev, - generatedCodeConfig: GeneratedCodeConfig.HTML_TAILWIND, + generatedCodeConfig: Stack.HTML_TAILWIND, })); } }, [settings.generatedCodeConfig, setSettings]); @@ -289,15 +284,14 @@ function App() { })); }; - // TODO: Rename everything to "stack" instead of "config" - function setStack(stack: GeneratedCodeConfig) { + function setStack(stack: Stack) { setSettings((prev) => ({ ...prev, generatedCodeConfig: stack, })); } - function importFromCode(code: string, stack: GeneratedCodeConfig) { + function importFromCode(code: string, stack: Stack) { setIsImportedFromCode(true); // Set up this project @@ -333,8 +327,8 @@ function App() {
setStack(config)} + stack={settings.generatedCodeConfig} + setStack={(config) => setStack(config)} shouldDisableUpdates={ appState === AppState.CODING || appState === AppState.CODE_READY } diff --git a/frontend/src/components/ImportCodeSection.tsx b/frontend/src/components/ImportCodeSection.tsx index 04b2b5a..94d3cf1 100644 --- a/frontend/src/components/ImportCodeSection.tsx +++ b/frontend/src/components/ImportCodeSection.tsx @@ -11,18 +11,16 @@ import { } from "./ui/dialog"; import { Textarea } from "./ui/textarea"; import OutputSettingsSection from "./OutputSettingsSection"; -import { GeneratedCodeConfig } from "../types"; import toast from "react-hot-toast"; +import { Stack } from "../lib/stacks/types"; interface Props { - importFromCode: (code: string, stack: GeneratedCodeConfig) => void; + importFromCode: (code: string, stack: Stack) => void; } function ImportCodeSection({ importFromCode }: Props) { const [code, setCode] = useState(""); - const [stack, setStack] = useState( - undefined - ); + const [stack, setStack] = useState(undefined); const doImport = () => { if (code === "") { @@ -57,10 +55,8 @@ function ImportCodeSection({ importFromCode }: Props) { /> - setStack(config) - } + stack={stack} + setStack={(config: Stack) => setStack(config)} label="Stack:" shouldDisableUpdates={false} /> diff --git a/frontend/src/components/OutputSettingsSection.tsx b/frontend/src/components/OutputSettingsSection.tsx index 99de309..6dc39d4 100644 --- a/frontend/src/components/OutputSettingsSection.tsx +++ b/frontend/src/components/OutputSettingsSection.tsx @@ -7,10 +7,10 @@ import { SelectTrigger, } from "./ui/select"; import { Badge } from "./ui/badge"; -import { GeneratedCodeConfig } from "../lib/stacks/types"; +import { Stack } from "../lib/stacks/types"; import { STACK_DESCRIPTIONS } from "../lib/stacks/descriptions"; -function generateDisplayComponent(stack: GeneratedCodeConfig) { +function generateDisplayComponent(stack: Stack) { const stackComponents = STACK_DESCRIPTIONS[stack].components; return ( @@ -26,15 +26,15 @@ function generateDisplayComponent(stack: GeneratedCodeConfig) { } interface Props { - generatedCodeConfig: GeneratedCodeConfig | undefined; - setGeneratedCodeConfig: (config: GeneratedCodeConfig) => void; + stack: Stack | undefined; + setStack: (config: Stack) => void; label?: string; shouldDisableUpdates?: boolean; } function OutputSettingsSection({ - generatedCodeConfig, - setGeneratedCodeConfig, + stack, + setStack, label = "Generating:", shouldDisableUpdates = false, }: Props) { @@ -43,20 +43,16 @@ function OutputSettingsSection({
{label}