diff --git a/README.md b/README.md
index 9a100ca..270beac 100644
--- a/README.md
+++ b/README.md
@@ -1,24 +1,26 @@
# screenshot-to-code
-This simple app converts a screenshot to HTML/Tailwind CSS. It uses GPT-4 Vision to generate the code and DALL-E 3 to generate similar-looking images. 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 Vue or Bootstrap). It uses GPT-4 Vision to generate the code and DALL-E 3 to generate similar-looking images. You can now also enter a URL to clone a live website!
https://github.com/abi/screenshot-to-code/assets/23818/6cebadae-2fe3-4986-ac6a-8fb9db030045
-See the [Examples](#examples) section below for more demos.
+See the [Examples](#-examples) section below for more demos.
## 🚀 Try It Out!
-🆕 [Try it here](https://picoapps.xyz/free-tools/screenshot-to-code) (bring your own OpenAI key - **your key must have access to GPT-4 Vision. See [FAQ](#%EF%B8%8F-faqs) section below for details**). Or see [Getting Started](#-getting-started) below for local install instructions.
+🆕 [Try it here](https://screenshottocode.com) (bring your own OpenAI key - **your key must have access to GPT-4 Vision. See [FAQ](#%EF%B8%8F-faqs) section below for details**). Or see [Getting Started](#-getting-started) below for local install instructions.
## 🌟 Recent Updates
+- Nov 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
- Nov 23 - Send in a screenshot of the current replicated version (sometimes improves quality of subsequent generations)
- Nov 21 - Edit code in the code editor and preview changes live thanks to [@clean99](https://github.com/clean99)
- Nov 20 - Paste in a URL to screenshot and clone (requires [ScreenshotOne free API key](https://screenshotone.com?via=screenshot-to-code))
- Nov 19 - Support for dark/light code editor theme - thanks [@kachbit](https://github.com/kachbit)
- Nov 16 - Added a setting to disable DALL-E image generation if you don't need that
- Nov 16 - View code directly within the app
-- Nov 15 - 🔥 You can now instruct the AI to update the code as you wish. It is helpful if the AI messed up some styles or missed a section.
+- Nov 15 - You can now instruct the AI to update the code as you wish. It is helpful if the AI messed up some styles or missed a section.
## 🛠 Getting Started
@@ -66,7 +68,7 @@ The app will be up and running at http://localhost:5173. Note that you can't dev
## 🙋♂️ FAQs
- **I'm running into an error when setting up the backend. How can I fix it?** [Try this](https://github.com/abi/screenshot-to-code/issues/3#issuecomment-1814777959). If that still doesn't work, open an issue.
-- **How do I get an OpenAI API key that has the GPT4 Vision model available?** Create an OpenAI account. And then, you need to buy at least $1 worth of credit on the [Billing dashboard](https://platform.openai.com/account/billing/overview). Also, see [OpenAI docs](https://help.openai.com/en/articles/7102672-how-can-i-access-gpt-4).
+- **How do I get an OpenAI API key?** See https://github.com/abi/screenshot-to-code/blob/main/Troubleshooting.md
- **How can I provide feedback?** For feedback, feature requests and bug reports, open an issue or ping me on [Twitter](https://twitter.com/_abi_).
## 📚 Examples
@@ -87,4 +89,6 @@ https://github.com/abi/screenshot-to-code/assets/23818/3fec0f77-44e8-4fb3-a769-a
## 🌍 Hosted Version
-🆕 [Try it here](https://picoapps.xyz/free-tools/screenshot-to-code) (bring your own OpenAI key - **your key must have access to GPT-4 Vision. See [FAQ](#%EF%B8%8F-faqs) section for details**). Or see [Getting Started](#-getting-started) for local install instructions.
+🆕 [Try it here](https://screenshottocode.com) (bring your own OpenAI key - **your key must have access to GPT-4 Vision. See [FAQ](#%EF%B8%8F-faqs) section for details**). Or see [Getting Started](#-getting-started) for local install instructions.
+
+[](https://www.buymeacoffee.com/abiraja)
diff --git a/Troubleshooting.md b/Troubleshooting.md
new file mode 100644
index 0000000..20fa815
--- /dev/null
+++ b/Troubleshooting.md
@@ -0,0 +1,17 @@
+### Getting an OpenAI API key with GPT4-Vision model access
+
+You don't need a ChatGPT Pro account. Screenshot to code uses API keys from your OpenAI developer account. In order to get access to the GPT4 Vision model, log into your OpenAI account and then, follow these instructions:
+
+1. Open [OpenAI Dashboard](https://platform.openai.com/)
+1. Go to Settings > Billing
+1. Click at the Add payment details
+
+4. You have to buy some credits. The minimum is $5.
+
+5. Go to Settings > Limits and check at the bottom of the page, your current tier has to be "Tier 1" to have GPT4 access
+
+6. Go to Screenshot to code and paste it in the Settings dialog under OpenAI key (gear icon). Your key is only stored in your browser. Never stored on our servers.
+
+Some users have also reported that it can take upto 30 minutes after your credit purchase for the GPT4 vision model to be activated.
+
+If you've followed these steps, and it still doesn't work, feel free to open a Github issue.
diff --git a/backend/README.md b/backend/README.md
new file mode 100644
index 0000000..7c1cad2
--- /dev/null
+++ b/backend/README.md
@@ -0,0 +1,3 @@
+Run tests
+
+pytest test_prompts.py
diff --git a/backend/access_token.py b/backend/access_token.py
new file mode 100644
index 0000000..e61ef12
--- /dev/null
+++ b/backend/access_token.py
@@ -0,0 +1,27 @@
+import json
+import os
+import httpx
+
+
+async def validate_access_token(access_code: str):
+ async with httpx.AsyncClient() as client:
+ url = (
+ "https://backend.buildpicoapps.com/screenshot_to_code/validate_access_token"
+ )
+ data = json.dumps(
+ {
+ "access_code": access_code,
+ "secret": os.environ.get("PICO_BACKEND_SECRET"),
+ }
+ )
+ headers = {"Content-Type": "application/json"}
+
+ response = await client.post(url, content=data, headers=headers)
+ response_data = response.json()
+
+ if response_data["success"]:
+ print("Access token is valid.")
+ return True
+ else:
+ print(f"Access token validation failed: {response_data['failure_reason']}")
+ return False
diff --git a/backend/image_generation.py b/backend/image_generation.py
index 080334f..bb272f8 100644
--- a/backend/image_generation.py
+++ b/backend/image_generation.py
@@ -5,8 +5,8 @@ from openai import AsyncOpenAI
from bs4 import BeautifulSoup
-async def process_tasks(prompts, api_key):
- tasks = [generate_image(prompt, api_key) for prompt in prompts]
+async def process_tasks(prompts, api_key, base_url):
+ tasks = [generate_image(prompt, api_key, base_url) for prompt in prompts]
results = await asyncio.gather(*tasks, return_exceptions=True)
processed_results = []
@@ -20,8 +20,8 @@ async def process_tasks(prompts, api_key):
return processed_results
-async def generate_image(prompt, api_key):
- client = AsyncOpenAI(api_key=api_key)
+async def generate_image(prompt, api_key, base_url):
+ client = AsyncOpenAI(api_key=api_key, base_url=base_url)
image_params = {
"model": "dall-e-3",
"quality": "standard",
@@ -31,6 +31,7 @@ async def generate_image(prompt, api_key):
"prompt": prompt,
}
res = await client.images.generate(**image_params)
+ await client.close()
return res.data[0].url
@@ -60,7 +61,7 @@ def create_alt_url_mapping(code):
return mapping
-async def generate_images(code, api_key, image_cache):
+async def generate_images(code, api_key, base_url, image_cache):
# Find all images
soup = BeautifulSoup(code, "html.parser")
images = soup.find_all("img")
@@ -87,7 +88,7 @@ async def generate_images(code, api_key, image_cache):
return code
# Generate images
- results = await process_tasks(prompts, api_key)
+ results = await process_tasks(prompts, api_key, base_url)
# Create a dict mapping alt text to image URL
mapped_image_urls = dict(zip(prompts, results))
diff --git a/backend/llm.py b/backend/llm.py
index b52c3c9..e2b41c4 100644
--- a/backend/llm.py
+++ b/backend/llm.py
@@ -6,9 +6,12 @@ MODEL_GPT_4_VISION = "gpt-4-vision-preview"
async def stream_openai_response(
- messages, api_key: str, callback: Callable[[str], Awaitable[None]]
+ messages,
+ api_key: str,
+ base_url: str | None,
+ callback: Callable[[str], Awaitable[None]],
):
- client = AsyncOpenAI(api_key=api_key)
+ client = AsyncOpenAI(api_key=api_key, base_url=base_url)
model = MODEL_GPT_4_VISION
@@ -27,4 +30,6 @@ async def stream_openai_response(
full_response += content
await callback(content)
+ await client.close()
+
return full_response
diff --git a/backend/main.py b/backend/main.py
index 1476d1b..c07e45d 100644
--- a/backend/main.py
+++ b/backend/main.py
@@ -1,6 +1,5 @@
# Load environment variables first
from dotenv import load_dotenv
-from pydantic import BaseModel
load_dotenv()
@@ -16,11 +15,10 @@ from mock import mock_completion
from image_generation import create_alt_url_mapping, generate_images
from prompts import assemble_prompt, assemble_instruction_generation_prompt
from routes import screenshot
+from access_token import validate_access_token
app = FastAPI(openapi_url=None, docs_url=None, redoc_url=None)
-# Configure CORS
-
# Configure CORS settings
app.add_middleware(
CORSMiddleware,
@@ -63,17 +61,41 @@ def write_logs(prompt_messages, completion):
async def stream_code(websocket: WebSocket):
await websocket.accept()
+ print("Incoming websocket connection...")
+
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.
- if params["openAiApiKey"]:
- openai_api_key = params["openAiApiKey"]
- print("Using OpenAI API key from client-side settings dialog")
+ openai_api_key = None
+ if "accessCode" in params and params["accessCode"]:
+ print("Access code - using platform API key")
+ if await validate_access_token(params["accessCode"]):
+ openai_api_key = os.environ.get("PLATFORM_OPENAI_API_KEY")
+ else:
+ await websocket.send_json(
+ {
+ "type": "error",
+ "value": "Invalid access code or you're out of credits. Please try again.",
+ }
+ )
+ return
else:
- openai_api_key = os.environ.get("OPENAI_API_KEY")
- if openai_api_key:
- print("Using OpenAI API key from environment variable")
+ if params["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")
@@ -85,6 +107,22 @@ async def stream_code(websocket: WebSocket):
)
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
@@ -97,10 +135,23 @@ async def stream_code(websocket: WebSocket):
async def process_chunk(content):
await websocket.send_json({"type": "chunk", "value": content})
- if params.get("resultImage") and params["resultImage"]:
- prompt_messages = assemble_prompt(params["image"], params["resultImage"])
- else:
- prompt_messages = assemble_prompt(params["image"])
+ # 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 = {}
@@ -121,6 +172,7 @@ async def stream_code(websocket: WebSocket):
completion = await stream_openai_response(
prompt_messages,
api_key=openai_api_key,
+ base_url=openai_base_url,
callback=lambda x: process_chunk(x),
)
@@ -133,7 +185,10 @@ async def stream_code(websocket: WebSocket):
{"type": "status", "value": "Generating images..."}
)
updated_html = await generate_images(
- completion, api_key=openai_api_key, image_cache=image_cache
+ completion,
+ api_key=openai_api_key,
+ base_url=openai_base_url,
+ image_cache=image_cache,
)
else:
updated_html = completion
@@ -147,8 +202,8 @@ async def stream_code(websocket: WebSocket):
await websocket.send_json(
{"type": "status", "value": "Image generation failed but code is complete."}
)
- finally:
- await websocket.close()
+
+ await websocket.close()
@app.websocket("/generate-instruction")
@@ -183,7 +238,9 @@ async def stream_code(websocket: WebSocket):
async def process_chunk(content):
await websocket.send_json({"type": "chunk", "value": content})
- prompt_messages = assemble_instruction_generation_prompt(params["image"], params["resultImage"])
+ prompt_messages = assemble_instruction_generation_prompt(
+ params["image"], params["resultImage"]
+ )
if SHOULD_MOCK_AI_RESPONSE:
completion = await mock_completion(process_chunk)
@@ -191,6 +248,7 @@ async def stream_code(websocket: WebSocket):
completion = await stream_openai_response(
prompt_messages,
api_key=openai_api_key,
+ base_url=None,
callback=lambda x: process_chunk(x),
)
@@ -209,4 +267,3 @@ async def stream_code(websocket: WebSocket):
)
finally:
await websocket.close()
-
diff --git a/backend/poetry.lock b/backend/poetry.lock
index 29e7937..889d46d 100644
--- a/backend/poetry.lock
+++ b/backend/poetry.lock
@@ -43,14 +43,14 @@ lxml = ["lxml"]
[[package]]
name = "certifi"
-version = "2023.7.22"
+version = "2023.11.17"
description = "Python package for providing Mozilla's CA Bundle."
category = "main"
optional = false
python-versions = ">=3.6"
files = [
- {file = "certifi-2023.7.22-py3-none-any.whl", hash = "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"},
- {file = "certifi-2023.7.22.tar.gz", hash = "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082"},
+ {file = "certifi-2023.11.17-py3-none-any.whl", hash = "sha256:e036ab49d5b79556f99cfc2d9320b34cfbe5be05c5871b51de9329f0603b0474"},
+ {file = "certifi-2023.11.17.tar.gz", hash = "sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1"},
]
[[package]]
@@ -94,14 +94,14 @@ files = [
[[package]]
name = "exceptiongroup"
-version = "1.1.3"
+version = "1.2.0"
description = "Backport of PEP 654 (exception groups)"
category = "main"
optional = false
python-versions = ">=3.7"
files = [
- {file = "exceptiongroup-1.1.3-py3-none-any.whl", hash = "sha256:343280667a4585d195ca1cf9cef84a4e178c4b6cf2274caef9859782b567d5e3"},
- {file = "exceptiongroup-1.1.3.tar.gz", hash = "sha256:097acd85d473d75af5bb98e41b61ff7fe35efe6675e4f9370ec6ec5126d160e9"},
+ {file = "exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14"},
+ {file = "exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"},
]
[package.extras]
@@ -165,20 +165,20 @@ trio = ["trio (>=0.22.0,<0.23.0)"]
[[package]]
name = "httpx"
-version = "0.25.1"
+version = "0.25.2"
description = "The next generation HTTP client."
category = "main"
optional = false
python-versions = ">=3.8"
files = [
- {file = "httpx-0.25.1-py3-none-any.whl", hash = "sha256:fec7d6cc5c27c578a391f7e87b9aa7d3d8fbcd034f6399f9f79b45bcc12a866a"},
- {file = "httpx-0.25.1.tar.gz", hash = "sha256:ffd96d5cf901e63863d9f1b4b6807861dbea4d301613415d9e6e57ead15fc5d0"},
+ {file = "httpx-0.25.2-py3-none-any.whl", hash = "sha256:a05d3d052d9b2dfce0e3896636467f8a5342fb2b902c819428e1ac65413ca118"},
+ {file = "httpx-0.25.2.tar.gz", hash = "sha256:8b8fcaa0c8ea7b05edd69a094e63a2094c4efcb48129fb757361bc423c0ad9e8"},
]
[package.dependencies]
anyio = "*"
certifi = "*"
-httpcore = "*"
+httpcore = ">=1.0.0,<2.0.0"
idna = "*"
sniffio = "*"
@@ -190,26 +190,26 @@ socks = ["socksio (>=1.0.0,<2.0.0)"]
[[package]]
name = "idna"
-version = "3.4"
+version = "3.6"
description = "Internationalized Domain Names in Applications (IDNA)"
category = "main"
optional = false
python-versions = ">=3.5"
files = [
- {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"},
- {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"},
+ {file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"},
+ {file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"},
]
[[package]]
name = "openai"
-version = "1.2.4"
+version = "1.3.7"
description = "The official Python library for the openai API"
category = "main"
optional = false
python-versions = ">=3.7.1"
files = [
- {file = "openai-1.2.4-py3-none-any.whl", hash = "sha256:53927a2ca276eec0a0efdc1ae829f74a51f49b7d3e14cc6f820aeafb0abfd802"},
- {file = "openai-1.2.4.tar.gz", hash = "sha256:d99a474049376be431d9b4dec3a5c895dd76e19165748c5944e80b7905d1b1ff"},
+ {file = "openai-1.3.7-py3-none-any.whl", hash = "sha256:e5c51367a910297e4d1cd33d2298fb87d7edf681edbe012873925ac16f95bee0"},
+ {file = "openai-1.3.7.tar.gz", hash = "sha256:18074a0f51f9b49d1ae268c7abc36f7f33212a0c0d08ce11b7053ab2d17798de"},
]
[package.dependencies]
@@ -217,6 +217,7 @@ anyio = ">=3.5.0,<4"
distro = ">=1.7.0,<2"
httpx = ">=0.23.0,<1"
pydantic = ">=1.9.0,<3"
+sniffio = "*"
tqdm = ">4"
typing-extensions = ">=4.5,<5"
diff --git a/backend/prompts.py b/backend/prompts.py
index 325e80b..a625efe 100644
--- a/backend/prompts.py
+++ b/backend/prompts.py
@@ -1,4 +1,4 @@
-SYSTEM_PROMPT = """
+TAILWIND_SYSTEM_PROMPT = """
You are an expert Tailwind developer
You take screenshots of a reference web page from the user, and then build single page apps
using Tailwind, HTML and JS.
@@ -23,10 +23,147 @@ 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.
+"""
+
USER_PROMPT = """
Generate code for a web page that looks exactly like this.
"""
+
+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")
+
+ user_content = [
+ {
+ "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,
+ },
+ ]
+
+
INSTUCTION_GENERATION_SYSTEM_PROMPT = """
You are a Frontend Vision Comparison expert,
You are required to compare two website screenshots: the first one is the original site and the second one is a redesigned version.
@@ -58,29 +195,6 @@ INSTUCTION_GENERATION_USER_PROMPT = """
Generate a list of differences between the two screenshots.
"""
-def assemble_prompt(image_data_url, result_image_data_url=None):
- content = [
- {
- "type": "image_url",
- "image_url": {"url": image_data_url, "detail": "high"},
- },
- {
- "type": "text",
- "text": USER_PROMPT,
- },
- ]
- if result_image_data_url:
- content.insert(1, {
- "type": "image_url",
- "image_url": {"url": result_image_data_url, "detail": "high"},
- })
- return [
- {"role": "system", "content": SYSTEM_PROMPT},
- {
- "role": "user",
- "content": content,
- },
- ]
def assemble_instruction_generation_prompt(image_data_url, result_image_data_url):
content = [
@@ -94,10 +208,13 @@ def assemble_instruction_generation_prompt(image_data_url, result_image_data_url
},
]
if result_image_data_url:
- content.insert(1, {
- "type": "image_url",
- "image_url": {"url": result_image_data_url, "detail": "high"},
- })
+ content.insert(
+ 1,
+ {
+ "type": "image_url",
+ "image_url": {"url": result_image_data_url, "detail": "high"},
+ },
+ )
return [
{"role": "system", "content": INSTUCTION_GENERATION_SYSTEM_PROMPT},
{
diff --git a/backend/test_prompts.py b/backend/test_prompts.py
new file mode 100644
index 0000000..5d8cd88
--- /dev/null
+++ b/backend/test_prompts.py
@@ -0,0 +1,136 @@
+from prompts import assemble_prompt
+
+TAILWIND_SYSTEM_PROMPT = """
+You are an expert Tailwind developer
+You take screenshots of a reference web page from the user, and then build single page apps
+using Tailwind, HTML and JS.
+You might also be given a screenshot(The second image) of a web page that you have already built, and asked to
+update it to look more like the reference image(The first image).
+
+- Make sure the app looks exactly like the screenshot.
+- Pay close attention to background color, text color, font size, font family,
+padding, margin, border, etc. Match the colors and sizes exactly.
+- Use the exact text from the screenshot.
+- Do not add comments in the code such as "" and "" in place of writing the full code. WRITE THE FULL CODE.
+- Repeat elements as needed to match the screenshot. For example, if there are 15 items, the code should have 15 items. DO NOT LEAVE comments like "" or bad things will happen.
+- For images, use placeholder images from https://placehold.co and include a detailed description of the image in the alt text so that an image generation AI can generate the image later.
+
+In terms of libraries,
+
+- Use this script to include Tailwind:
+- You can use Google Fonts
+- Font Awesome for icons:
+
+Return only the full code in tags.
+Do not include markdown "```" or "```html" at the start or end.
+"""
+
+BOOTSTRAP_SYSTEM_PROMPT = """
+You are an expert Bootstrap developer
+You take screenshots of a reference web page from the user, and then build single page apps
+using Bootstrap, HTML and JS.
+You might also be given a screenshot(The second image) of a web page that you have already built, and asked to
+update it to look more like the reference image(The first image).
+
+- Make sure the app looks exactly like the screenshot.
+- Pay close attention to background color, text color, font size, font family,
+padding, margin, border, etc. Match the colors and sizes exactly.
+- Use the exact text from the screenshot.
+- Do not add comments in the code such as "" and "" in place of writing the full code. WRITE THE FULL CODE.
+- Repeat elements as needed to match the screenshot. For example, if there are 15 items, the code should have 15 items. DO NOT LEAVE comments like "" or bad things will happen.
+- For images, use placeholder images from https://placehold.co and include a detailed description of the image in the alt text so that an image generation AI can generate the image later.
+
+In terms of libraries,
+
+- Use this script to include Bootstrap:
+- You can use Google Fonts
+- Font Awesome for icons:
+
+Return only the full code in tags.
+Do not include markdown "```" or "```html" at the start or end.
+"""
+
+REACT_TAILWIND_SYSTEM_PROMPT = """
+You are an expert React/Tailwind developer
+You take screenshots of a reference web page from the user, and then build single page apps
+using React and Tailwind CSS.
+You might also be given a screenshot(The second image) of a web page that you have already built, and asked to
+update it to look more like the reference image(The first image).
+
+- Make sure the app looks exactly like the screenshot.
+- Pay close attention to background color, text color, font size, font family,
+padding, margin, border, etc. Match the colors and sizes exactly.
+- Use the exact text from the screenshot.
+- Do not add comments in the code such as "" and "" in place of writing the full code. WRITE THE FULL CODE.
+- Repeat elements as needed to match the screenshot. For example, if there are 15 items, the code should have 15 items. DO NOT LEAVE comments like "" or bad things will happen.
+- For images, use placeholder images from https://placehold.co and include a detailed description of the image in the alt text so that an image generation AI can generate the image later.
+
+In terms of libraries,
+
+- Use these script to include React so that it can run on a standalone page:
+
+
+
+- Use this script to include Tailwind:
+- You can use Google Fonts
+- Font Awesome for icons:
+
+Return only the full code in tags.
+Do not include markdown "```" or "```html" at the start or end.
+"""
+
+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.
+"""
+
+
+def test_prompts():
+ tailwind_prompt = assemble_prompt(
+ "image_data_url", "html_tailwind", "result_image_data_url"
+ )
+ assert tailwind_prompt[0]["content"] == TAILWIND_SYSTEM_PROMPT
+
+ react_tailwind_prompt = assemble_prompt(
+ "image_data_url", "react_tailwind", "result_image_data_url"
+ )
+ assert react_tailwind_prompt[0]["content"] == REACT_TAILWIND_SYSTEM_PROMPT
+
+ bootstrap_prompt = assemble_prompt(
+ "image_data_url", "bootstrap", "result_image_data_url"
+ )
+ assert bootstrap_prompt[0]["content"] == BOOTSTRAP_SYSTEM_PROMPT
+
+ ionic_tailwind = assemble_prompt(
+ "image_data_url", "ionic_tailwind", "result_image_data_url"
+ )
+ assert ionic_tailwind[0]["content"] == IONIC_TAILWIND_SYSTEM_PROMPT
diff --git a/frontend/.gitignore b/frontend/.gitignore
index a547bf3..17ceca3 100644
--- a/frontend/.gitignore
+++ b/frontend/.gitignore
@@ -22,3 +22,6 @@ dist-ssr
*.njsproj
*.sln
*.sw?
+
+# Env files
+.env*
diff --git a/frontend/index.html b/frontend/index.html
index 0d85bb4..2a7fa0e 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -17,7 +17,38 @@
rel="stylesheet"
/>
+
+ <%- injectHead %>
+
Screenshot to Code
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/package.json b/frontend/package.json
index ef840e7..476a72d 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -5,15 +5,22 @@
"type": "module",
"scripts": {
"dev": "vite",
+ "dev-hosted": "vite --mode prod",
"build": "tsc && vite build",
+ "build-hosted": "tsc && vite build --mode prod",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
"dependencies": {
"@codemirror/lang-html": "^6.4.6",
+ "@radix-ui/react-accordion": "^1.1.2",
+ "@radix-ui/react-alert-dialog": "^1.0.5",
+ "@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.0.2",
+ "@radix-ui/react-popover": "^1.0.7",
+ "@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-separator": "^1.0.3",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-switch": "^1.0.3",
@@ -22,8 +29,8 @@
"classnames": "^2.3.2",
"clsx": "^2.0.0",
"codemirror": "^6.0.1",
- "html2canvas": "^1.4.1",
"copy-to-clipboard": "^3.3.3",
+ "html2canvas": "^1.4.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-dropzone": "^14.2.3",
@@ -48,7 +55,8 @@
"postcss": "^8.4.31",
"tailwindcss": "^3.3.5",
"typescript": "^5.0.2",
- "vite": "^4.4.5"
+ "vite": "^4.4.5",
+ "vite-plugin-html": "^3.2.0"
},
"engines": {
"node": ">=14.18.0"
diff --git a/frontend/public/brand/twitter-summary-card.png b/frontend/public/brand/twitter-summary-card.png
new file mode 100644
index 0000000..6fcedbf
Binary files /dev/null and b/frontend/public/brand/twitter-summary-card.png differ
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 956ad03..433c821 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -1,13 +1,17 @@
-import { useRef, useState, useCallback } from "react";
+import { useEffect, useRef, useState } from "react";
import ImageUpload from "./components/ImageUpload";
import CodePreview from "./components/CodePreview";
import Preview from "./components/Preview";
-import { CodeGenerationParams, InstructionGenerationParams, generateCode, generateInstruction } from "./generateCode";
+import {
+ CodeGenerationParams,
+ InstructionGenerationParams,
+ generateCode,
+ generateInstruction,
+} from "./generateCode";
import Spinner from "./components/Spinner";
import classNames from "classnames";
import {
FaCode,
- FaCopy,
FaDesktop,
FaDownload,
FaMobile,
@@ -15,14 +19,11 @@ import {
} from "react-icons/fa";
import { Switch } from "./components/ui/switch";
-import copy from "copy-to-clipboard";
-import toast from "react-hot-toast";
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "./components/ui/tabs";
-import CodeMirror from "./components/CodeMirror";
import SettingsDialog from "./components/SettingsDialog";
-import { Settings, EditorTheme, AppState } from "./types";
+import { Settings, EditorTheme, AppState, GeneratedCodeConfig } from "./types";
import { IS_RUNNING_ON_CLOUD } from "./config";
import { PicoBadge } from "./components/PicoBadge";
import { OnboardingNote } from "./components/OnboardingNote";
@@ -32,6 +33,8 @@ import TermsOfServiceDialog from "./components/TermsOfServiceDialog";
import html2canvas from "html2canvas";
import { USER_CLOSE_WEB_SOCKET_CODE } from "./constants";
import { calculateMistakesNum, handleInstructions } from "./lib/utils";
+import CodeTab from "./components/CodeTab";
+import OutputSettingsSection from "./components/OutputSettingsSection";
function App() {
const [appState, setAppState] = useState(AppState.INITIAL);
@@ -43,18 +46,35 @@ function App() {
const [settings, setSettings] = usePersistedState(
{
openAiApiKey: null,
+ openAiBaseURL: null,
screenshotOneApiKey: null,
isImageGenerationEnabled: true,
editorTheme: EditorTheme.COBALT,
+ generatedCodeConfig: GeneratedCodeConfig.HTML_TAILWIND,
+ // Only relevant for hosted version
isTermOfServiceAccepted: false,
+ accessCode: null,
},
"setting"
);
+
const [shouldIncludeResultImage, setShouldIncludeResultImage] =
useState(false);
const [mistakesNum, setMistakesNum] = useState(0);
const wsRef = useRef(null);
+ // When the user already has the settings in local storage, newly added keys
+ // do not get added to the settings so if it's falsy, we populate it with the default
+ // value
+ useEffect(() => {
+ if (!settings.generatedCodeConfig) {
+ setSettings((prev) => ({
+ ...prev,
+ generatedCodeConfig: GeneratedCodeConfig.HTML_TAILWIND,
+ }));
+ }
+ }, [settings.generatedCodeConfig, setSettings]);
+
const takeScreenshot = async (): Promise => {
const iframeElement = document.querySelector(
"#preview-desktop"
@@ -132,17 +152,19 @@ function App() {
(line) => setExecutionConsole((prev) => [...prev, line]),
() => {
setAppState(AppState.CODE_READY);
- setUpdateInstruction(instruction => {
+ setUpdateInstruction((instruction) => {
setMistakesNum(calculateMistakesNum(instruction));
return handleInstructions(instruction);
});
-
}
);
}
// Initial version creation
function doCreate(referenceImages: string[]) {
+ // Reset any existing state
+ reset();
+
setReferenceImages(referenceImages);
if (referenceImages.length > 0) {
doGenerateCode({
@@ -176,11 +198,6 @@ function App() {
setUpdateInstruction("");
}
- const doCopyCode = useCallback(() => {
- copy(generatedCode);
- toast.success("Copied to clipboard");
- }, [generatedCode]);
-
const handleTermDialogOpenChange = (open: boolean) => {
setSettings((s) => ({
...s,
@@ -195,31 +212,41 @@ function App() {
image: originalImage,
resultImage: resultImage,
});
- }
+ };
return (
-
- Please add your OpenAI API key (must have GPT4 vision access) in the
- settings dialog (gear icon above).
-
-
- How do you get an OpenAI API key that has the GPT4 Vision model available?
- Create an OpenAI account. And then, you need to buy at least $1 worth of
- credit on the Billing dashboard.
-
+