Compare commits

..

No commits in common. "main" and "video" have entirely different histories.
main ... video

106 changed files with 1612 additions and 7426 deletions

1
.github/FUNDING.yml vendored
View File

@ -1 +0,0 @@
github: [abi]

View File

@ -1,21 +0,0 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Screenshots of backend AND frontend terminal logs**
If applicable, add screenshots to help explain your problem.

View File

@ -1,10 +0,0 @@
---
name: Custom issue template
about: Describe this issue template's purpose here.
title: ''
labels: ''
assignees: ''
---

View File

@ -1,20 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@ -5,8 +5,8 @@ Evaluation dataset consists of 16 screenshots. A Python script for running scree
### Running evals ### Running evals
- Input screenshots should be located at `backend/evals_data/inputs` and the outputs will be `backend/evals_data/outputs`. If you want to modify this, modify `EVALS_DIR` in `backend/evals/config.py`. You can download the input screenshot dataset here: TODO. - Input screenshots should be located at `backend/evals_data/inputs` and the outputs will be `backend/evals_data/outputs`. If you want to modify this, modify `EVALS_DIR` in `backend/evals/config.py`. You can download the input screenshot dataset here: TODO.
- Set a stack and model (`STACK` var, `MODEL` var) in `backend/run_evals.py` - Set a stack (`STACK` var) in `backend/run_evals.py`
- Run `OPENAI_API_KEY=sk-... python run_evals.py` - this runs the screenshot-to-code on the input dataset in parallel but it will still take a few minutes to complete. - Run `python backend/run_evals.py` - this runs the screenshot-to-code on the input dataset in parallel but it will still take a few minutes to complete.
- Once the script is done, you can find the outputs in `backend/evals_data/outputs`. - Once the script is done, you can find the outputs in `backend/evals_data/outputs`.
### Rating evals ### Rating evals

View File

@ -1,50 +1,24 @@
# screenshot-to-code # screenshot-to-code
A simple tool to convert screenshots, mockups and Figma designs into clean, functional code using AI. **Now supporting Claude Sonnet 3.5 and GPT-4O!** This simple app converts a screenshot to code (HTML/Tailwind CSS, or React or Bootstrap or Vue). It uses GPT-4 Vision (or Claude 3) 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 https://github.com/abi/screenshot-to-code/assets/23818/6cebadae-2fe3-4986-ac6a-8fb9db030045
Supported stacks:
- HTML + Tailwind
- HTML + CSS
- React + Tailwind
- Vue + Tailwind
- Bootstrap
- Ionic + Tailwind
- SVG
Supported AI models:
- Claude Sonnet 3.5 - Best model!
- GPT-4O - also recommended!
- GPT-4 Turbo (Apr 2024)
- GPT-4 Vision (Nov 2023)
- Claude 3 Sonnet
- DALL-E 3 for image generation
See the [Examples](#-examples) section below for more demos. See the [Examples](#-examples) section below for more demos.
We also just added experimental support for taking a video/screen recording of a website in action and turning that into a functional prototype.
![google in app quick 3](https://github.com/abi/screenshot-to-code/assets/23818/8758ffa4-9483-4b9b-bb66-abd6d1594c33)
[Learn more about video here](https://github.com/abi/screenshot-to-code/wiki/Screen-Recording-to-Code).
[Follow me on Twitter for updates](https://twitter.com/_abi_). [Follow me on Twitter for updates](https://twitter.com/_abi_).
## 🚀 Hosted Version ## 🚀 Try It Out!
[Try it live on the hosted version (paid)](https://screenshottocode.com). 🆕 [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
- Mar 5 - Added support for Claude Sonnet 3 (as capable as or better than GPT-4 Vision, and faster!)
## 🛠 Getting Started ## 🛠 Getting Started
The app has a React/Vite frontend and a FastAPI backend. The app has a React/Vite frontend and a FastAPI backend. You will need an OpenAI API key with access to the GPT-4 Vision API.
Keys needed:
- [OpenAI API key with access to GPT-4](https://github.com/abi/screenshot-to-code/blob/main/Troubleshooting.md)
- Anthropic key (optional) - only if you want to use Claude Sonnet, or for experimental video support.
Run the backend (I use Poetry for package management - `pip install poetry` if you don't have it): Run the backend (I use Poetry for package management - `pip install poetry` if you don't have it):
@ -56,8 +30,6 @@ poetry shell
poetry run uvicorn main:app --reload --port 7001 poetry run uvicorn main:app --reload --port 7001
``` ```
If you want to use Anthropic, add `ANTHROPIC_API_KEY` to `backend/.env`. You can also set up the keys using the settings dialog on the front-end (click the gear icon after loading the frontend).
Run the frontend: Run the frontend:
```bash ```bash
@ -76,6 +48,17 @@ For debugging purposes, if you don't want to waste GPT4-Vision credits, you can
MOCK=true poetry run uvicorn main:app --reload --port 7001 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
## Using Claude 3
We recently added support for Claude 3 Sonnet. It performs well, on par or better than GPT-4 vision for many inputs, and it tends to be faster.
1. Add an env var `ANTHROPIC_API_KEY` to `backend/.env` with your API key from Anthropic
2. When using the front-end, select "Claude 3 Sonnet" from the model dropdown
## Docker ## Docker
If you have Docker installed on your system, in the root directory, run: If you have Docker installed on your system, in the root directory, run:
@ -91,9 +74,6 @@ The app will be up and running at http://localhost:5173. Note that you can't dev
- **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. - **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?** See https://github.com/abi/screenshot-to-code/blob/main/Troubleshooting.md - **How do I get an OpenAI API key?** See https://github.com/abi/screenshot-to-code/blob/main/Troubleshooting.md
- **How can I configure an OpenAI proxy?** - If you're not able to access the OpenAI API directly (due to e.g. country restrictions), you can try a VPN or you can configure the OpenAI base URL to use a proxy: Set OPENAI_BASE_URL in the `backend/.env` or directly in the UI in the settings dialog. Make sure the URL has "v1" in the path so it should look like this: `https://xxx.xxxxx.xxx/v1`
- **How can I update the backend host that my front-end connects to?** - Configure VITE_HTTP_BACKEND_URL and VITE_WS_BACKEND_URL in front/.env.local For example, set VITE_HTTP_BACKEND_URL=http://124.10.20.1:7001
- **Seeing UTF-8 errors when running the backend?** - On windows, open the .env file with notepad++, then go to Encoding and select UTF-8.
- **How can I provide feedback?** For feedback, feature requests and bug reports, open an issue or ping me on [Twitter](https://twitter.com/_abi_). - **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 ## 📚 Examples
@ -114,4 +94,6 @@ https://github.com/abi/screenshot-to-code/assets/23818/3fec0f77-44e8-4fb3-a769-a
## 🌍 Hosted Version ## 🌍 Hosted Version
🆕 [Try it here (paid)](https://screenshottocode.com). Or see [Getting Started](#-getting-started) for local install instructions to use with your own API keys. 🆕 [Try it here](https://screenshottocode.com) (bring your own OpenAI key - **your key must have access to GPT-4 Vision. See [FAQ](#%EF%B8%8F-faqs) section for details**). Or see [Getting Started](#-getting-started) for local install instructions.
[!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/abiraja)

View File

@ -1,4 +1,4 @@
### Getting an OpenAI API key with GPT-4 model access ### 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: 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:
@ -11,12 +11,8 @@ You don't need a ChatGPT Pro account. Screenshot to code uses API keys from your
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 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
<img width="900" alt="285636973-da38bd4d-8a78-4904-8027-ca67d729b933" src="https://github.com/abi/screenshot-to-code/assets/23818/8d07cd84-0cf9-4f88-bc00-80eba492eadf"> <img width="900" alt="285636973-da38bd4d-8a78-4904-8027-ca67d729b933" src="https://github.com/abi/screenshot-to-code/assets/23818/8d07cd84-0cf9-4f88-bc00-80eba492eadf">
6. Navigate to OpenAI [api keys](https://platform.openai.com/api-keys) page and create and copy a new secret key. 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.
7. 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.
## Still not working? Some users have also reported that it can take upto 30 minutes after your credit purchase for the GPT4 vision model to be activated.
- 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.
- You need to add credits to your account AND set it to renew when credits run out in order to be upgraded to Tier 1. Make sure your "Settings > Limits" page shows that you are at Tier 1.
If you've followed these steps, and it still doesn't work, feel free to open a Github issue. We only provide support for the open source version since we don't have debugging logs on the hosted version. If you're looking to use the hosted version, we recommend getting a paid subscription on screenshottocode.com

View File

@ -7,19 +7,19 @@ repos:
- id: end-of-file-fixer - id: end-of-file-fixer
- id: check-yaml - id: check-yaml
- id: check-added-large-files - id: check-added-large-files
# - repo: local - repo: local
# hooks: hooks:
# - id: poetry-pytest - id: poetry-pytest
# name: Run pytest with Poetry name: Run pytest with Poetry
# entry: poetry run --directory backend pytest entry: poetry run --directory backend pytest
language: system
pass_filenames: false
always_run: true
files: ^backend/
# - id: poetry-pyright
# name: Run pyright with Poetry
# entry: poetry run --directory backend pyright
# language: system # language: system
# pass_filenames: false # pass_filenames: false
# always_run: true # always_run: true
# files: ^backend/ # files: ^backend/
# # - id: poetry-pyright
# # name: Run pyright with Poetry
# # entry: poetry run --directory backend pyright
# # language: system
# # pass_filenames: false
# # always_run: true
# # files: ^backend/

View File

@ -1,4 +1,4 @@
FROM python:3.12.3-slim-bullseye FROM python:3.12-slim-bullseye
ENV POETRY_VERSION 1.4.1 ENV POETRY_VERSION 1.4.1

21
backend/access_token.py Normal file
View File

@ -0,0 +1,21 @@
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()
return response_data

View File

@ -1,57 +0,0 @@
import unittest
from codegen.utils import extract_html_content
class TestUtils(unittest.TestCase):
def test_extract_html_content_with_html_tags(self):
text = "<html><body><p>Hello, World!</p></body></html>"
expected = "<html><body><p>Hello, World!</p></body></html>"
result = extract_html_content(text)
self.assertEqual(result, expected)
def test_extract_html_content_without_html_tags(self):
text = "No HTML content here."
expected = "No HTML content here."
result = extract_html_content(text)
self.assertEqual(result, expected)
def test_extract_html_content_with_partial_html_tags(self):
text = "<html><body><p>Hello, World!</p></body>"
expected = "<html><body><p>Hello, World!</p></body>"
result = extract_html_content(text)
self.assertEqual(result, expected)
def test_extract_html_content_with_multiple_html_tags(self):
text = "<html><body><p>First</p></body></html> Some text <html><body><p>Second</p></body></html>"
expected = "<html><body><p>First</p></body></html>"
result = extract_html_content(text)
self.assertEqual(result, expected)
## The following are tests based on actual LLM outputs
def test_extract_html_content_some_explanation_before(self):
text = """Got it! You want the song list to be displayed horizontally. I'll update the code to ensure that the song list is displayed in a horizontal layout.
Here's the updated code:
<html lang="en"><head></head><body class="bg-black text-white"></body></html>"""
expected = '<html lang="en"><head></head><body class="bg-black text-white"></body></html>'
result = extract_html_content(text)
self.assertEqual(result, expected)
def test_markdown_tags(self):
text = "```html<head></head>```"
expected = "```html<head></head>```"
result = extract_html_content(text)
self.assertEqual(result, expected)
def test_doctype_text(self):
text = '<!DOCTYPE html><html lang="en"><head></head><body></body></html>'
expected = '<html lang="en"><head></head><body></body></html>'
result = extract_html_content(text)
self.assertEqual(result, expected)
if __name__ == "__main__":
unittest.main()

View File

@ -1,14 +0,0 @@
import re
def extract_html_content(text: str):
# Use regex to find content within <html> tags and include the tags themselves
match = re.search(r"(<html.*?>.*?</html>)", text, re.DOTALL)
if match:
return match.group(1)
else:
# Otherwise, we just send the previous HTML over
print(
"[HTML Extraction] No <html> tags found in the generated content: " + text
)
return text

View File

@ -3,21 +3,9 @@
# TODO: Should only be set to true when value is 'True', not any abitrary truthy value # TODO: Should only be set to true when value is 'True', not any abitrary truthy value
import os import os
NUM_VARIANTS = 2
# LLM-related
OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", None)
ANTHROPIC_API_KEY = os.environ.get("ANTHROPIC_API_KEY", None) ANTHROPIC_API_KEY = os.environ.get("ANTHROPIC_API_KEY", None)
OPENAI_BASE_URL = os.environ.get("OPENAI_BASE_URL", None)
# Image generation (optional)
REPLICATE_API_KEY = os.environ.get("REPLICATE_API_KEY", None)
# Debugging-related
SHOULD_MOCK_AI_RESPONSE = bool(os.environ.get("MOCK", False)) SHOULD_MOCK_AI_RESPONSE = bool(os.environ.get("MOCK", False))
IS_DEBUG_ENABLED = bool(os.environ.get("IS_DEBUG_ENABLED", False))
DEBUG_DIR = os.environ.get("DEBUG_DIR", "")
# Set to True when running in production (on the hosted version) # Set to True when running in production (on the hosted version)
# Used as a feature flag to enable or disable certain features # Used as a feature flag to enable or disable certain features

View File

@ -1,30 +0,0 @@
import os
import logging
import uuid
from config import DEBUG_DIR, IS_DEBUG_ENABLED
class DebugFileWriter:
def __init__(self):
if not IS_DEBUG_ENABLED:
return
try:
self.debug_artifacts_path = os.path.expanduser(
f"{DEBUG_DIR}/{str(uuid.uuid4())}"
)
os.makedirs(self.debug_artifacts_path, exist_ok=True)
print(f"Debugging artifacts will be stored in: {self.debug_artifacts_path}")
except:
logging.error("Failed to create debug directory")
def write_to_file(self, filename: str, content: str) -> None:
try:
with open(os.path.join(self.debug_artifacts_path, filename), "w") as file:
file.write(content)
except Exception as e:
logging.error(f"Failed to write to file: {e}")
def extract_html_content(self, text: str) -> str:
return str(text.split("<html>")[-1].rsplit("</html>", 1)[0] + "</html>")

View File

@ -1,42 +1,40 @@
from config import ANTHROPIC_API_KEY, OPENAI_API_KEY import os
from llm import Llm, stream_claude_response, stream_openai_response from config import ANTHROPIC_API_KEY
from llm import stream_claude_response, stream_openai_response
from prompts import assemble_prompt from prompts import assemble_prompt
from prompts.types import Stack from prompts.types import Stack
from openai.types.chat import ChatCompletionMessageParam
async def generate_code_for_image(image_url: str, stack: Stack, model: Llm) -> str: async def generate_code_core(image_url: str, stack: Stack) -> str:
model = "CLAUDE"
prompt_messages = assemble_prompt(image_url, stack) prompt_messages = assemble_prompt(image_url, stack)
return await generate_code_core(prompt_messages, model) openai_api_key = os.environ.get("OPENAI_API_KEY")
anthropic_api_key = ANTHROPIC_API_KEY
openai_base_url = None
async def process_chunk(content: str):
async def generate_code_core(
prompt_messages: list[ChatCompletionMessageParam], model: Llm
) -> str:
async def process_chunk(_: str):
pass pass
if model == Llm.CLAUDE_3_SONNET or model == Llm.CLAUDE_3_5_SONNET_2024_06_20: if model == "CLAUDE":
if not ANTHROPIC_API_KEY: if not anthropic_api_key:
raise Exception("Anthropic API key not found") raise Exception("Anthropic API key not found")
completion = await stream_claude_response( completion = await stream_claude_response(
prompt_messages, prompt_messages,
api_key=ANTHROPIC_API_KEY, api_key=anthropic_api_key,
callback=lambda x: process_chunk(x), callback=lambda x: process_chunk(x),
model=model,
) )
else: else:
if not OPENAI_API_KEY: if not openai_api_key:
raise Exception("OpenAI API key not found") raise Exception("OpenAI API key not found")
completion = await stream_openai_response( completion = await stream_openai_response(
prompt_messages, prompt_messages,
api_key=OPENAI_API_KEY, api_key=openai_api_key,
base_url=None, base_url=openai_base_url,
callback=lambda x: process_chunk(x), callback=lambda x: process_chunk(x),
model=model,
) )
return completion return completion

View File

@ -1,23 +0,0 @@
from datetime import datetime
import json
import os
from openai.types.chat import ChatCompletionMessageParam
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}))

View File

@ -1,72 +1,40 @@
import asyncio import asyncio
import re import re
from typing import Dict, List, Literal, Union from typing import Dict, List, Union
from openai import AsyncOpenAI from openai import AsyncOpenAI
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from image_generation.replicate import call_replicate
async def process_tasks(prompts: List[str], api_key: str, base_url: str):
async def process_tasks( tasks = [generate_image(prompt, api_key, base_url) for prompt in prompts]
prompts: List[str],
api_key: str,
base_url: str | None,
model: Literal["dalle3", "flux"],
):
import time
start_time = time.time()
if model == "dalle3":
tasks = [generate_image_dalle(prompt, api_key, base_url) for prompt in prompts]
else:
tasks = [generate_image_replicate(prompt, api_key) for prompt in prompts]
results = await asyncio.gather(*tasks, return_exceptions=True) results = await asyncio.gather(*tasks, return_exceptions=True)
end_time = time.time()
generation_time = end_time - start_time
print(f"Image generation time: {generation_time:.2f} seconds")
processed_results: List[Union[str, None]] = [] processed_results: List[Union[str, None]] = []
for result in results: for result in results:
if isinstance(result, BaseException): if isinstance(result, Exception):
print(f"An exception occurred: {result}") print(f"An exception occurred: {result}")
processed_results.append(None) processed_results.append(None)
else: else:
processed_results.append(result) processed_results.append(result) # type: ignore
return processed_results return processed_results
async def generate_image_dalle( async def generate_image(prompt: str, api_key: str, base_url: str):
prompt: str, api_key: str, base_url: str | None
) -> Union[str, None]:
client = AsyncOpenAI(api_key=api_key, base_url=base_url) client = AsyncOpenAI(api_key=api_key, base_url=base_url)
res = await client.images.generate( image_params: Dict[str, Union[str, int]] = {
model="dall-e-3", "model": "dall-e-3",
quality="standard", "quality": "standard",
style="natural", "style": "natural",
n=1, "n": 1,
size="1024x1024", "size": "1024x1024",
prompt=prompt, "prompt": prompt,
) }
res = await client.images.generate(**image_params) # type: ignore
await client.close() await client.close()
return res.data[0].url return res.data[0].url
async def generate_image_replicate(prompt: str, api_key: str) -> str:
# We use Flux Schnell
return await call_replicate(
{
"prompt": prompt,
"num_outputs": 1,
"aspect_ratio": "1:1",
"output_format": "png",
"output_quality": 100,
},
api_key,
)
def extract_dimensions(url: str): def extract_dimensions(url: str):
# Regular expression to match numbers in the format '300x200' # Regular expression to match numbers in the format '300x200'
matches = re.findall(r"(\d+)x(\d+)", url) matches = re.findall(r"(\d+)x(\d+)", url)
@ -94,18 +62,14 @@ def create_alt_url_mapping(code: str) -> Dict[str, str]:
async def generate_images( async def generate_images(
code: str, code: str, api_key: str, base_url: Union[str, None], image_cache: Dict[str, str]
api_key: str, ):
base_url: Union[str, None],
image_cache: Dict[str, str],
model: Literal["dalle3", "flux"] = "dalle3",
) -> str:
# Find all images # Find all images
soup = BeautifulSoup(code, "html.parser") soup = BeautifulSoup(code, "html.parser")
images = soup.find_all("img") images = soup.find_all("img")
# Extract alt texts as image prompts # Extract alt texts as image prompts
alts: List[str | None] = [] alts = []
for img in images: for img in images:
# Only include URL if the image starts with https://placehold.co # Only include URL if the image starts with https://placehold.co
# and it's not already in the image_cache # and it's not already in the image_cache
@ -113,26 +77,26 @@ async def generate_images(
img["src"].startswith("https://placehold.co") img["src"].startswith("https://placehold.co")
and image_cache.get(img.get("alt")) is None and image_cache.get(img.get("alt")) is None
): ):
alts.append(img.get("alt", None)) alts.append(img.get("alt", None)) # type: ignore
# Exclude images with no alt text # Exclude images with no alt text
filtered_alts: List[str] = [alt for alt in alts if alt is not None] alts = [alt for alt in alts if alt is not None] # type: ignore
# Remove duplicates # Remove duplicates
prompts = list(set(filtered_alts)) prompts = list(set(alts)) # type: ignore
# Return early if there are no images to replace # Return early if there are no images to replace
if len(prompts) == 0: if len(prompts) == 0: # type: ignore
return code return code
# Generate images # Generate images
results = await process_tasks(prompts, api_key, base_url, model) results = await process_tasks(prompts, api_key, base_url) # type: ignore
# Create a dict mapping alt text to image URL # Create a dict mapping alt text to image URL
mapped_image_urls = dict(zip(prompts, results)) mapped_image_urls = dict(zip(prompts, results)) # type: ignore
# Merge with image_cache # Merge with image_cache
mapped_image_urls = {**mapped_image_urls, **image_cache} mapped_image_urls = {**mapped_image_urls, **image_cache} # type: ignore
# Replace old image URLs with the generated URLs # Replace old image URLs with the generated URLs
for img in images: for img in images:

View File

@ -1,65 +0,0 @@
import asyncio
import httpx
async def call_replicate(input: dict[str, str | int], api_token: str) -> str:
headers = {
"Authorization": f"Bearer {api_token}",
"Content-Type": "application/json",
}
data = {"input": input}
async with httpx.AsyncClient() as client:
try:
response = await client.post(
"https://api.replicate.com/v1/models/black-forest-labs/flux-schnell/predictions",
headers=headers,
json=data,
)
response.raise_for_status()
response_json = response.json()
# Extract the id from the response
prediction_id = response_json.get("id")
if not prediction_id:
raise ValueError("Prediction ID not found in initial response.")
# Polling every 0.1 seconds until the status is succeeded or error (upto 10s)
num_polls = 0
max_polls = 100
while num_polls < max_polls:
num_polls += 1
await asyncio.sleep(0.1)
# Check the status
status_check_url = (
f"https://api.replicate.com/v1/predictions/{prediction_id}"
)
status_response = await client.get(status_check_url, headers=headers)
status_response.raise_for_status()
status_response_json = status_response.json()
status = status_response_json.get("status")
# If status is succeeded or if there's an error, break out of the loop
if status == "succeeded":
return status_response_json["output"][0]
elif status == "error":
raise ValueError(
f"Inference errored out: {status_response_json.get('error', 'Unknown error')}"
)
elif status == "failed":
raise ValueError("Inference failed")
# If we've reached here, it means we've exceeded the max number of polls
raise TimeoutError("Inference timed out")
except httpx.HTTPStatusError as e:
raise ValueError(f"HTTP error occurred: {e}")
except httpx.RequestError as e:
raise ValueError(f"An error occurred while requesting: {e}")
except asyncio.TimeoutError:
raise TimeoutError("Request timed out")
except Exception as e:
raise ValueError(f"An unexpected error occurred: {e}")

View File

@ -1,80 +0,0 @@
import base64
import io
import time
from PIL import Image
CLAUDE_IMAGE_MAX_SIZE = 5 * 1024 * 1024
CLAUDE_MAX_IMAGE_DIMENSION = 7990
# Process image so it meets Claude requirements
def process_image(image_data_url: str) -> tuple[str, str]:
# Extract bytes and media type from base64 data URL
media_type = image_data_url.split(";")[0].split(":")[1]
base64_data = image_data_url.split(",")[1]
image_bytes = base64.b64decode(base64_data)
img = Image.open(io.BytesIO(image_bytes))
# Check if image is under max dimensions and size
is_under_dimension_limit = (
img.width < CLAUDE_MAX_IMAGE_DIMENSION
and img.height < CLAUDE_MAX_IMAGE_DIMENSION
)
is_under_size_limit = len(base64_data) <= CLAUDE_IMAGE_MAX_SIZE
# If image is under both limits, no processing needed
if is_under_dimension_limit and is_under_size_limit:
print("[CLAUDE IMAGE PROCESSING] no processing needed")
return (media_type, base64_data)
# Time image processing
start_time = time.time()
# Check if either dimension exceeds 7900px (Claude disallows >= 8000px)
# Resize image if needed
if not is_under_dimension_limit:
# Calculate the new dimensions while maintaining aspect ratio
if img.width > img.height:
new_width = CLAUDE_MAX_IMAGE_DIMENSION
new_height = int((CLAUDE_MAX_IMAGE_DIMENSION / img.width) * img.height)
else:
new_height = CLAUDE_MAX_IMAGE_DIMENSION
new_width = int((CLAUDE_MAX_IMAGE_DIMENSION / img.height) * img.width)
# Resize the image
img = img.resize((new_width, new_height), Image.DEFAULT_STRATEGY)
print(
f"[CLAUDE IMAGE PROCESSING] image resized: width = {new_width}, height = {new_height}"
)
# Convert and compress as JPEG
# We always compress as JPEG (95% at the least) even when we resize and the original image
# is under the size limit.
quality = 95
output = io.BytesIO()
img = img.convert("RGB") # Ensure image is in RGB mode for JPEG conversion
img.save(output, format="JPEG", quality=quality)
# Reduce quality until image is under max size
while (
len(base64.b64encode(output.getvalue())) > CLAUDE_IMAGE_MAX_SIZE
and quality > 10
):
output = io.BytesIO()
img.save(output, format="JPEG", quality=quality)
quality -= 5
# Log so we know it was modified
old_size = len(base64_data)
new_size = len(base64.b64encode(output.getvalue()))
print(
f"[CLAUDE IMAGE PROCESSING] image size updated: old size = {old_size} bytes, new size = {new_size} bytes"
)
end_time = time.time()
processing_time = end_time - start_time
print(f"[CLAUDE IMAGE PROCESSING] processing time: {processing_time:.2f} seconds")
return ("image/jpeg", base64.b64encode(output.getvalue()).decode("utf-8"))

View File

@ -1,35 +1,18 @@
import copy
from enum import Enum
from typing import Any, Awaitable, Callable, List, cast from typing import Any, Awaitable, Callable, List, cast
from anthropic import AsyncAnthropic from anthropic import AsyncAnthropic
from openai import AsyncOpenAI from openai import AsyncOpenAI
from openai.types.chat import ChatCompletionMessageParam, ChatCompletionChunk from openai.types.chat import ChatCompletionMessageParam, ChatCompletionChunk
from config import IS_DEBUG_ENABLED
from debug.DebugFileWriter import DebugFileWriter
from image_processing.utils import process_image
from utils import pprint_prompt MODEL_GPT_4_VISION = "gpt-4-vision-preview"
MODEL_CLAUDE_SONNET = "claude-3-sonnet-20240229"
MODEL_CLAUDE_OPUS = "claude-3-opus-20240229"
# Actual model versions that are passed to the LLMs and stored in our logs # Keep in sync with frontend (lib/models.ts)
class Llm(Enum): CODE_GENERATION_MODELS = [
GPT_4_VISION = "gpt-4-vision-preview" "gpt_4_vision",
GPT_4_TURBO_2024_04_09 = "gpt-4-turbo-2024-04-09" "claude_3_sonnet",
GPT_4O_2024_05_13 = "gpt-4o-2024-05-13" ]
CLAUDE_3_SONNET = "claude-3-sonnet-20240229"
CLAUDE_3_OPUS = "claude-3-opus-20240229"
CLAUDE_3_HAIKU = "claude-3-haiku-20240307"
CLAUDE_3_5_SONNET_2024_06_20 = "claude-3-5-sonnet-20240620"
# Will throw errors if you send a garbage string
def convert_frontend_str_to_llm(frontend_str: str) -> Llm:
if frontend_str == "gpt_4_vision":
return Llm.GPT_4_VISION
elif frontend_str == "claude_3_sonnet":
return Llm.CLAUDE_3_SONNET
else:
return Llm(frontend_str)
async def stream_openai_response( async def stream_openai_response(
@ -37,37 +20,23 @@ async def stream_openai_response(
api_key: str, api_key: str,
base_url: str | None, base_url: str | None,
callback: Callable[[str], Awaitable[None]], callback: Callable[[str], Awaitable[None]],
model: Llm,
) -> str: ) -> str:
client = AsyncOpenAI(api_key=api_key, base_url=base_url) client = AsyncOpenAI(api_key=api_key, base_url=base_url)
# Base parameters model = MODEL_GPT_4_VISION
params = {
"model": model.value,
"messages": messages,
"stream": True,
"timeout": 600,
"temperature": 0.0,
}
# Add 'max_tokens' only if the model is a GPT4 vision or Turbo model # Base parameters
if ( params = {"model": model, "messages": messages, "stream": True, "timeout": 600}
model == Llm.GPT_4_VISION
or model == Llm.GPT_4_TURBO_2024_04_09 # Add 'max_tokens' only if the model is a GPT4 vision model
or model == Llm.GPT_4O_2024_05_13 if model == MODEL_GPT_4_VISION:
):
params["max_tokens"] = 4096 params["max_tokens"] = 4096
params["temperature"] = 0
stream = await client.chat.completions.create(**params) # type: ignore stream = await client.chat.completions.create(**params) # type: ignore
full_response = "" full_response = ""
async for chunk in stream: # type: ignore async for chunk in stream: # type: ignore
assert isinstance(chunk, ChatCompletionChunk) assert isinstance(chunk, ChatCompletionChunk)
if (
chunk.choices
and len(chunk.choices) > 0
and chunk.choices[0].delta
and chunk.choices[0].delta.content
):
content = chunk.choices[0].delta.content or "" content = chunk.choices[0].delta.content or ""
full_response += content full_response += content
await callback(content) await callback(content)
@ -82,22 +51,18 @@ async def stream_claude_response(
messages: List[ChatCompletionMessageParam], messages: List[ChatCompletionMessageParam],
api_key: str, api_key: str,
callback: Callable[[str], Awaitable[None]], callback: Callable[[str], Awaitable[None]],
model: Llm,
) -> str: ) -> str:
client = AsyncAnthropic(api_key=api_key) client = AsyncAnthropic(api_key=api_key)
# Base parameters # Base parameters
max_tokens = 8192 model = MODEL_CLAUDE_SONNET
max_tokens = 4096
temperature = 0.0 temperature = 0.0
# Translate OpenAI messages to Claude messages # Translate OpenAI messages to Claude messages
system_prompt = cast(str, messages[0]["content"])
# Deep copy messages to avoid modifying the original list claude_messages = [dict(message) for message in messages[1:]]
cloned_messages = copy.deepcopy(messages)
system_prompt = cast(str, cloned_messages[0].get("content"))
claude_messages = [dict(message) for message in cloned_messages[1:]]
for message in claude_messages: for message in claude_messages:
if not isinstance(message["content"], list): if not isinstance(message["content"], list):
continue continue
@ -109,10 +74,8 @@ async def stream_claude_response(
# Extract base64 data and media type from data URL # Extract base64 data and media type from data URL
# Example base64 data URL: data:image/png;base64,iVBOR... # Example base64 data URL: data:image/png;base64,iVBOR...
image_data_url = cast(str, content["image_url"]["url"]) image_data_url = cast(str, content["image_url"]["url"])
media_type = image_data_url.split(";")[0].split(":")[1]
# Process image and split media type and data base64_data = image_data_url.split(",")[1]
# so it works with Claude (under 5mb in base64 encoding)
(media_type, base64_data) = process_image(image_data_url)
# Remove OpenAI parameter # Remove OpenAI parameter
del content["image_url"] del content["image_url"]
@ -125,22 +88,17 @@ async def stream_claude_response(
# Stream Claude response # Stream Claude response
async with client.messages.stream( async with client.messages.stream(
model=model.value, model=model,
max_tokens=max_tokens, max_tokens=max_tokens,
temperature=temperature, temperature=temperature,
system=system_prompt, system=system_prompt,
messages=claude_messages, # type: ignore messages=claude_messages, # type: ignore
extra_headers={"anthropic-beta": "max-tokens-3-5-sonnet-2024-07-15"},
) as stream: ) as stream:
async for text in stream.text_stream: async for text in stream.text_stream:
await callback(text) await callback(text)
# Return final message # Return final message
response = await stream.get_final_message() response = await stream.get_final_message()
# Close the Anthropic client
await client.close()
return response.content[0].text return response.content[0].text
@ -150,84 +108,39 @@ async def stream_claude_response_native(
api_key: str, api_key: str,
callback: Callable[[str], Awaitable[None]], callback: Callable[[str], Awaitable[None]],
include_thinking: bool = False, include_thinking: bool = False,
model: Llm = Llm.CLAUDE_3_OPUS, model: str = MODEL_CLAUDE_OPUS,
) -> str: ) -> str:
client = AsyncAnthropic(api_key=api_key) client = AsyncAnthropic(api_key=api_key)
# Base model parameters # Base parameters
max_tokens = 4096 max_tokens = 4096
temperature = 0.0 temperature = 0.0
# Multi-pass flow # Stream Claude response
current_pass_num = 1
max_passes = 2
prefix = "<thinking>"
response = None
# For debugging
full_stream = ""
debug_file_writer = DebugFileWriter()
while current_pass_num <= max_passes:
current_pass_num += 1
# Set up message depending on whether we have a <thinking> prefix # Set up message depending on whether we have a <thinking> prefix
messages_to_send = ( messages = (
messages + [{"role": "assistant", "content": prefix}] messages + [{"role": "assistant", "content": "<thinking>"}]
if include_thinking if include_thinking
else messages else messages
) )
pprint_prompt(messages_to_send)
async with client.messages.stream( async with client.messages.stream(
model=model.value, model=model,
max_tokens=max_tokens, max_tokens=max_tokens,
temperature=temperature, temperature=temperature,
system=system_prompt, system=system_prompt,
messages=messages_to_send, # type: ignore messages=messages, # type: ignore
) as stream: ) as stream:
async for text in stream.text_stream: async for text in stream.text_stream:
print(text, end="", flush=True)
full_stream += text
await callback(text) await callback(text)
# Return final message
response = await stream.get_final_message() response = await stream.get_final_message()
response_text = response.content[0].text
# Write each pass's code to .html file and thinking to .txt file
if IS_DEBUG_ENABLED:
debug_file_writer.write_to_file(
f"pass_{current_pass_num - 1}.html",
debug_file_writer.extract_html_content(response_text),
)
debug_file_writer.write_to_file(
f"thinking_pass_{current_pass_num - 1}.txt",
response_text.split("</thinking>")[0],
)
# Set up messages array for next pass
messages += [
{"role": "assistant", "content": str(prefix) + response.content[0].text},
{
"role": "user",
"content": "You've done a good job with a first draft. Improve this further based on the original instructions so that the app is fully functional and looks like the original video of the app we're trying to replicate.",
},
]
print( print(
f"Token usage: Input Tokens: {response.usage.input_tokens}, Output Tokens: {response.usage.output_tokens}" f"Token usage: Input Tokens: {response.usage.input_tokens}, Output Tokens: {response.usage.output_tokens}"
) )
# Close the Anthropic client
await client.close()
if IS_DEBUG_ENABLED:
debug_file_writer.write_to_file("full_stream.txt", full_stream)
if not response:
raise Exception("No HTML response found in AI response")
else:
return response.content[0].text return response.content[0].text

File diff suppressed because it is too large Load Diff

607
backend/poetry.lock generated
View File

@ -1,115 +1,5 @@
# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. # This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand.
[[package]]
name = "aiohttp"
version = "3.9.5"
description = "Async http client/server framework (asyncio)"
optional = false
python-versions = ">=3.8"
files = [
{file = "aiohttp-3.9.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fcde4c397f673fdec23e6b05ebf8d4751314fa7c24f93334bf1f1364c1c69ac7"},
{file = "aiohttp-3.9.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5d6b3f1fabe465e819aed2c421a6743d8debbde79b6a8600739300630a01bf2c"},
{file = "aiohttp-3.9.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6ae79c1bc12c34082d92bf9422764f799aee4746fd7a392db46b7fd357d4a17a"},
{file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d3ebb9e1316ec74277d19c5f482f98cc65a73ccd5430540d6d11682cd857430"},
{file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84dabd95154f43a2ea80deffec9cb44d2e301e38a0c9d331cc4aa0166fe28ae3"},
{file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c8a02fbeca6f63cb1f0475c799679057fc9268b77075ab7cf3f1c600e81dd46b"},
{file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c26959ca7b75ff768e2776d8055bf9582a6267e24556bb7f7bd29e677932be72"},
{file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:714d4e5231fed4ba2762ed489b4aec07b2b9953cf4ee31e9871caac895a839c0"},
{file = "aiohttp-3.9.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e7a6a8354f1b62e15d48e04350f13e726fa08b62c3d7b8401c0a1314f02e3558"},
{file = "aiohttp-3.9.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:c413016880e03e69d166efb5a1a95d40f83d5a3a648d16486592c49ffb76d0db"},
{file = "aiohttp-3.9.5-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:ff84aeb864e0fac81f676be9f4685f0527b660f1efdc40dcede3c251ef1e867f"},
{file = "aiohttp-3.9.5-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:ad7f2919d7dac062f24d6f5fe95d401597fbb015a25771f85e692d043c9d7832"},
{file = "aiohttp-3.9.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:702e2c7c187c1a498a4e2b03155d52658fdd6fda882d3d7fbb891a5cf108bb10"},
{file = "aiohttp-3.9.5-cp310-cp310-win32.whl", hash = "sha256:67c3119f5ddc7261d47163ed86d760ddf0e625cd6246b4ed852e82159617b5fb"},
{file = "aiohttp-3.9.5-cp310-cp310-win_amd64.whl", hash = "sha256:471f0ef53ccedec9995287f02caf0c068732f026455f07db3f01a46e49d76bbb"},
{file = "aiohttp-3.9.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e0ae53e33ee7476dd3d1132f932eeb39bf6125083820049d06edcdca4381f342"},
{file = "aiohttp-3.9.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c088c4d70d21f8ca5c0b8b5403fe84a7bc8e024161febdd4ef04575ef35d474d"},
{file = "aiohttp-3.9.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:639d0042b7670222f33b0028de6b4e2fad6451462ce7df2af8aee37dcac55424"},
{file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f26383adb94da5e7fb388d441bf09c61e5e35f455a3217bfd790c6b6bc64b2ee"},
{file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:66331d00fb28dc90aa606d9a54304af76b335ae204d1836f65797d6fe27f1ca2"},
{file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4ff550491f5492ab5ed3533e76b8567f4b37bd2995e780a1f46bca2024223233"},
{file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f22eb3a6c1080d862befa0a89c380b4dafce29dc6cd56083f630073d102eb595"},
{file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a81b1143d42b66ffc40a441379387076243ef7b51019204fd3ec36b9f69e77d6"},
{file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f64fd07515dad67f24b6ea4a66ae2876c01031de91c93075b8093f07c0a2d93d"},
{file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:93e22add827447d2e26d67c9ac0161756007f152fdc5210277d00a85f6c92323"},
{file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:55b39c8684a46e56ef8c8d24faf02de4a2b2ac60d26cee93bc595651ff545de9"},
{file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4715a9b778f4293b9f8ae7a0a7cef9829f02ff8d6277a39d7f40565c737d3771"},
{file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:afc52b8d969eff14e069a710057d15ab9ac17cd4b6753042c407dcea0e40bf75"},
{file = "aiohttp-3.9.5-cp311-cp311-win32.whl", hash = "sha256:b3df71da99c98534be076196791adca8819761f0bf6e08e07fd7da25127150d6"},
{file = "aiohttp-3.9.5-cp311-cp311-win_amd64.whl", hash = "sha256:88e311d98cc0bf45b62fc46c66753a83445f5ab20038bcc1b8a1cc05666f428a"},
{file = "aiohttp-3.9.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:c7a4b7a6cf5b6eb11e109a9755fd4fda7d57395f8c575e166d363b9fc3ec4678"},
{file = "aiohttp-3.9.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:0a158704edf0abcac8ac371fbb54044f3270bdbc93e254a82b6c82be1ef08f3c"},
{file = "aiohttp-3.9.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d153f652a687a8e95ad367a86a61e8d53d528b0530ef382ec5aaf533140ed00f"},
{file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82a6a97d9771cb48ae16979c3a3a9a18b600a8505b1115cfe354dfb2054468b4"},
{file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60cdbd56f4cad9f69c35eaac0fbbdf1f77b0ff9456cebd4902f3dd1cf096464c"},
{file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8676e8fd73141ded15ea586de0b7cda1542960a7b9ad89b2b06428e97125d4fa"},
{file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da00da442a0e31f1c69d26d224e1efd3a1ca5bcbf210978a2ca7426dfcae9f58"},
{file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18f634d540dd099c262e9f887c8bbacc959847cfe5da7a0e2e1cf3f14dbf2daf"},
{file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:320e8618eda64e19d11bdb3bd04ccc0a816c17eaecb7e4945d01deee2a22f95f"},
{file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:2faa61a904b83142747fc6a6d7ad8fccff898c849123030f8e75d5d967fd4a81"},
{file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:8c64a6dc3fe5db7b1b4d2b5cb84c4f677768bdc340611eca673afb7cf416ef5a"},
{file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:393c7aba2b55559ef7ab791c94b44f7482a07bf7640d17b341b79081f5e5cd1a"},
{file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:c671dc117c2c21a1ca10c116cfcd6e3e44da7fcde37bf83b2be485ab377b25da"},
{file = "aiohttp-3.9.5-cp312-cp312-win32.whl", hash = "sha256:5a7ee16aab26e76add4afc45e8f8206c95d1d75540f1039b84a03c3b3800dd59"},
{file = "aiohttp-3.9.5-cp312-cp312-win_amd64.whl", hash = "sha256:5ca51eadbd67045396bc92a4345d1790b7301c14d1848feaac1d6a6c9289e888"},
{file = "aiohttp-3.9.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:694d828b5c41255e54bc2dddb51a9f5150b4eefa9886e38b52605a05d96566e8"},
{file = "aiohttp-3.9.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0605cc2c0088fcaae79f01c913a38611ad09ba68ff482402d3410bf59039bfb8"},
{file = "aiohttp-3.9.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4558e5012ee03d2638c681e156461d37b7a113fe13970d438d95d10173d25f78"},
{file = "aiohttp-3.9.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dbc053ac75ccc63dc3a3cc547b98c7258ec35a215a92bd9f983e0aac95d3d5b"},
{file = "aiohttp-3.9.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4109adee842b90671f1b689901b948f347325045c15f46b39797ae1bf17019de"},
{file = "aiohttp-3.9.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6ea1a5b409a85477fd8e5ee6ad8f0e40bf2844c270955e09360418cfd09abac"},
{file = "aiohttp-3.9.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3c2890ca8c59ee683fd09adf32321a40fe1cf164e3387799efb2acebf090c11"},
{file = "aiohttp-3.9.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3916c8692dbd9d55c523374a3b8213e628424d19116ac4308e434dbf6d95bbdd"},
{file = "aiohttp-3.9.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8d1964eb7617907c792ca00b341b5ec3e01ae8c280825deadbbd678447b127e1"},
{file = "aiohttp-3.9.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:d5ab8e1f6bee051a4bf6195e38a5c13e5e161cb7bad83d8854524798bd9fcd6e"},
{file = "aiohttp-3.9.5-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:52c27110f3862a1afbcb2af4281fc9fdc40327fa286c4625dfee247c3ba90156"},
{file = "aiohttp-3.9.5-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:7f64cbd44443e80094309875d4f9c71d0401e966d191c3d469cde4642bc2e031"},
{file = "aiohttp-3.9.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8b4f72fbb66279624bfe83fd5eb6aea0022dad8eec62b71e7bf63ee1caadeafe"},
{file = "aiohttp-3.9.5-cp38-cp38-win32.whl", hash = "sha256:6380c039ec52866c06d69b5c7aad5478b24ed11696f0e72f6b807cfb261453da"},
{file = "aiohttp-3.9.5-cp38-cp38-win_amd64.whl", hash = "sha256:da22dab31d7180f8c3ac7c7635f3bcd53808f374f6aa333fe0b0b9e14b01f91a"},
{file = "aiohttp-3.9.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:1732102949ff6087589408d76cd6dea656b93c896b011ecafff418c9661dc4ed"},
{file = "aiohttp-3.9.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c6021d296318cb6f9414b48e6a439a7f5d1f665464da507e8ff640848ee2a58a"},
{file = "aiohttp-3.9.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:239f975589a944eeb1bad26b8b140a59a3a320067fb3cd10b75c3092405a1372"},
{file = "aiohttp-3.9.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b7b30258348082826d274504fbc7c849959f1989d86c29bc355107accec6cfb"},
{file = "aiohttp-3.9.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cd2adf5c87ff6d8b277814a28a535b59e20bfea40a101db6b3bdca7e9926bc24"},
{file = "aiohttp-3.9.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e9a3d838441bebcf5cf442700e3963f58b5c33f015341f9ea86dcd7d503c07e2"},
{file = "aiohttp-3.9.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e3a1ae66e3d0c17cf65c08968a5ee3180c5a95920ec2731f53343fac9bad106"},
{file = "aiohttp-3.9.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9c69e77370cce2d6df5d12b4e12bdcca60c47ba13d1cbbc8645dd005a20b738b"},
{file = "aiohttp-3.9.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0cbf56238f4bbf49dab8c2dc2e6b1b68502b1e88d335bea59b3f5b9f4c001475"},
{file = "aiohttp-3.9.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d1469f228cd9ffddd396d9948b8c9cd8022b6d1bf1e40c6f25b0fb90b4f893ed"},
{file = "aiohttp-3.9.5-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:45731330e754f5811c314901cebdf19dd776a44b31927fa4b4dbecab9e457b0c"},
{file = "aiohttp-3.9.5-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:3fcb4046d2904378e3aeea1df51f697b0467f2aac55d232c87ba162709478c46"},
{file = "aiohttp-3.9.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8cf142aa6c1a751fcb364158fd710b8a9be874b81889c2bd13aa8893197455e2"},
{file = "aiohttp-3.9.5-cp39-cp39-win32.whl", hash = "sha256:7b179eea70833c8dee51ec42f3b4097bd6370892fa93f510f76762105568cf09"},
{file = "aiohttp-3.9.5-cp39-cp39-win_amd64.whl", hash = "sha256:38d80498e2e169bc61418ff36170e0aad0cd268da8b38a17c4cf29d254a8b3f1"},
{file = "aiohttp-3.9.5.tar.gz", hash = "sha256:edea7d15772ceeb29db4aff55e482d4bcfb6ae160ce144f2682de02f6d693551"},
]
[package.dependencies]
aiosignal = ">=1.1.2"
async-timeout = {version = ">=4.0,<5.0", markers = "python_version < \"3.11\""}
attrs = ">=17.3.0"
frozenlist = ">=1.1.1"
multidict = ">=4.5,<7.0"
yarl = ">=1.0,<2.0"
[package.extras]
speedups = ["Brotli", "aiodns", "brotlicffi"]
[[package]]
name = "aiosignal"
version = "1.3.1"
description = "aiosignal: a list of registered asynchronous callbacks"
optional = false
python-versions = ">=3.7"
files = [
{file = "aiosignal-1.3.1-py3-none-any.whl", hash = "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17"},
{file = "aiosignal-1.3.1.tar.gz", hash = "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc"},
]
[package.dependencies]
frozenlist = ">=1.1.0"
[[package]] [[package]]
name = "anthropic" name = "anthropic"
version = "0.18.1" version = "0.18.1"
@ -156,36 +46,6 @@ doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphin
test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"]
trio = ["trio (>=0.23)"] trio = ["trio (>=0.23)"]
[[package]]
name = "async-timeout"
version = "4.0.3"
description = "Timeout context manager for asyncio programs"
optional = false
python-versions = ">=3.7"
files = [
{file = "async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f"},
{file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"},
]
[[package]]
name = "attrs"
version = "23.2.0"
description = "Classes Without Boilerplate"
optional = false
python-versions = ">=3.7"
files = [
{file = "attrs-23.2.0-py3-none-any.whl", hash = "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1"},
{file = "attrs-23.2.0.tar.gz", hash = "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30"},
]
[package.extras]
cov = ["attrs[tests]", "coverage[toml] (>=5.3)"]
dev = ["attrs[tests]", "pre-commit"]
docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"]
tests = ["attrs[tests-no-zope]", "zope-interface"]
tests-mypy = ["mypy (>=1.6)", "pytest-mypy-plugins"]
tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"]
[[package]] [[package]]
name = "beautifulsoup4" name = "beautifulsoup4"
version = "4.12.3" version = "4.12.3"
@ -437,92 +297,6 @@ docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1
testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"] testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"]
typing = ["typing-extensions (>=4.8)"] typing = ["typing-extensions (>=4.8)"]
[[package]]
name = "frozenlist"
version = "1.4.1"
description = "A list-like structure which implements collections.abc.MutableSequence"
optional = false
python-versions = ">=3.8"
files = [
{file = "frozenlist-1.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f9aa1878d1083b276b0196f2dfbe00c9b7e752475ed3b682025ff20c1c1f51ac"},
{file = "frozenlist-1.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:29acab3f66f0f24674b7dc4736477bcd4bc3ad4b896f5f45379a67bce8b96868"},
{file = "frozenlist-1.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:74fb4bee6880b529a0c6560885fce4dc95936920f9f20f53d99a213f7bf66776"},
{file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:590344787a90ae57d62511dd7c736ed56b428f04cd8c161fcc5e7232c130c69a"},
{file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:068b63f23b17df8569b7fdca5517edef76171cf3897eb68beb01341131fbd2ad"},
{file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c849d495bf5154cd8da18a9eb15db127d4dba2968d88831aff6f0331ea9bd4c"},
{file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9750cc7fe1ae3b1611bb8cfc3f9ec11d532244235d75901fb6b8e42ce9229dfe"},
{file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9b2de4cf0cdd5bd2dee4c4f63a653c61d2408055ab77b151c1957f221cabf2a"},
{file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0633c8d5337cb5c77acbccc6357ac49a1770b8c487e5b3505c57b949b4b82e98"},
{file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:27657df69e8801be6c3638054e202a135c7f299267f1a55ed3a598934f6c0d75"},
{file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:f9a3ea26252bd92f570600098783d1371354d89d5f6b7dfd87359d669f2109b5"},
{file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:4f57dab5fe3407b6c0c1cc907ac98e8a189f9e418f3b6e54d65a718aaafe3950"},
{file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e02a0e11cf6597299b9f3bbd3f93d79217cb90cfd1411aec33848b13f5c656cc"},
{file = "frozenlist-1.4.1-cp310-cp310-win32.whl", hash = "sha256:a828c57f00f729620a442881cc60e57cfcec6842ba38e1b19fd3e47ac0ff8dc1"},
{file = "frozenlist-1.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:f56e2333dda1fe0f909e7cc59f021eba0d2307bc6f012a1ccf2beca6ba362439"},
{file = "frozenlist-1.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a0cb6f11204443f27a1628b0e460f37fb30f624be6051d490fa7d7e26d4af3d0"},
{file = "frozenlist-1.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b46c8ae3a8f1f41a0d2ef350c0b6e65822d80772fe46b653ab6b6274f61d4a49"},
{file = "frozenlist-1.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fde5bd59ab5357e3853313127f4d3565fc7dad314a74d7b5d43c22c6a5ed2ced"},
{file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:722e1124aec435320ae01ee3ac7bec11a5d47f25d0ed6328f2273d287bc3abb0"},
{file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2471c201b70d58a0f0c1f91261542a03d9a5e088ed3dc6c160d614c01649c106"},
{file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c757a9dd70d72b076d6f68efdbb9bc943665ae954dad2801b874c8c69e185068"},
{file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f146e0911cb2f1da549fc58fc7bcd2b836a44b79ef871980d605ec392ff6b0d2"},
{file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f9c515e7914626b2a2e1e311794b4c35720a0be87af52b79ff8e1429fc25f19"},
{file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c302220494f5c1ebeb0912ea782bcd5e2f8308037b3c7553fad0e48ebad6ad82"},
{file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:442acde1e068288a4ba7acfe05f5f343e19fac87bfc96d89eb886b0363e977ec"},
{file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:1b280e6507ea8a4fa0c0a7150b4e526a8d113989e28eaaef946cc77ffd7efc0a"},
{file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:fe1a06da377e3a1062ae5fe0926e12b84eceb8a50b350ddca72dc85015873f74"},
{file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:db9e724bebd621d9beca794f2a4ff1d26eed5965b004a97f1f1685a173b869c2"},
{file = "frozenlist-1.4.1-cp311-cp311-win32.whl", hash = "sha256:e774d53b1a477a67838a904131c4b0eef6b3d8a651f8b138b04f748fccfefe17"},
{file = "frozenlist-1.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:fb3c2db03683b5767dedb5769b8a40ebb47d6f7f45b1b3e3b4b51ec8ad9d9825"},
{file = "frozenlist-1.4.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:1979bc0aeb89b33b588c51c54ab0161791149f2461ea7c7c946d95d5f93b56ae"},
{file = "frozenlist-1.4.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:cc7b01b3754ea68a62bd77ce6020afaffb44a590c2289089289363472d13aedb"},
{file = "frozenlist-1.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c9c92be9fd329ac801cc420e08452b70e7aeab94ea4233a4804f0915c14eba9b"},
{file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c3894db91f5a489fc8fa6a9991820f368f0b3cbdb9cd8849547ccfab3392d86"},
{file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ba60bb19387e13597fb059f32cd4d59445d7b18b69a745b8f8e5db0346f33480"},
{file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8aefbba5f69d42246543407ed2461db31006b0f76c4e32dfd6f42215a2c41d09"},
{file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:780d3a35680ced9ce682fbcf4cb9c2bad3136eeff760ab33707b71db84664e3a"},
{file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9acbb16f06fe7f52f441bb6f413ebae6c37baa6ef9edd49cdd567216da8600cd"},
{file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:23b701e65c7b36e4bf15546a89279bd4d8675faabc287d06bbcfac7d3c33e1e6"},
{file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:3e0153a805a98f5ada7e09826255ba99fb4f7524bb81bf6b47fb702666484ae1"},
{file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:dd9b1baec094d91bf36ec729445f7769d0d0cf6b64d04d86e45baf89e2b9059b"},
{file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:1a4471094e146b6790f61b98616ab8e44f72661879cc63fa1049d13ef711e71e"},
{file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5667ed53d68d91920defdf4035d1cdaa3c3121dc0b113255124bcfada1cfa1b8"},
{file = "frozenlist-1.4.1-cp312-cp312-win32.whl", hash = "sha256:beee944ae828747fd7cb216a70f120767fc9f4f00bacae8543c14a6831673f89"},
{file = "frozenlist-1.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:64536573d0a2cb6e625cf309984e2d873979709f2cf22839bf2d61790b448ad5"},
{file = "frozenlist-1.4.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:20b51fa3f588ff2fe658663db52a41a4f7aa6c04f6201449c6c7c476bd255c0d"},
{file = "frozenlist-1.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:410478a0c562d1a5bcc2f7ea448359fcb050ed48b3c6f6f4f18c313a9bdb1826"},
{file = "frozenlist-1.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c6321c9efe29975232da3bd0af0ad216800a47e93d763ce64f291917a381b8eb"},
{file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48f6a4533887e189dae092f1cf981f2e3885175f7a0f33c91fb5b7b682b6bab6"},
{file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6eb73fa5426ea69ee0e012fb59cdc76a15b1283d6e32e4f8dc4482ec67d1194d"},
{file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fbeb989b5cc29e8daf7f976b421c220f1b8c731cbf22b9130d8815418ea45887"},
{file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32453c1de775c889eb4e22f1197fe3bdfe457d16476ea407472b9442e6295f7a"},
{file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:693945278a31f2086d9bf3df0fe8254bbeaef1fe71e1351c3bd730aa7d31c41b"},
{file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:1d0ce09d36d53bbbe566fe296965b23b961764c0bcf3ce2fa45f463745c04701"},
{file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:3a670dc61eb0d0eb7080890c13de3066790f9049b47b0de04007090807c776b0"},
{file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:dca69045298ce5c11fd539682cff879cc1e664c245d1c64da929813e54241d11"},
{file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a06339f38e9ed3a64e4c4e43aec7f59084033647f908e4259d279a52d3757d09"},
{file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b7f2f9f912dca3934c1baec2e4585a674ef16fe00218d833856408c48d5beee7"},
{file = "frozenlist-1.4.1-cp38-cp38-win32.whl", hash = "sha256:e7004be74cbb7d9f34553a5ce5fb08be14fb33bc86f332fb71cbe5216362a497"},
{file = "frozenlist-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:5a7d70357e7cee13f470c7883a063aae5fe209a493c57d86eb7f5a6f910fae09"},
{file = "frozenlist-1.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:bfa4a17e17ce9abf47a74ae02f32d014c5e9404b6d9ac7f729e01562bbee601e"},
{file = "frozenlist-1.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b7e3ed87d4138356775346e6845cccbe66cd9e207f3cd11d2f0b9fd13681359d"},
{file = "frozenlist-1.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c99169d4ff810155ca50b4da3b075cbde79752443117d89429595c2e8e37fed8"},
{file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:edb678da49d9f72c9f6c609fbe41a5dfb9a9282f9e6a2253d5a91e0fc382d7c0"},
{file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6db4667b187a6742b33afbbaf05a7bc551ffcf1ced0000a571aedbb4aa42fc7b"},
{file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55fdc093b5a3cb41d420884cdaf37a1e74c3c37a31f46e66286d9145d2063bd0"},
{file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82e8211d69a4f4bc360ea22cd6555f8e61a1bd211d1d5d39d3d228b48c83a897"},
{file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89aa2c2eeb20957be2d950b85974b30a01a762f3308cd02bb15e1ad632e22dc7"},
{file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9d3e0c25a2350080e9319724dede4f31f43a6c9779be48021a7f4ebde8b2d742"},
{file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7268252af60904bf52c26173cbadc3a071cece75f873705419c8681f24d3edea"},
{file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:0c250a29735d4f15321007fb02865f0e6b6a41a6b88f1f523ca1596ab5f50bd5"},
{file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:96ec70beabbd3b10e8bfe52616a13561e58fe84c0101dd031dc78f250d5128b9"},
{file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:23b2d7679b73fe0e5a4560b672a39f98dfc6f60df63823b0a9970525325b95f6"},
{file = "frozenlist-1.4.1-cp39-cp39-win32.whl", hash = "sha256:a7496bfe1da7fb1a4e1cc23bb67c58fab69311cc7d32b5a99c2007b4b2a0e932"},
{file = "frozenlist-1.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:e6a20a581f9ce92d389a8c7d7c3dd47c81fd5d6e655c8dddf341e14aa48659d0"},
{file = "frozenlist-1.4.1-py3-none-any.whl", hash = "sha256:04ced3e6a46b4cfffe20f9ae482818e34eba9b5fb0ce4056e4cc9b6e212d09b7"},
{file = "frozenlist-1.4.1.tar.gz", hash = "sha256:c037a86e8513059a2613aaba4d817bb90b9d9b6b69aace3ce9c877e8c8ed402b"},
]
[[package]] [[package]]
name = "fsspec" name = "fsspec"
version = "2024.2.0" version = "2024.2.0"
@ -757,105 +531,6 @@ doc = ["Sphinx (>=1.5.2,<2.0)", "numpydoc (>=0.6.0,<1.0)", "pygame (>=1.9.3,<2.0
optional = ["matplotlib (>=2.0.0,<3.0)", "opencv-python (>=3.0,<4.0)", "scikit-image (>=0.13.0,<1.0)", "scikit-learn", "scipy (>=0.19.0,<1.5)", "youtube_dl"] optional = ["matplotlib (>=2.0.0,<3.0)", "opencv-python (>=3.0,<4.0)", "scikit-image (>=0.13.0,<1.0)", "scikit-learn", "scipy (>=0.19.0,<1.5)", "youtube_dl"]
test = ["coverage (<5.0)", "coveralls (>=1.1,<2.0)", "pytest (>=3.0.0,<4.0)", "pytest-cov (>=2.5.1,<3.0)", "requests (>=2.8.1,<3.0)"] test = ["coverage (<5.0)", "coveralls (>=1.1,<2.0)", "pytest (>=3.0.0,<4.0)", "pytest-cov (>=2.5.1,<3.0)", "requests (>=2.8.1,<3.0)"]
[[package]]
name = "multidict"
version = "6.0.5"
description = "multidict implementation"
optional = false
python-versions = ">=3.7"
files = [
{file = "multidict-6.0.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:228b644ae063c10e7f324ab1ab6b548bdf6f8b47f3ec234fef1093bc2735e5f9"},
{file = "multidict-6.0.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:896ebdcf62683551312c30e20614305f53125750803b614e9e6ce74a96232604"},
{file = "multidict-6.0.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:411bf8515f3be9813d06004cac41ccf7d1cd46dfe233705933dd163b60e37600"},
{file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d147090048129ce3c453f0292e7697d333db95e52616b3793922945804a433c"},
{file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:215ed703caf15f578dca76ee6f6b21b7603791ae090fbf1ef9d865571039ade5"},
{file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c6390cf87ff6234643428991b7359b5f59cc15155695deb4eda5c777d2b880f"},
{file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21fd81c4ebdb4f214161be351eb5bcf385426bf023041da2fd9e60681f3cebae"},
{file = "multidict-6.0.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3cc2ad10255f903656017363cd59436f2111443a76f996584d1077e43ee51182"},
{file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6939c95381e003f54cd4c5516740faba40cf5ad3eeff460c3ad1d3e0ea2549bf"},
{file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:220dd781e3f7af2c2c1053da9fa96d9cf3072ca58f057f4c5adaaa1cab8fc442"},
{file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:766c8f7511df26d9f11cd3a8be623e59cca73d44643abab3f8c8c07620524e4a"},
{file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:fe5d7785250541f7f5019ab9cba2c71169dc7d74d0f45253f8313f436458a4ef"},
{file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1c1496e73051918fcd4f58ff2e0f2f3066d1c76a0c6aeffd9b45d53243702cc"},
{file = "multidict-6.0.5-cp310-cp310-win32.whl", hash = "sha256:7afcdd1fc07befad18ec4523a782cde4e93e0a2bf71239894b8d61ee578c1319"},
{file = "multidict-6.0.5-cp310-cp310-win_amd64.whl", hash = "sha256:99f60d34c048c5c2fabc766108c103612344c46e35d4ed9ae0673d33c8fb26e8"},
{file = "multidict-6.0.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f285e862d2f153a70586579c15c44656f888806ed0e5b56b64489afe4a2dbfba"},
{file = "multidict-6.0.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:53689bb4e102200a4fafa9de9c7c3c212ab40a7ab2c8e474491914d2305f187e"},
{file = "multidict-6.0.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:612d1156111ae11d14afaf3a0669ebf6c170dbb735e510a7438ffe2369a847fd"},
{file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7be7047bd08accdb7487737631d25735c9a04327911de89ff1b26b81745bd4e3"},
{file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de170c7b4fe6859beb8926e84f7d7d6c693dfe8e27372ce3b76f01c46e489fcf"},
{file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04bde7a7b3de05732a4eb39c94574db1ec99abb56162d6c520ad26f83267de29"},
{file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85f67aed7bb647f93e7520633d8f51d3cbc6ab96957c71272b286b2f30dc70ed"},
{file = "multidict-6.0.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:425bf820055005bfc8aa9a0b99ccb52cc2f4070153e34b701acc98d201693733"},
{file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d3eb1ceec286eba8220c26f3b0096cf189aea7057b6e7b7a2e60ed36b373b77f"},
{file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:7901c05ead4b3fb75113fb1dd33eb1253c6d3ee37ce93305acd9d38e0b5f21a4"},
{file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:e0e79d91e71b9867c73323a3444724d496c037e578a0e1755ae159ba14f4f3d1"},
{file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:29bfeb0dff5cb5fdab2023a7a9947b3b4af63e9c47cae2a10ad58394b517fddc"},
{file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e030047e85cbcedbfc073f71836d62dd5dadfbe7531cae27789ff66bc551bd5e"},
{file = "multidict-6.0.5-cp311-cp311-win32.whl", hash = "sha256:2f4848aa3baa109e6ab81fe2006c77ed4d3cd1e0ac2c1fbddb7b1277c168788c"},
{file = "multidict-6.0.5-cp311-cp311-win_amd64.whl", hash = "sha256:2faa5ae9376faba05f630d7e5e6be05be22913782b927b19d12b8145968a85ea"},
{file = "multidict-6.0.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:51d035609b86722963404f711db441cf7134f1889107fb171a970c9701f92e1e"},
{file = "multidict-6.0.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:cbebcd5bcaf1eaf302617c114aa67569dd3f090dd0ce8ba9e35e9985b41ac35b"},
{file = "multidict-6.0.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2ffc42c922dbfddb4a4c3b438eb056828719f07608af27d163191cb3e3aa6cc5"},
{file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ceb3b7e6a0135e092de86110c5a74e46bda4bd4fbfeeb3a3bcec79c0f861e450"},
{file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:79660376075cfd4b2c80f295528aa6beb2058fd289f4c9252f986751a4cd0496"},
{file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e4428b29611e989719874670fd152b6625500ad6c686d464e99f5aaeeaca175a"},
{file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d84a5c3a5f7ce6db1f999fb9438f686bc2e09d38143f2d93d8406ed2dd6b9226"},
{file = "multidict-6.0.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:76c0de87358b192de7ea9649beb392f107dcad9ad27276324c24c91774ca5271"},
{file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:79a6d2ba910adb2cbafc95dad936f8b9386e77c84c35bc0add315b856d7c3abb"},
{file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:92d16a3e275e38293623ebf639c471d3e03bb20b8ebb845237e0d3664914caef"},
{file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:fb616be3538599e797a2017cccca78e354c767165e8858ab5116813146041a24"},
{file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:14c2976aa9038c2629efa2c148022ed5eb4cb939e15ec7aace7ca932f48f9ba6"},
{file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:435a0984199d81ca178b9ae2c26ec3d49692d20ee29bc4c11a2a8d4514c67eda"},
{file = "multidict-6.0.5-cp312-cp312-win32.whl", hash = "sha256:9fe7b0653ba3d9d65cbe7698cca585bf0f8c83dbbcc710db9c90f478e175f2d5"},
{file = "multidict-6.0.5-cp312-cp312-win_amd64.whl", hash = "sha256:01265f5e40f5a17f8241d52656ed27192be03bfa8764d88e8220141d1e4b3556"},
{file = "multidict-6.0.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:19fe01cea168585ba0f678cad6f58133db2aa14eccaf22f88e4a6dccadfad8b3"},
{file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6bf7a982604375a8d49b6cc1b781c1747f243d91b81035a9b43a2126c04766f5"},
{file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:107c0cdefe028703fb5dafe640a409cb146d44a6ae201e55b35a4af8e95457dd"},
{file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:403c0911cd5d5791605808b942c88a8155c2592e05332d2bf78f18697a5fa15e"},
{file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aeaf541ddbad8311a87dd695ed9642401131ea39ad7bc8cf3ef3967fd093b626"},
{file = "multidict-6.0.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e4972624066095e52b569e02b5ca97dbd7a7ddd4294bf4e7247d52635630dd83"},
{file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d946b0a9eb8aaa590df1fe082cee553ceab173e6cb5b03239716338629c50c7a"},
{file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b55358304d7a73d7bdf5de62494aaf70bd33015831ffd98bc498b433dfe5b10c"},
{file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:a3145cb08d8625b2d3fee1b2d596a8766352979c9bffe5d7833e0503d0f0b5e5"},
{file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:d65f25da8e248202bd47445cec78e0025c0fe7582b23ec69c3b27a640dd7a8e3"},
{file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c9bf56195c6bbd293340ea82eafd0071cb3d450c703d2c93afb89f93b8386ccc"},
{file = "multidict-6.0.5-cp37-cp37m-win32.whl", hash = "sha256:69db76c09796b313331bb7048229e3bee7928eb62bab5e071e9f7fcc4879caee"},
{file = "multidict-6.0.5-cp37-cp37m-win_amd64.whl", hash = "sha256:fce28b3c8a81b6b36dfac9feb1de115bab619b3c13905b419ec71d03a3fc1423"},
{file = "multidict-6.0.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:76f067f5121dcecf0d63a67f29080b26c43c71a98b10c701b0677e4a065fbd54"},
{file = "multidict-6.0.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b82cc8ace10ab5bd93235dfaab2021c70637005e1ac787031f4d1da63d493c1d"},
{file = "multidict-6.0.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5cb241881eefd96b46f89b1a056187ea8e9ba14ab88ba632e68d7a2ecb7aadf7"},
{file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8e94e6912639a02ce173341ff62cc1201232ab86b8a8fcc05572741a5dc7d93"},
{file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09a892e4a9fb47331da06948690ae38eaa2426de97b4ccbfafbdcbe5c8f37ff8"},
{file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55205d03e8a598cfc688c71ca8ea5f66447164efff8869517f175ea632c7cb7b"},
{file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37b15024f864916b4951adb95d3a80c9431299080341ab9544ed148091b53f50"},
{file = "multidict-6.0.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2a1dee728b52b33eebff5072817176c172050d44d67befd681609b4746e1c2e"},
{file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:edd08e6f2f1a390bf137080507e44ccc086353c8e98c657e666c017718561b89"},
{file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:60d698e8179a42ec85172d12f50b1668254628425a6bd611aba022257cac1386"},
{file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:3d25f19500588cbc47dc19081d78131c32637c25804df8414463ec908631e453"},
{file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:4cc0ef8b962ac7a5e62b9e826bd0cd5040e7d401bc45a6835910ed699037a461"},
{file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:eca2e9d0cc5a889850e9bbd68e98314ada174ff6ccd1129500103df7a94a7a44"},
{file = "multidict-6.0.5-cp38-cp38-win32.whl", hash = "sha256:4a6a4f196f08c58c59e0b8ef8ec441d12aee4125a7d4f4fef000ccb22f8d7241"},
{file = "multidict-6.0.5-cp38-cp38-win_amd64.whl", hash = "sha256:0275e35209c27a3f7951e1ce7aaf93ce0d163b28948444bec61dd7badc6d3f8c"},
{file = "multidict-6.0.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e7be68734bd8c9a513f2b0cfd508802d6609da068f40dc57d4e3494cefc92929"},
{file = "multidict-6.0.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1d9ea7a7e779d7a3561aade7d596649fbecfa5c08a7674b11b423783217933f9"},
{file = "multidict-6.0.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ea1456df2a27c73ce51120fa2f519f1bea2f4a03a917f4a43c8707cf4cbbae1a"},
{file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf590b134eb70629e350691ecca88eac3e3b8b3c86992042fb82e3cb1830d5e1"},
{file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5c0631926c4f58e9a5ccce555ad7747d9a9f8b10619621f22f9635f069f6233e"},
{file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dce1c6912ab9ff5f179eaf6efe7365c1f425ed690b03341911bf4939ef2f3046"},
{file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0868d64af83169e4d4152ec612637a543f7a336e4a307b119e98042e852ad9c"},
{file = "multidict-6.0.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:141b43360bfd3bdd75f15ed811850763555a251e38b2405967f8e25fb43f7d40"},
{file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7df704ca8cf4a073334e0427ae2345323613e4df18cc224f647f251e5e75a527"},
{file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6214c5a5571802c33f80e6c84713b2c79e024995b9c5897f794b43e714daeec9"},
{file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:cd6c8fca38178e12c00418de737aef1261576bd1b6e8c6134d3e729a4e858b38"},
{file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:e02021f87a5b6932fa6ce916ca004c4d441509d33bbdbeca70d05dff5e9d2479"},
{file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ebd8d160f91a764652d3e51ce0d2956b38efe37c9231cd82cfc0bed2e40b581c"},
{file = "multidict-6.0.5-cp39-cp39-win32.whl", hash = "sha256:04da1bb8c8dbadf2a18a452639771951c662c5ad03aefe4884775454be322c9b"},
{file = "multidict-6.0.5-cp39-cp39-win_amd64.whl", hash = "sha256:d6f6d4f185481c9669b9447bf9d9cf3b95a0e9df9d169bbc17e363b7d5487755"},
{file = "multidict-6.0.5-py3-none-any.whl", hash = "sha256:0d63c74e3d7ab26de115c49bffc92cc77ed23395303d496eae515d4204a625e7"},
{file = "multidict-6.0.5.tar.gz", hash = "sha256:f7e301075edaf50500f0b341543c41194d8df3ae5caf4702f2095f3ca73dd8da"},
]
[[package]] [[package]]
name = "nodeenv" name = "nodeenv"
version = "1.8.0" version = "1.8.0"
@ -917,13 +592,13 @@ files = [
[[package]] [[package]]
name = "openai" name = "openai"
version = "1.14.0" version = "1.13.3"
description = "The official Python library for the openai API" description = "The official Python library for the openai API"
optional = false optional = false
python-versions = ">=3.7.1" python-versions = ">=3.7.1"
files = [ files = [
{file = "openai-1.14.0-py3-none-any.whl", hash = "sha256:5c9fd3a59f5cbdb4020733ddf79a22f6b7a36d561968cb3f3dd255cdd263d9fe"}, {file = "openai-1.13.3-py3-none-any.whl", hash = "sha256:5769b62abd02f350a8dd1a3a242d8972c947860654466171d60fb0972ae0a41c"},
{file = "openai-1.14.0.tar.gz", hash = "sha256:e287057adf0ec3315abc32ddcc968d095879abd9b68bf51c0402dab13ab5ae9b"}, {file = "openai-1.13.3.tar.gz", hash = "sha256:ff6c6b3bc7327e715e4b3592a923a5a1c7519ff5dd764a83d69f633d49e77a7b"},
] ]
[package.dependencies] [package.dependencies]
@ -940,91 +615,90 @@ datalib = ["numpy (>=1)", "pandas (>=1.2.3)", "pandas-stubs (>=1.1.0.11)"]
[[package]] [[package]]
name = "packaging" name = "packaging"
version = "24.0" version = "23.2"
description = "Core utilities for Python packages" description = "Core utilities for Python packages"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
{file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"},
{file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"},
] ]
[[package]] [[package]]
name = "pillow" name = "pillow"
version = "10.3.0" version = "10.2.0"
description = "Python Imaging Library (Fork)" description = "Python Imaging Library (Fork)"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "pillow-10.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:90b9e29824800e90c84e4022dd5cc16eb2d9605ee13f05d47641eb183cd73d45"}, {file = "pillow-10.2.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:7823bdd049099efa16e4246bdf15e5a13dbb18a51b68fa06d6c1d4d8b99a796e"},
{file = "pillow-10.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a2c405445c79c3f5a124573a051062300936b0281fee57637e706453e452746c"}, {file = "pillow-10.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:83b2021f2ade7d1ed556bc50a399127d7fb245e725aa0113ebd05cfe88aaf588"},
{file = "pillow-10.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78618cdbccaa74d3f88d0ad6cb8ac3007f1a6fa5c6f19af64b55ca170bfa1edf"}, {file = "pillow-10.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fad5ff2f13d69b7e74ce5b4ecd12cc0ec530fcee76356cac6742785ff71c452"},
{file = "pillow-10.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:261ddb7ca91fcf71757979534fb4c128448b5b4c55cb6152d280312062f69599"}, {file = "pillow-10.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da2b52b37dad6d9ec64e653637a096905b258d2fc2b984c41ae7d08b938a67e4"},
{file = "pillow-10.3.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:ce49c67f4ea0609933d01c0731b34b8695a7a748d6c8d186f95e7d085d2fe475"}, {file = "pillow-10.2.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:47c0995fc4e7f79b5cfcab1fc437ff2890b770440f7696a3ba065ee0fd496563"},
{file = "pillow-10.3.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:b14f16f94cbc61215115b9b1236f9c18403c15dd3c52cf629072afa9d54c1cbf"}, {file = "pillow-10.2.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:322bdf3c9b556e9ffb18f93462e5f749d3444ce081290352c6070d014c93feb2"},
{file = "pillow-10.3.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d33891be6df59d93df4d846640f0e46f1a807339f09e79a8040bc887bdcd7ed3"}, {file = "pillow-10.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:51f1a1bffc50e2e9492e87d8e09a17c5eea8409cda8d3f277eb6edc82813c17c"},
{file = "pillow-10.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b50811d664d392f02f7761621303eba9d1b056fb1868c8cdf4231279645c25f5"}, {file = "pillow-10.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:69ffdd6120a4737710a9eee73e1d2e37db89b620f702754b8f6e62594471dee0"},
{file = "pillow-10.3.0-cp310-cp310-win32.whl", hash = "sha256:ca2870d5d10d8726a27396d3ca4cf7976cec0f3cb706debe88e3a5bd4610f7d2"}, {file = "pillow-10.2.0-cp310-cp310-win32.whl", hash = "sha256:c6dafac9e0f2b3c78df97e79af707cdc5ef8e88208d686a4847bab8266870023"},
{file = "pillow-10.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:f0d0591a0aeaefdaf9a5e545e7485f89910c977087e7de2b6c388aec32011e9f"}, {file = "pillow-10.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:aebb6044806f2e16ecc07b2a2637ee1ef67a11840a66752751714a0d924adf72"},
{file = "pillow-10.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:ccce24b7ad89adb5a1e34a6ba96ac2530046763912806ad4c247356a8f33a67b"}, {file = "pillow-10.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:7049e301399273a0136ff39b84c3678e314f2158f50f517bc50285fb5ec847ad"},
{file = "pillow-10.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:5f77cf66e96ae734717d341c145c5949c63180842a545c47a0ce7ae52ca83795"}, {file = "pillow-10.2.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:35bb52c37f256f662abdfa49d2dfa6ce5d93281d323a9af377a120e89a9eafb5"},
{file = "pillow-10.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e4b878386c4bf293578b48fc570b84ecfe477d3b77ba39a6e87150af77f40c57"}, {file = "pillow-10.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9c23f307202661071d94b5e384e1e1dc7dfb972a28a2310e4ee16103e66ddb67"},
{file = "pillow-10.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdcbb4068117dfd9ce0138d068ac512843c52295ed996ae6dd1faf537b6dbc27"}, {file = "pillow-10.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:773efe0603db30c281521a7c0214cad7836c03b8ccff897beae9b47c0b657d61"},
{file = "pillow-10.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9797a6c8fe16f25749b371c02e2ade0efb51155e767a971c61734b1bf6293994"}, {file = "pillow-10.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11fa2e5984b949b0dd6d7a94d967743d87c577ff0b83392f17cb3990d0d2fd6e"},
{file = "pillow-10.3.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:9e91179a242bbc99be65e139e30690e081fe6cb91a8e77faf4c409653de39451"}, {file = "pillow-10.2.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:716d30ed977be8b37d3ef185fecb9e5a1d62d110dfbdcd1e2a122ab46fddb03f"},
{file = "pillow-10.3.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:1b87bd9d81d179bd8ab871603bd80d8645729939f90b71e62914e816a76fc6bd"}, {file = "pillow-10.2.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:a086c2af425c5f62a65e12fbf385f7c9fcb8f107d0849dba5839461a129cf311"},
{file = "pillow-10.3.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:81d09caa7b27ef4e61cb7d8fbf1714f5aec1c6b6c5270ee53504981e6e9121ad"}, {file = "pillow-10.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c8de2789052ed501dd829e9cae8d3dcce7acb4777ea4a479c14521c942d395b1"},
{file = "pillow-10.3.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:048ad577748b9fa4a99a0548c64f2cb8d672d5bf2e643a739ac8faff1164238c"}, {file = "pillow-10.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:609448742444d9290fd687940ac0b57fb35e6fd92bdb65386e08e99af60bf757"},
{file = "pillow-10.3.0-cp311-cp311-win32.whl", hash = "sha256:7161ec49ef0800947dc5570f86568a7bb36fa97dd09e9827dc02b718c5643f09"}, {file = "pillow-10.2.0-cp311-cp311-win32.whl", hash = "sha256:823ef7a27cf86df6597fa0671066c1b596f69eba53efa3d1e1cb8b30f3533068"},
{file = "pillow-10.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:8eb0908e954d093b02a543dc963984d6e99ad2b5e36503d8a0aaf040505f747d"}, {file = "pillow-10.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:1da3b2703afd040cf65ec97efea81cfba59cdbed9c11d8efc5ab09df9509fc56"},
{file = "pillow-10.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:4e6f7d1c414191c1199f8996d3f2282b9ebea0945693fb67392c75a3a320941f"}, {file = "pillow-10.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:edca80cbfb2b68d7b56930b84a0e45ae1694aeba0541f798e908a49d66b837f1"},
{file = "pillow-10.3.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:e46f38133e5a060d46bd630faa4d9fa0202377495df1f068a8299fd78c84de84"}, {file = "pillow-10.2.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:1b5e1b74d1bd1b78bc3477528919414874748dd363e6272efd5abf7654e68bef"},
{file = "pillow-10.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:50b8eae8f7334ec826d6eeffaeeb00e36b5e24aa0b9df322c247539714c6df19"}, {file = "pillow-10.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0eae2073305f451d8ecacb5474997c08569fb4eb4ac231ffa4ad7d342fdc25ac"},
{file = "pillow-10.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d3bea1c75f8c53ee4d505c3e67d8c158ad4df0d83170605b50b64025917f338"}, {file = "pillow-10.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7c2286c23cd350b80d2fc9d424fc797575fb16f854b831d16fd47ceec078f2c"},
{file = "pillow-10.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19aeb96d43902f0a783946a0a87dbdad5c84c936025b8419da0a0cd7724356b1"}, {file = "pillow-10.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e23412b5c41e58cec602f1135c57dfcf15482013ce6e5f093a86db69646a5aa"},
{file = "pillow-10.3.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:74d28c17412d9caa1066f7a31df8403ec23d5268ba46cd0ad2c50fb82ae40462"}, {file = "pillow-10.2.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:52a50aa3fb3acb9cf7213573ef55d31d6eca37f5709c69e6858fe3bc04a5c2a2"},
{file = "pillow-10.3.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:ff61bfd9253c3915e6d41c651d5f962da23eda633cf02262990094a18a55371a"}, {file = "pillow-10.2.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:127cee571038f252a552760076407f9cff79761c3d436a12af6000cd182a9d04"},
{file = "pillow-10.3.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d886f5d353333b4771d21267c7ecc75b710f1a73d72d03ca06df49b09015a9ef"}, {file = "pillow-10.2.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:8d12251f02d69d8310b046e82572ed486685c38f02176bd08baf216746eb947f"},
{file = "pillow-10.3.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4b5ec25d8b17217d635f8935dbc1b9aa5907962fae29dff220f2659487891cd3"}, {file = "pillow-10.2.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:54f1852cd531aa981bc0965b7d609f5f6cc8ce8c41b1139f6ed6b3c54ab82bfb"},
{file = "pillow-10.3.0-cp312-cp312-win32.whl", hash = "sha256:51243f1ed5161b9945011a7360e997729776f6e5d7005ba0c6879267d4c5139d"}, {file = "pillow-10.2.0-cp312-cp312-win32.whl", hash = "sha256:257d8788df5ca62c980314053197f4d46eefedf4e6175bc9412f14412ec4ea2f"},
{file = "pillow-10.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:412444afb8c4c7a6cc11a47dade32982439925537e483be7c0ae0cf96c4f6a0b"}, {file = "pillow-10.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:154e939c5f0053a383de4fd3d3da48d9427a7e985f58af8e94d0b3c9fcfcf4f9"},
{file = "pillow-10.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:798232c92e7665fe82ac085f9d8e8ca98826f8e27859d9a96b41d519ecd2e49a"}, {file = "pillow-10.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:f379abd2f1e3dddb2b61bc67977a6b5a0a3f7485538bcc6f39ec76163891ee48"},
{file = "pillow-10.3.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:4eaa22f0d22b1a7e93ff0a596d57fdede2e550aecffb5a1ef1106aaece48e96b"}, {file = "pillow-10.2.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:8373c6c251f7ef8bda6675dd6d2b3a0fcc31edf1201266b5cf608b62a37407f9"},
{file = "pillow-10.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cd5e14fbf22a87321b24c88669aad3a51ec052eb145315b3da3b7e3cc105b9a2"}, {file = "pillow-10.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:870ea1ada0899fd0b79643990809323b389d4d1d46c192f97342eeb6ee0b8483"},
{file = "pillow-10.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1530e8f3a4b965eb6a7785cf17a426c779333eb62c9a7d1bbcf3ffd5bf77a4aa"}, {file = "pillow-10.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4b6b1e20608493548b1f32bce8cca185bf0480983890403d3b8753e44077129"},
{file = "pillow-10.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d512aafa1d32efa014fa041d38868fda85028e3f930a96f85d49c7d8ddc0383"}, {file = "pillow-10.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3031709084b6e7852d00479fd1d310b07d0ba82765f973b543c8af5061cf990e"},
{file = "pillow-10.3.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:339894035d0ede518b16073bdc2feef4c991ee991a29774b33e515f1d308e08d"}, {file = "pillow-10.2.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:3ff074fc97dd4e80543a3e91f69d58889baf2002b6be64347ea8cf5533188213"},
{file = "pillow-10.3.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:aa7e402ce11f0885305bfb6afb3434b3cd8f53b563ac065452d9d5654c7b86fd"}, {file = "pillow-10.2.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:cb4c38abeef13c61d6916f264d4845fab99d7b711be96c326b84df9e3e0ff62d"},
{file = "pillow-10.3.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:0ea2a783a2bdf2a561808fe4a7a12e9aa3799b701ba305de596bc48b8bdfce9d"}, {file = "pillow-10.2.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b1b3020d90c2d8e1dae29cf3ce54f8094f7938460fb5ce8bc5c01450b01fbaf6"},
{file = "pillow-10.3.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:c78e1b00a87ce43bb37642c0812315b411e856a905d58d597750eb79802aaaa3"}, {file = "pillow-10.2.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:170aeb00224ab3dc54230c797f8404507240dd868cf52066f66a41b33169bdbe"},
{file = "pillow-10.3.0-cp38-cp38-win32.whl", hash = "sha256:72d622d262e463dfb7595202d229f5f3ab4b852289a1cd09650362db23b9eb0b"}, {file = "pillow-10.2.0-cp38-cp38-win32.whl", hash = "sha256:c4225f5220f46b2fde568c74fca27ae9771536c2e29d7c04f4fb62c83275ac4e"},
{file = "pillow-10.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:2034f6759a722da3a3dbd91a81148cf884e91d1b747992ca288ab88c1de15999"}, {file = "pillow-10.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:0689b5a8c5288bc0504d9fcee48f61a6a586b9b98514d7d29b840143d6734f39"},
{file = "pillow-10.3.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:2ed854e716a89b1afcedea551cd85f2eb2a807613752ab997b9974aaa0d56936"}, {file = "pillow-10.2.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:b792a349405fbc0163190fde0dc7b3fef3c9268292586cf5645598b48e63dc67"},
{file = "pillow-10.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:dc1a390a82755a8c26c9964d457d4c9cbec5405896cba94cf51f36ea0d855002"}, {file = "pillow-10.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c570f24be1e468e3f0ce7ef56a89a60f0e05b30a3669a459e419c6eac2c35364"},
{file = "pillow-10.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4203efca580f0dd6f882ca211f923168548f7ba334c189e9eab1178ab840bf60"}, {file = "pillow-10.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8ecd059fdaf60c1963c58ceb8997b32e9dc1b911f5da5307aab614f1ce5c2fb"},
{file = "pillow-10.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3102045a10945173d38336f6e71a8dc71bcaeed55c3123ad4af82c52807b9375"}, {file = "pillow-10.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c365fd1703040de1ec284b176d6af5abe21b427cb3a5ff68e0759e1e313a5e7e"},
{file = "pillow-10.3.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:6fb1b30043271ec92dc65f6d9f0b7a830c210b8a96423074b15c7bc999975f57"}, {file = "pillow-10.2.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:70c61d4c475835a19b3a5aa42492409878bbca7438554a1f89d20d58a7c75c01"},
{file = "pillow-10.3.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:1dfc94946bc60ea375cc39cff0b8da6c7e5f8fcdc1d946beb8da5c216156ddd8"}, {file = "pillow-10.2.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:b6f491cdf80ae540738859d9766783e3b3c8e5bd37f5dfa0b76abdecc5081f13"},
{file = "pillow-10.3.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b09b86b27a064c9624d0a6c54da01c1beaf5b6cadfa609cf63789b1d08a797b9"}, {file = "pillow-10.2.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9d189550615b4948f45252d7f005e53c2040cea1af5b60d6f79491a6e147eef7"},
{file = "pillow-10.3.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d3b2348a78bc939b4fed6552abfd2e7988e0f81443ef3911a4b8498ca084f6eb"}, {file = "pillow-10.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:49d9ba1ed0ef3e061088cd1e7538a0759aab559e2e0a80a36f9fd9d8c0c21591"},
{file = "pillow-10.3.0-cp39-cp39-win32.whl", hash = "sha256:45ebc7b45406febf07fef35d856f0293a92e7417ae7933207e90bf9090b70572"}, {file = "pillow-10.2.0-cp39-cp39-win32.whl", hash = "sha256:babf5acfede515f176833ed6028754cbcd0d206f7f614ea3447d67c33be12516"},
{file = "pillow-10.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:0ba26351b137ca4e0db0342d5d00d2e355eb29372c05afd544ebf47c0956ffeb"}, {file = "pillow-10.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:0304004f8067386b477d20a518b50f3fa658a28d44e4116970abfcd94fac34a8"},
{file = "pillow-10.3.0-cp39-cp39-win_arm64.whl", hash = "sha256:50fd3f6b26e3441ae07b7c979309638b72abc1a25da31a81a7fbd9495713ef4f"}, {file = "pillow-10.2.0-cp39-cp39-win_arm64.whl", hash = "sha256:0fb3e7fc88a14eacd303e90481ad983fd5b69c761e9e6ef94c983f91025da869"},
{file = "pillow-10.3.0-pp310-pypy310_pp73-macosx_10_10_x86_64.whl", hash = "sha256:6b02471b72526ab8a18c39cb7967b72d194ec53c1fd0a70b050565a0f366d355"}, {file = "pillow-10.2.0-pp310-pypy310_pp73-macosx_10_10_x86_64.whl", hash = "sha256:322209c642aabdd6207517e9739c704dc9f9db943015535783239022002f054a"},
{file = "pillow-10.3.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:8ab74c06ffdab957d7670c2a5a6e1a70181cd10b727cd788c4dd9005b6a8acd9"}, {file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3eedd52442c0a5ff4f887fab0c1c0bb164d8635b32c894bc1faf4c618dd89df2"},
{file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:048eeade4c33fdf7e08da40ef402e748df113fd0b4584e32c4af74fe78baaeb2"}, {file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb28c753fd5eb3dd859b4ee95de66cc62af91bcff5db5f2571d32a520baf1f04"},
{file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e2ec1e921fd07c7cda7962bad283acc2f2a9ccc1b971ee4b216b75fad6f0463"}, {file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:33870dc4653c5017bf4c8873e5488d8f8d5f8935e2f1fb9a2208c47cdd66efd2"},
{file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:4c8e73e99da7db1b4cad7f8d682cf6abad7844da39834c288fbfa394a47bbced"}, {file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:3c31822339516fb3c82d03f30e22b1d038da87ef27b6a78c9549888f8ceda39a"},
{file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:16563993329b79513f59142a6b02055e10514c1a8e86dca8b48a893e33cf91e3"}, {file = "pillow-10.2.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a2b56ba36e05f973d450582fb015594aaa78834fefe8dfb8fcd79b93e64ba4c6"},
{file = "pillow-10.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:dd78700f5788ae180b5ee8902c6aea5a5726bac7c364b202b4b3e3ba2d293170"}, {file = "pillow-10.2.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:d8e6aeb9201e655354b3ad049cb77d19813ad4ece0df1249d3c793de3774f8c7"},
{file = "pillow-10.3.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:aff76a55a8aa8364d25400a210a65ff59d0168e0b4285ba6bf2bd83cf675ba32"}, {file = "pillow-10.2.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:2247178effb34a77c11c0e8ac355c7a741ceca0a732b27bf11e747bbc950722f"},
{file = "pillow-10.3.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:b7bc2176354defba3edc2b9a777744462da2f8e921fbaf61e52acb95bafa9828"}, {file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15587643b9e5eb26c48e49a7b33659790d28f190fc514a322d55da2fb5c2950e"},
{file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:793b4e24db2e8742ca6423d3fde8396db336698c55cd34b660663ee9e45ed37f"}, {file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753cd8f2086b2b80180d9b3010dd4ed147efc167c90d3bf593fe2af21265e5a5"},
{file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d93480005693d247f8346bc8ee28c72a2191bdf1f6b5db469c096c0c867ac015"}, {file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:7c8f97e8e7a9009bcacbe3766a36175056c12f9a44e6e6f2d5caad06dcfbf03b"},
{file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c83341b89884e2b2e55886e8fbbf37c3fa5efd6c8907124aeb72f285ae5696e5"}, {file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d1b35bcd6c5543b9cb547dee3150c93008f8dd0f1fef78fc0cd2b141c5baf58a"},
{file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1a1d1915db1a4fdb2754b9de292642a39a7fb28f1736699527bb649484fb966a"}, {file = "pillow-10.2.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:fe4c15f6c9285dc54ce6553a3ce908ed37c8f3825b5a51a15c91442bb955b868"},
{file = "pillow-10.3.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a0eaa93d054751ee9964afa21c06247779b90440ca41d184aeb5d410f20ff591"}, {file = "pillow-10.2.0.tar.gz", hash = "sha256:e87f0b2c78157e12d7686b27d63c070fd65d994e8ddae6f328e0dcf4a0cd007e"},
{file = "pillow-10.3.0.tar.gz", hash = "sha256:9d2455fbf44c914840c793e89aa82d0e1763a14253a000743719ae5946814b2d"},
] ]
[package.extras] [package.extras]
@ -1151,13 +825,13 @@ email = ["email-validator (>=1.0.3)"]
[[package]] [[package]]
name = "pyright" name = "pyright"
version = "1.1.354" version = "1.1.352"
description = "Command line wrapper for pyright" description = "Command line wrapper for pyright"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
{file = "pyright-1.1.354-py3-none-any.whl", hash = "sha256:f28d61ae8ae035fc52ded1070e8d9e786051a26a4127bbd7a4ba0399b81b37b5"}, {file = "pyright-1.1.352-py3-none-any.whl", hash = "sha256:0040cf173c6a60704e553bfd129dfe54de59cc76d0b2b80f77cfab4f50701d64"},
{file = "pyright-1.1.354.tar.gz", hash = "sha256:b1070dc774ff2e79eb0523fe87f4ba9a90550de7e4b030a2bc9e031864029a1f"}, {file = "pyright-1.1.352.tar.gz", hash = "sha256:a621c0dfbcf1291b3610641a07380fefaa1d0e182890a1b2a7f13b446e8109a9"},
] ]
[package.dependencies] [package.dependencies]
@ -1228,7 +902,6 @@ files = [
{file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"},
{file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"},
{file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"},
{file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"},
{file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"},
{file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"},
{file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"},
@ -1286,18 +959,18 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
[[package]] [[package]]
name = "setuptools" name = "setuptools"
version = "69.2.0" version = "69.1.1"
description = "Easily download, build, install, upgrade, and uninstall Python packages" description = "Easily download, build, install, upgrade, and uninstall Python packages"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "setuptools-69.2.0-py3-none-any.whl", hash = "sha256:c21c49fb1042386df081cb5d86759792ab89efca84cf114889191cd09aacc80c"}, {file = "setuptools-69.1.1-py3-none-any.whl", hash = "sha256:02fa291a0471b3a18b2b2481ed902af520c69e8ae0919c13da936542754b4c56"},
{file = "setuptools-69.2.0.tar.gz", hash = "sha256:0ff4183f8f42cd8fa3acea16c45205521a4ef28f73c6391d8a25e92893134f2e"}, {file = "setuptools-69.1.1.tar.gz", hash = "sha256:5c0806c7d9af348e6dd3777b4f4dbb42c7ad85b190104837488eab9a7c945cf8"},
] ]
[package.extras] [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"] 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)", "importlib-metadata", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mypy (==1.9)", "packaging (>=23.2)", "pip (>=19.1)", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] 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)", "packaging (>=23.2)", "pip (>=19.1)", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff (>=0.2.1)", "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.2)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "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.2)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"]
[[package]] [[package]]
@ -1497,17 +1170,6 @@ notebook = ["ipywidgets (>=6)"]
slack = ["slack-sdk"] slack = ["slack-sdk"]
telegram = ["requests"] telegram = ["requests"]
[[package]]
name = "types-pillow"
version = "10.2.0.20240520"
description = "Typing stubs for Pillow"
optional = false
python-versions = ">=3.8"
files = [
{file = "types-Pillow-10.2.0.20240520.tar.gz", hash = "sha256:130b979195465fa1e1676d8e81c9c7c30319e8e95b12fae945e8f0d525213107"},
{file = "types_Pillow-10.2.0.20240520-py3-none-any.whl", hash = "sha256:33c36494b380e2a269bb742181bea5d9b00820367822dbd3760f07210a1da23d"},
]
[[package]] [[package]]
name = "typing-extensions" name = "typing-extensions"
version = "4.10.0" version = "4.10.0"
@ -1656,110 +1318,7 @@ files = [
{file = "websockets-12.0.tar.gz", hash = "sha256:81df9cbcbb6c260de1e007e58c011bfebe2dafc8435107b0537f393dd38c8b1b"}, {file = "websockets-12.0.tar.gz", hash = "sha256:81df9cbcbb6c260de1e007e58c011bfebe2dafc8435107b0537f393dd38c8b1b"},
] ]
[[package]]
name = "yarl"
version = "1.9.4"
description = "Yet another URL library"
optional = false
python-versions = ">=3.7"
files = [
{file = "yarl-1.9.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a8c1df72eb746f4136fe9a2e72b0c9dc1da1cbd23b5372f94b5820ff8ae30e0e"},
{file = "yarl-1.9.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a3a6ed1d525bfb91b3fc9b690c5a21bb52de28c018530ad85093cc488bee2dd2"},
{file = "yarl-1.9.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c38c9ddb6103ceae4e4498f9c08fac9b590c5c71b0370f98714768e22ac6fa66"},
{file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d9e09c9d74f4566e905a0b8fa668c58109f7624db96a2171f21747abc7524234"},
{file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8477c1ee4bd47c57d49621a062121c3023609f7a13b8a46953eb6c9716ca392"},
{file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5ff2c858f5f6a42c2a8e751100f237c5e869cbde669a724f2062d4c4ef93551"},
{file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:357495293086c5b6d34ca9616a43d329317feab7917518bc97a08f9e55648455"},
{file = "yarl-1.9.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:54525ae423d7b7a8ee81ba189f131054defdb122cde31ff17477951464c1691c"},
{file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:801e9264d19643548651b9db361ce3287176671fb0117f96b5ac0ee1c3530d53"},
{file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e516dc8baf7b380e6c1c26792610230f37147bb754d6426462ab115a02944385"},
{file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:7d5aaac37d19b2904bb9dfe12cdb08c8443e7ba7d2852894ad448d4b8f442863"},
{file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:54beabb809ffcacbd9d28ac57b0db46e42a6e341a030293fb3185c409e626b8b"},
{file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bac8d525a8dbc2a1507ec731d2867025d11ceadcb4dd421423a5d42c56818541"},
{file = "yarl-1.9.4-cp310-cp310-win32.whl", hash = "sha256:7855426dfbddac81896b6e533ebefc0af2f132d4a47340cee6d22cac7190022d"},
{file = "yarl-1.9.4-cp310-cp310-win_amd64.whl", hash = "sha256:848cd2a1df56ddbffeb375535fb62c9d1645dde33ca4d51341378b3f5954429b"},
{file = "yarl-1.9.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:35a2b9396879ce32754bd457d31a51ff0a9d426fd9e0e3c33394bf4b9036b099"},
{file = "yarl-1.9.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c7d56b293cc071e82532f70adcbd8b61909eec973ae9d2d1f9b233f3d943f2c"},
{file = "yarl-1.9.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d8a1c6c0be645c745a081c192e747c5de06e944a0d21245f4cf7c05e457c36e0"},
{file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b3c1ffe10069f655ea2d731808e76e0f452fc6c749bea04781daf18e6039525"},
{file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:549d19c84c55d11687ddbd47eeb348a89df9cb30e1993f1b128f4685cd0ebbf8"},
{file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a7409f968456111140c1c95301cadf071bd30a81cbd7ab829169fb9e3d72eae9"},
{file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e23a6d84d9d1738dbc6e38167776107e63307dfc8ad108e580548d1f2c587f42"},
{file = "yarl-1.9.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d8b889777de69897406c9fb0b76cdf2fd0f31267861ae7501d93003d55f54fbe"},
{file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:03caa9507d3d3c83bca08650678e25364e1843b484f19986a527630ca376ecce"},
{file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4e9035df8d0880b2f1c7f5031f33f69e071dfe72ee9310cfc76f7b605958ceb9"},
{file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:c0ec0ed476f77db9fb29bca17f0a8fcc7bc97ad4c6c1d8959c507decb22e8572"},
{file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:ee04010f26d5102399bd17f8df8bc38dc7ccd7701dc77f4a68c5b8d733406958"},
{file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:49a180c2e0743d5d6e0b4d1a9e5f633c62eca3f8a86ba5dd3c471060e352ca98"},
{file = "yarl-1.9.4-cp311-cp311-win32.whl", hash = "sha256:81eb57278deb6098a5b62e88ad8281b2ba09f2f1147c4767522353eaa6260b31"},
{file = "yarl-1.9.4-cp311-cp311-win_amd64.whl", hash = "sha256:d1d2532b340b692880261c15aee4dc94dd22ca5d61b9db9a8a361953d36410b1"},
{file = "yarl-1.9.4-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0d2454f0aef65ea81037759be5ca9947539667eecebca092733b2eb43c965a81"},
{file = "yarl-1.9.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:44d8ffbb9c06e5a7f529f38f53eda23e50d1ed33c6c869e01481d3fafa6b8142"},
{file = "yarl-1.9.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:aaaea1e536f98754a6e5c56091baa1b6ce2f2700cc4a00b0d49eca8dea471074"},
{file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3777ce5536d17989c91696db1d459574e9a9bd37660ea7ee4d3344579bb6f129"},
{file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fc5fc1eeb029757349ad26bbc5880557389a03fa6ada41703db5e068881e5f2"},
{file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea65804b5dc88dacd4a40279af0cdadcfe74b3e5b4c897aa0d81cf86927fee78"},
{file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa102d6d280a5455ad6a0f9e6d769989638718e938a6a0a2ff3f4a7ff8c62cc4"},
{file = "yarl-1.9.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09efe4615ada057ba2d30df871d2f668af661e971dfeedf0c159927d48bbeff0"},
{file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:008d3e808d03ef28542372d01057fd09168419cdc8f848efe2804f894ae03e51"},
{file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:6f5cb257bc2ec58f437da2b37a8cd48f666db96d47b8a3115c29f316313654ff"},
{file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:992f18e0ea248ee03b5a6e8b3b4738850ae7dbb172cc41c966462801cbf62cf7"},
{file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:0e9d124c191d5b881060a9e5060627694c3bdd1fe24c5eecc8d5d7d0eb6faabc"},
{file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3986b6f41ad22988e53d5778f91855dc0399b043fc8946d4f2e68af22ee9ff10"},
{file = "yarl-1.9.4-cp312-cp312-win32.whl", hash = "sha256:4b21516d181cd77ebd06ce160ef8cc2a5e9ad35fb1c5930882baff5ac865eee7"},
{file = "yarl-1.9.4-cp312-cp312-win_amd64.whl", hash = "sha256:a9bd00dc3bc395a662900f33f74feb3e757429e545d831eef5bb280252631984"},
{file = "yarl-1.9.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:63b20738b5aac74e239622d2fe30df4fca4942a86e31bf47a81a0e94c14df94f"},
{file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7d7f7de27b8944f1fee2c26a88b4dabc2409d2fea7a9ed3df79b67277644e17"},
{file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c74018551e31269d56fab81a728f683667e7c28c04e807ba08f8c9e3bba32f14"},
{file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ca06675212f94e7a610e85ca36948bb8fc023e458dd6c63ef71abfd482481aa5"},
{file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5aef935237d60a51a62b86249839b51345f47564208c6ee615ed2a40878dccdd"},
{file = "yarl-1.9.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b134fd795e2322b7684155b7855cc99409d10b2e408056db2b93b51a52accc7"},
{file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d25039a474c4c72a5ad4b52495056f843a7ff07b632c1b92ea9043a3d9950f6e"},
{file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f7d6b36dd2e029b6bcb8a13cf19664c7b8e19ab3a58e0fefbb5b8461447ed5ec"},
{file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:957b4774373cf6f709359e5c8c4a0af9f6d7875db657adb0feaf8d6cb3c3964c"},
{file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:d7eeb6d22331e2fd42fce928a81c697c9ee2d51400bd1a28803965883e13cead"},
{file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:6a962e04b8f91f8c4e5917e518d17958e3bdee71fd1d8b88cdce74dd0ebbf434"},
{file = "yarl-1.9.4-cp37-cp37m-win32.whl", hash = "sha256:f3bc6af6e2b8f92eced34ef6a96ffb248e863af20ef4fde9448cc8c9b858b749"},
{file = "yarl-1.9.4-cp37-cp37m-win_amd64.whl", hash = "sha256:ad4d7a90a92e528aadf4965d685c17dacff3df282db1121136c382dc0b6014d2"},
{file = "yarl-1.9.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ec61d826d80fc293ed46c9dd26995921e3a82146feacd952ef0757236fc137be"},
{file = "yarl-1.9.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8be9e837ea9113676e5754b43b940b50cce76d9ed7d2461df1af39a8ee674d9f"},
{file = "yarl-1.9.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:bef596fdaa8f26e3d66af846bbe77057237cb6e8efff8cd7cc8dff9a62278bbf"},
{file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d47552b6e52c3319fede1b60b3de120fe83bde9b7bddad11a69fb0af7db32f1"},
{file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84fc30f71689d7fc9168b92788abc977dc8cefa806909565fc2951d02f6b7d57"},
{file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4aa9741085f635934f3a2583e16fcf62ba835719a8b2b28fb2917bb0537c1dfa"},
{file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:206a55215e6d05dbc6c98ce598a59e6fbd0c493e2de4ea6cc2f4934d5a18d130"},
{file = "yarl-1.9.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07574b007ee20e5c375a8fe4a0789fad26db905f9813be0f9fef5a68080de559"},
{file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5a2e2433eb9344a163aced6a5f6c9222c0786e5a9e9cac2c89f0b28433f56e23"},
{file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6ad6d10ed9b67a382b45f29ea028f92d25bc0bc1daf6c5b801b90b5aa70fb9ec"},
{file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:6fe79f998a4052d79e1c30eeb7d6c1c1056ad33300f682465e1b4e9b5a188b78"},
{file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a825ec844298c791fd28ed14ed1bffc56a98d15b8c58a20e0e08c1f5f2bea1be"},
{file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8619d6915b3b0b34420cf9b2bb6d81ef59d984cb0fde7544e9ece32b4b3043c3"},
{file = "yarl-1.9.4-cp38-cp38-win32.whl", hash = "sha256:686a0c2f85f83463272ddffd4deb5e591c98aac1897d65e92319f729c320eece"},
{file = "yarl-1.9.4-cp38-cp38-win_amd64.whl", hash = "sha256:a00862fb23195b6b8322f7d781b0dc1d82cb3bcac346d1e38689370cc1cc398b"},
{file = "yarl-1.9.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:604f31d97fa493083ea21bd9b92c419012531c4e17ea6da0f65cacdcf5d0bd27"},
{file = "yarl-1.9.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8a854227cf581330ffa2c4824d96e52ee621dd571078a252c25e3a3b3d94a1b1"},
{file = "yarl-1.9.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ba6f52cbc7809cd8d74604cce9c14868306ae4aa0282016b641c661f981a6e91"},
{file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a6327976c7c2f4ee6816eff196e25385ccc02cb81427952414a64811037bbc8b"},
{file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8397a3817d7dcdd14bb266283cd1d6fc7264a48c186b986f32e86d86d35fbac5"},
{file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e0381b4ce23ff92f8170080c97678040fc5b08da85e9e292292aba67fdac6c34"},
{file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23d32a2594cb5d565d358a92e151315d1b2268bc10f4610d098f96b147370136"},
{file = "yarl-1.9.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ddb2a5c08a4eaaba605340fdee8fc08e406c56617566d9643ad8bf6852778fc7"},
{file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:26a1dc6285e03f3cc9e839a2da83bcbf31dcb0d004c72d0730e755b33466c30e"},
{file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:18580f672e44ce1238b82f7fb87d727c4a131f3a9d33a5e0e82b793362bf18b4"},
{file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:29e0f83f37610f173eb7e7b5562dd71467993495e568e708d99e9d1944f561ec"},
{file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:1f23e4fe1e8794f74b6027d7cf19dc25f8b63af1483d91d595d4a07eca1fb26c"},
{file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:db8e58b9d79200c76956cefd14d5c90af54416ff5353c5bfd7cbe58818e26ef0"},
{file = "yarl-1.9.4-cp39-cp39-win32.whl", hash = "sha256:c7224cab95645c7ab53791022ae77a4509472613e839dab722a72abe5a684575"},
{file = "yarl-1.9.4-cp39-cp39-win_amd64.whl", hash = "sha256:824d6c50492add5da9374875ce72db7a0733b29c2394890aef23d533106e2b15"},
{file = "yarl-1.9.4-py3-none-any.whl", hash = "sha256:928cecb0ef9d5a7946eb6ff58417ad2fe9375762382f1bf5c55e61645f2c43ad"},
{file = "yarl-1.9.4.tar.gz", hash = "sha256:566db86717cf8080b99b58b083b773a908ae40f06681e87e589a976faf8246bf"},
]
[package.dependencies]
idna = ">=2.0"
multidict = ">=4.0"
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = "^3.10" python-versions = "^3.10"
content-hash = "9cec287b530dcda39515e7c76eed76d88eb0c725f37148af5ef93083cfb46ad8" content-hash = "274fed55cf4a2f4e9954b3d196c103b72225409c6050759a939f0ca197ae3f79"

View File

@ -1,12 +1,10 @@
from typing import Union from typing import List, NoReturn, Union
from openai.types.chat import ChatCompletionMessageParam, ChatCompletionContentPartParam from openai.types.chat import ChatCompletionMessageParam, ChatCompletionContentPartParam
from custom_types import InputMode
from image_generation.core import create_alt_url_mapping
from prompts.imported_code_prompts import IMPORTED_CODE_SYSTEM_PROMPTS from prompts.imported_code_prompts import IMPORTED_CODE_SYSTEM_PROMPTS
from prompts.screenshot_system_prompts import SYSTEM_PROMPTS from prompts.screenshot_system_prompts import SYSTEM_PROMPTS
from prompts.types import Stack from prompts.types import Stack
from video.utils import assemble_claude_prompt_video
USER_PROMPT = """ USER_PROMPT = """
@ -18,65 +16,9 @@ Generate code for a SVG that looks exactly like this.
""" """
async def create_prompt(
params: dict[str, str], stack: Stack, input_mode: InputMode
) -> tuple[list[ChatCompletionMessageParam], dict[str, str]]:
image_cache: dict[str, str] = {}
# If this generation started off with imported code, we need to assemble the prompt differently
if params.get("isImportedFromCode"):
original_imported_code = params["history"][0]
prompt_messages = assemble_imported_code_prompt(original_imported_code, 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 for non-imported code
if params.get("resultImage"):
prompt_messages = assemble_prompt(
params["image"], stack, params["resultImage"]
)
else:
prompt_messages = assemble_prompt(params["image"], stack)
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])
if input_mode == "video":
video_data_url = params["image"]
prompt_messages = await assemble_claude_prompt_video(video_data_url)
return prompt_messages, image_cache
def assemble_imported_code_prompt( def assemble_imported_code_prompt(
code: str, stack: Stack code: str, stack: Stack, result_image_data_url: Union[str, None] = None
) -> list[ChatCompletionMessageParam]: ) -> List[ChatCompletionMessageParam]:
system_content = IMPORTED_CODE_SYSTEM_PROMPTS[stack] system_content = IMPORTED_CODE_SYSTEM_PROMPTS[stack]
user_content = ( user_content = (
@ -84,12 +26,15 @@ def assemble_imported_code_prompt(
if stack != "svg" if stack != "svg"
else "Here is the code of the SVG: " + code else "Here is the code of the SVG: " + code
) )
return [ return [
{ {
"role": "system", "role": "system",
"content": system_content + "\n " + user_content, "content": system_content,
} },
{
"role": "user",
"content": user_content,
},
] ]
# TODO: Use result_image_data_url # TODO: Use result_image_data_url
@ -98,11 +43,11 @@ def assemble_prompt(
image_data_url: str, image_data_url: str,
stack: Stack, stack: Stack,
result_image_data_url: Union[str, None] = None, result_image_data_url: Union[str, None] = None,
) -> list[ChatCompletionMessageParam]: ) -> List[ChatCompletionMessageParam]:
system_content = SYSTEM_PROMPTS[stack] system_content = SYSTEM_PROMPTS[stack]
user_prompt = USER_PROMPT if stack != "svg" else SVG_USER_PROMPT user_prompt = USER_PROMPT if stack != "svg" else SVG_USER_PROMPT
user_content: list[ChatCompletionContentPartParam] = [ user_content: List[ChatCompletionContentPartParam] = [
{ {
"type": "image_url", "type": "image_url",
"image_url": {"url": image_data_url, "detail": "high"}, "image_url": {"url": image_data_url, "detail": "high"},

View File

@ -18,22 +18,6 @@ Return only the full code in <html></html> tags.
Do not include markdown "```" or "```html" at the start or end. Do not include markdown "```" or "```html" at the start or end.
""" """
IMPORTED_CODE_HTML_CSS_SYSTEM_PROMPT = """
You are an expert CSS developer.
- Do not add comments in the code such as "<!-- Add other navigation links as needed -->" and "<!-- ... other news items ... -->" 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 "<!-- Repeat for each news item -->" 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,
- You can use Google Fonts
- Font Awesome for icons: <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css"></link>
Return only the full code in <html></html> tags.
Do not include markdown "```" or "```html" at the start or end.
"""
IMPORTED_CODE_REACT_TAILWIND_SYSTEM_PROMPT = """ IMPORTED_CODE_REACT_TAILWIND_SYSTEM_PROMPT = """
You are an expert React/Tailwind developer You are an expert React/Tailwind developer
@ -144,7 +128,6 @@ Do not include markdown "```" or "```svg" at the start or end.
IMPORTED_CODE_SYSTEM_PROMPTS = SystemPrompts( IMPORTED_CODE_SYSTEM_PROMPTS = SystemPrompts(
html_tailwind=IMPORTED_CODE_TAILWIND_SYSTEM_PROMPT, html_tailwind=IMPORTED_CODE_TAILWIND_SYSTEM_PROMPT,
html_css=IMPORTED_CODE_HTML_CSS_SYSTEM_PROMPT,
react_tailwind=IMPORTED_CODE_REACT_TAILWIND_SYSTEM_PROMPT, react_tailwind=IMPORTED_CODE_REACT_TAILWIND_SYSTEM_PROMPT,
bootstrap=IMPORTED_CODE_BOOTSTRAP_SYSTEM_PROMPT, bootstrap=IMPORTED_CODE_BOOTSTRAP_SYSTEM_PROMPT,
ionic_tailwind=IMPORTED_CODE_IONIC_TAILWIND_SYSTEM_PROMPT, ionic_tailwind=IMPORTED_CODE_IONIC_TAILWIND_SYSTEM_PROMPT,

View File

@ -26,30 +26,6 @@ Return only the full code in <html></html> tags.
Do not include markdown "```" or "```html" at the start or end. Do not include markdown "```" or "```html" at the start or end.
""" """
HTML_CSS_SYSTEM_PROMPT = """
You are an expert CSS developer
You take screenshots of a reference web page from the user, and then build single page apps
using CSS, 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 "<!-- Add other navigation links as needed -->" and "<!-- ... other news items ... -->" 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 "<!-- Repeat for each news item -->" 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,
- You can use Google Fonts
- Font Awesome for icons: <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css"></link>
Return only the full code in <html></html> tags.
Do not include markdown "```" or "```html" at the start or end.
"""
BOOTSTRAP_SYSTEM_PROMPT = """ BOOTSTRAP_SYSTEM_PROMPT = """
You are an expert Bootstrap developer You are an expert Bootstrap developer
You take screenshots of a reference web page from the user, and then build single page apps You take screenshots of a reference web page from the user, and then build single page apps
@ -200,7 +176,6 @@ Do not include markdown "```" or "```svg" at the start or end.
SYSTEM_PROMPTS = SystemPrompts( SYSTEM_PROMPTS = SystemPrompts(
html_css=HTML_CSS_SYSTEM_PROMPT,
html_tailwind=HTML_TAILWIND_SYSTEM_PROMPT, html_tailwind=HTML_TAILWIND_SYSTEM_PROMPT,
react_tailwind=REACT_TAILWIND_SYSTEM_PROMPT, react_tailwind=REACT_TAILWIND_SYSTEM_PROMPT,
bootstrap=BOOTSTRAP_SYSTEM_PROMPT, bootstrap=BOOTSTRAP_SYSTEM_PROMPT,

View File

@ -1,4 +1,3 @@
from llm import Llm
from prompts import assemble_imported_code_prompt, assemble_prompt from prompts import assemble_imported_code_prompt, assemble_prompt
TAILWIND_SYSTEM_PROMPT = """ TAILWIND_SYSTEM_PROMPT = """
@ -26,30 +25,6 @@ Return only the full code in <html></html> tags.
Do not include markdown "```" or "```html" at the start or end. Do not include markdown "```" or "```html" at the start or end.
""" """
HTML_CSS_SYSTEM_PROMPT = """
You are an expert CSS developer
You take screenshots of a reference web page from the user, and then build single page apps
using CSS, 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 "<!-- Add other navigation links as needed -->" and "<!-- ... other news items ... -->" 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 "<!-- Repeat for each news item -->" 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,
- You can use Google Fonts
- Font Awesome for icons: <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css"></link>
Return only the full code in <html></html> tags.
Do not include markdown "```" or "```html" at the start or end.
"""
BOOTSTRAP_SYSTEM_PROMPT = """ BOOTSTRAP_SYSTEM_PROMPT = """
You are an expert Bootstrap developer You are an expert Bootstrap developer
You take screenshots of a reference web page from the user, and then build single page apps You take screenshots of a reference web page from the user, and then build single page apps
@ -214,22 +189,6 @@ Return only the full code in <html></html> tags.
Do not include markdown "```" or "```html" at the start or end. Do not include markdown "```" or "```html" at the start or end.
""" """
IMPORTED_CODE_HTML_CSS_SYSTEM_PROMPT = """
You are an expert CSS developer.
- Do not add comments in the code such as "<!-- Add other navigation links as needed -->" and "<!-- ... other news items ... -->" 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 "<!-- Repeat for each news item -->" 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,
- You can use Google Fonts
- Font Awesome for icons: <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css"></link>
Return only the full code in <html></html> tags.
Do not include markdown "```" or "```html" at the start or end.
"""
IMPORTED_CODE_REACT_TAILWIND_SYSTEM_PROMPT = """ IMPORTED_CODE_REACT_TAILWIND_SYSTEM_PROMPT = """
You are an expert React/Tailwind developer You are an expert React/Tailwind developer
@ -352,120 +311,87 @@ def test_prompts():
tailwind_prompt = assemble_prompt( tailwind_prompt = assemble_prompt(
"image_data_url", "html_tailwind", "result_image_data_url" "image_data_url", "html_tailwind", "result_image_data_url"
) )
assert tailwind_prompt[0].get("content") == TAILWIND_SYSTEM_PROMPT assert tailwind_prompt[0]["content"] == TAILWIND_SYSTEM_PROMPT
assert tailwind_prompt[1]["content"][2]["text"] == USER_PROMPT # type: ignore assert tailwind_prompt[1]["content"][2]["text"] == USER_PROMPT # type: ignore
html_css_prompt = assemble_prompt(
"image_data_url", "html_css", "result_image_data_url"
)
assert html_css_prompt[0].get("content") == HTML_CSS_SYSTEM_PROMPT
assert html_css_prompt[1]["content"][2]["text"] == USER_PROMPT # type: ignore
react_tailwind_prompt = assemble_prompt( react_tailwind_prompt = assemble_prompt(
"image_data_url", "react_tailwind", "result_image_data_url" "image_data_url", "react_tailwind", "result_image_data_url"
) )
assert react_tailwind_prompt[0].get("content") == REACT_TAILWIND_SYSTEM_PROMPT assert react_tailwind_prompt[0]["content"] == REACT_TAILWIND_SYSTEM_PROMPT
assert react_tailwind_prompt[1]["content"][2]["text"] == USER_PROMPT # type: ignore assert react_tailwind_prompt[1]["content"][2]["text"] == USER_PROMPT # type: ignore
bootstrap_prompt = assemble_prompt( bootstrap_prompt = assemble_prompt(
"image_data_url", "bootstrap", "result_image_data_url" "image_data_url", "bootstrap", "result_image_data_url"
) )
assert bootstrap_prompt[0].get("content") == BOOTSTRAP_SYSTEM_PROMPT assert bootstrap_prompt[0]["content"] == BOOTSTRAP_SYSTEM_PROMPT
assert bootstrap_prompt[1]["content"][2]["text"] == USER_PROMPT # type: ignore assert bootstrap_prompt[1]["content"][2]["text"] == USER_PROMPT # type: ignore
ionic_tailwind = assemble_prompt( ionic_tailwind = assemble_prompt(
"image_data_url", "ionic_tailwind", "result_image_data_url" "image_data_url", "ionic_tailwind", "result_image_data_url"
) )
assert ionic_tailwind[0].get("content") == IONIC_TAILWIND_SYSTEM_PROMPT assert ionic_tailwind[0]["content"] == IONIC_TAILWIND_SYSTEM_PROMPT
assert ionic_tailwind[1]["content"][2]["text"] == USER_PROMPT # type: ignore assert ionic_tailwind[1]["content"][2]["text"] == USER_PROMPT # type: ignore
vue_tailwind = assemble_prompt( vue_tailwind = assemble_prompt(
"image_data_url", "vue_tailwind", "result_image_data_url" "image_data_url", "vue_tailwind", "result_image_data_url"
) )
assert vue_tailwind[0].get("content") == VUE_TAILWIND_SYSTEM_PROMPT assert vue_tailwind[0]["content"] == VUE_TAILWIND_SYSTEM_PROMPT
assert vue_tailwind[1]["content"][2]["text"] == USER_PROMPT # type: ignore assert vue_tailwind[1]["content"][2]["text"] == USER_PROMPT # type: ignore
svg_prompt = assemble_prompt("image_data_url", "svg", "result_image_data_url") svg_prompt = assemble_prompt("image_data_url", "svg", "result_image_data_url")
assert svg_prompt[0].get("content") == SVG_SYSTEM_PROMPT assert svg_prompt[0]["content"] == SVG_SYSTEM_PROMPT
assert svg_prompt[1]["content"][2]["text"] == SVG_USER_PROMPT # type: ignore assert svg_prompt[1]["content"][2]["text"] == SVG_USER_PROMPT # type: ignore
def test_imported_code_prompts(): def test_imported_code_prompts():
code = "Sample code" tailwind_prompt = assemble_imported_code_prompt(
"code", "html_tailwind", "result_image_data_url"
tailwind_prompt = assemble_imported_code_prompt(code, "html_tailwind") )
expected_tailwind_prompt = [ expected_tailwind_prompt = [
{ {"role": "system", "content": IMPORTED_CODE_TAILWIND_SYSTEM_PROMPT},
"role": "system", {"role": "user", "content": "Here is the code of the app: code"},
"content": IMPORTED_CODE_TAILWIND_SYSTEM_PROMPT
+ "\n Here is the code of the app: "
+ code,
}
] ]
assert tailwind_prompt == expected_tailwind_prompt assert tailwind_prompt == expected_tailwind_prompt
html_css_prompt = assemble_imported_code_prompt(code, "html_css") react_tailwind_prompt = assemble_imported_code_prompt(
expected_html_css_prompt = [ "code", "react_tailwind", "result_image_data_url"
{ )
"role": "system",
"content": IMPORTED_CODE_HTML_CSS_SYSTEM_PROMPT
+ "\n Here is the code of the app: "
+ code,
}
]
assert html_css_prompt == expected_html_css_prompt
react_tailwind_prompt = assemble_imported_code_prompt(code, "react_tailwind")
expected_react_tailwind_prompt = [ expected_react_tailwind_prompt = [
{ {"role": "system", "content": IMPORTED_CODE_REACT_TAILWIND_SYSTEM_PROMPT},
"role": "system", {"role": "user", "content": "Here is the code of the app: code"},
"content": IMPORTED_CODE_REACT_TAILWIND_SYSTEM_PROMPT
+ "\n Here is the code of the app: "
+ code,
}
] ]
assert react_tailwind_prompt == expected_react_tailwind_prompt assert react_tailwind_prompt == expected_react_tailwind_prompt
bootstrap_prompt = assemble_imported_code_prompt(code, "bootstrap") bootstrap_prompt = assemble_imported_code_prompt(
"code", "bootstrap", "result_image_data_url"
)
expected_bootstrap_prompt = [ expected_bootstrap_prompt = [
{ {"role": "system", "content": IMPORTED_CODE_BOOTSTRAP_SYSTEM_PROMPT},
"role": "system", {"role": "user", "content": "Here is the code of the app: code"},
"content": IMPORTED_CODE_BOOTSTRAP_SYSTEM_PROMPT
+ "\n Here is the code of the app: "
+ code,
}
] ]
assert bootstrap_prompt == expected_bootstrap_prompt assert bootstrap_prompt == expected_bootstrap_prompt
ionic_tailwind = assemble_imported_code_prompt(code, "ionic_tailwind") ionic_tailwind = assemble_imported_code_prompt(
"code", "ionic_tailwind", "result_image_data_url"
)
expected_ionic_tailwind = [ expected_ionic_tailwind = [
{ {"role": "system", "content": IMPORTED_CODE_IONIC_TAILWIND_SYSTEM_PROMPT},
"role": "system", {"role": "user", "content": "Here is the code of the app: code"},
"content": IMPORTED_CODE_IONIC_TAILWIND_SYSTEM_PROMPT
+ "\n Here is the code of the app: "
+ code,
}
] ]
assert ionic_tailwind == expected_ionic_tailwind assert ionic_tailwind == expected_ionic_tailwind
vue_tailwind = assemble_imported_code_prompt(code, "vue_tailwind") vue_tailwind = assemble_imported_code_prompt(
"code", "vue_tailwind", "result_image_data_url"
)
expected_vue_tailwind = [ expected_vue_tailwind = [
{ {"role": "system", "content": IMPORTED_CODE_VUE_TAILWIND_PROMPT},
"role": "system", {"role": "user", "content": "Here is the code of the app: code"},
"content": IMPORTED_CODE_VUE_TAILWIND_PROMPT
+ "\n Here is the code of the app: "
+ code,
}
] ]
assert vue_tailwind == expected_vue_tailwind assert vue_tailwind == expected_vue_tailwind
svg = assemble_imported_code_prompt(code, "svg") svg = assemble_imported_code_prompt("code", "svg", "result_image_data_url")
expected_svg = [ expected_svg = [
{ {"role": "system", "content": IMPORTED_CODE_SVG_SYSTEM_PROMPT},
"role": "system", {"role": "user", "content": "Here is the code of the SVG: code"},
"content": IMPORTED_CODE_SVG_SYSTEM_PROMPT
+ "\n Here is the code of the SVG: "
+ code,
}
] ]
assert svg == expected_svg assert svg == expected_svg

View File

@ -2,7 +2,6 @@ from typing import Literal, TypedDict
class SystemPrompts(TypedDict): class SystemPrompts(TypedDict):
html_css: str
html_tailwind: str html_tailwind: str
react_tailwind: str react_tailwind: str
bootstrap: str bootstrap: str
@ -12,7 +11,6 @@ class SystemPrompts(TypedDict):
Stack = Literal[ Stack = Literal[
"html_css",
"html_tailwind", "html_tailwind",
"react_tailwind", "react_tailwind",
"bootstrap", "bootstrap",

View File

@ -17,9 +17,6 @@ httpx = "^0.25.1"
pre-commit = "^3.6.2" pre-commit = "^3.6.2"
anthropic = "^0.18.0" anthropic = "^0.18.0"
moviepy = "^1.0.3" moviepy = "^1.0.3"
pillow = "^10.3.0"
types-pillow = "^10.2.0.20240520"
aiohttp = "^3.9.5"
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]
pytest = "^7.4.3" pytest = "^7.4.3"

View File

@ -7,13 +7,10 @@ from evals.config import EVALS_DIR
router = APIRouter() router = APIRouter()
# Update this if the number of outputs generated per input changes
N = 1
class Eval(BaseModel): class Eval(BaseModel):
input: str input: str
outputs: list[str] output: str
@router.get("/evals") @router.get("/evals")
@ -28,27 +25,21 @@ async def get_evals():
input_file_path = os.path.join(input_dir, file) input_file_path = os.path.join(input_dir, file)
input_file = await image_to_data_url(input_file_path) input_file = await image_to_data_url(input_file_path)
# Construct the corresponding output file names # Construct the corresponding output file name
output_file_names = [ output_file_name = file.replace(".png", ".html")
file.replace(".png", f"_{i}.html") for i in range(0, N)
] # Assuming 3 outputs for each input
output_files_data: list[str] = []
for output_file_name in output_file_names:
output_file_path = os.path.join(output_dir, output_file_name) output_file_path = os.path.join(output_dir, output_file_name)
# Check if the output file exists # Check if the output file exists
if os.path.exists(output_file_path): if os.path.exists(output_file_path):
with open(output_file_path, "r") as f: with open(output_file_path, "r") as f:
output_files_data.append(f.read()) output_file_data = f.read()
else: else:
output_files_data.append( output_file_data = "Output file not found."
"<html><h1>Output file not found.</h1></html>"
)
evals.append( evals.append(
Eval( Eval(
input=input_file, input=input_file,
outputs=output_files_data, output=output_file_data,
) )
) )

View File

@ -1,343 +1,265 @@
import asyncio import os
from dataclasses import dataclass
import traceback import traceback
from fastapi import APIRouter, WebSocket from fastapi import APIRouter, WebSocket
import openai import openai
from codegen.utils import extract_html_content from config import ANTHROPIC_API_KEY, IS_PROD, SHOULD_MOCK_AI_RESPONSE
from config import (
ANTHROPIC_API_KEY,
IS_PROD,
NUM_VARIANTS,
OPENAI_API_KEY,
OPENAI_BASE_URL,
REPLICATE_API_KEY,
SHOULD_MOCK_AI_RESPONSE,
)
from custom_types import InputMode from custom_types import InputMode
from llm import ( from llm import (
Llm, CODE_GENERATION_MODELS,
convert_frontend_str_to_llm, MODEL_CLAUDE_OPUS,
stream_claude_response, stream_claude_response,
stream_claude_response_native, stream_claude_response_native,
stream_openai_response, stream_openai_response,
) )
from fs_logging.core import write_logs from openai.types.chat import ChatCompletionMessageParam
from mock_llm import mock_completion from mock_llm import mock_completion
from typing import Any, Callable, Coroutine, Dict, List, Literal, cast, get_args from typing import Dict, List, cast, get_args
from image_generation.core import generate_images from image_generation import create_alt_url_mapping, generate_images
from prompts import create_prompt from prompts import assemble_imported_code_prompt, assemble_prompt
from access_token import validate_access_token
from datetime import datetime
import json
from prompts.claude_prompts import VIDEO_PROMPT from prompts.claude_prompts import VIDEO_PROMPT
from prompts.types import Stack from prompts.types import Stack
# from utils import pprint_prompt # from utils import pprint_prompt
from ws.constants import APP_ERROR_WEB_SOCKET_CODE # type: ignore from video.utils import extract_tag_content, assemble_claude_prompt_video # type: ignore
router = APIRouter() router = APIRouter()
# Auto-upgrade usage of older models def write_logs(prompt_messages: List[ChatCompletionMessageParam], completion: str):
def auto_upgrade_model(code_generation_model: Llm) -> Llm: # Get the logs path from environment, default to the current working directory
if code_generation_model in {Llm.GPT_4_VISION, Llm.GPT_4_TURBO_2024_04_09}: logs_path = os.environ.get("LOGS_PATH", os.getcwd())
print(
f"Initial deprecated model: {code_generation_model}. Auto-updating code generation model to GPT-4O-2024-05-13"
)
return Llm.GPT_4O_2024_05_13
elif code_generation_model == Llm.CLAUDE_3_SONNET:
print(
f"Initial deprecated model: {code_generation_model}. Auto-updating code generation model to CLAUDE-3.5-SONNET-2024-06-20"
)
return Llm.CLAUDE_3_5_SONNET_2024_06_20
return code_generation_model
# 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)
# Generate images, if needed print("Writing to logs directory:", logs_directory)
async def perform_image_generation(
completion: str,
should_generate_images: bool,
openai_api_key: str | None,
openai_base_url: str | None,
image_cache: dict[str, str],
):
replicate_api_key = REPLICATE_API_KEY
if not should_generate_images:
return completion
if replicate_api_key: # Generate a unique filename using the current timestamp within the logs directory
image_generation_model = "flux" filename = datetime.now().strftime(f"{logs_directory}/messages_%Y%m%d_%H%M%S.json")
api_key = replicate_api_key
else:
if not openai_api_key:
print(
"No OpenAI API key and Replicate key found. Skipping image generation."
)
return completion
image_generation_model = "dalle3"
api_key = openai_api_key
print("Generating images with model: ", image_generation_model) # Write the messages dict into a new file for each run
with open(filename, "w") as f:
return await generate_images( f.write(json.dumps({"prompt": prompt_messages, "completion": completion}))
completion,
api_key=api_key,
base_url=openai_base_url,
image_cache=image_cache,
model=image_generation_model,
)
@dataclass
class ExtractedParams:
stack: Stack
input_mode: InputMode
code_generation_model: Llm
should_generate_images: bool
openai_api_key: str | None
anthropic_api_key: str | None
openai_base_url: str | None
async def extract_params(
params: Dict[str, str], throw_error: Callable[[str], Coroutine[Any, Any, None]]
) -> ExtractedParams:
# Read the code config settings (stack) from the request.
generated_code_config = params.get("generatedCodeConfig", "")
if generated_code_config not in get_args(Stack):
await throw_error(f"Invalid generated code config: {generated_code_config}")
raise ValueError(f"Invalid generated code config: {generated_code_config}")
validated_stack = cast(Stack, generated_code_config)
# Validate the input mode
input_mode = params.get("inputMode")
if input_mode not in get_args(InputMode):
await throw_error(f"Invalid input mode: {input_mode}")
raise ValueError(f"Invalid input mode: {input_mode}")
validated_input_mode = cast(InputMode, input_mode)
# Read the model from the request. Fall back to default if not provided.
code_generation_model_str = params.get(
"codeGenerationModel", Llm.GPT_4O_2024_05_13.value
)
try:
code_generation_model = convert_frontend_str_to_llm(code_generation_model_str)
except ValueError:
await throw_error(f"Invalid model: {code_generation_model_str}")
raise ValueError(f"Invalid model: {code_generation_model_str}")
openai_api_key = get_from_settings_dialog_or_env(
params, "openAiApiKey", OPENAI_API_KEY
)
# If neither is provided, we throw an error later only if Claude is used.
anthropic_api_key = get_from_settings_dialog_or_env(
params, "anthropicApiKey", ANTHROPIC_API_KEY
)
# Base URL for OpenAI API
openai_base_url: str | None = None
# Disable user-specified OpenAI Base URL in prod
if not IS_PROD:
openai_base_url = get_from_settings_dialog_or_env(
params, "openAiBaseURL", OPENAI_BASE_URL
)
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 = bool(params.get("isImageGenerationEnabled", True))
return ExtractedParams(
stack=validated_stack,
input_mode=validated_input_mode,
code_generation_model=code_generation_model,
should_generate_images=should_generate_images,
openai_api_key=openai_api_key,
anthropic_api_key=anthropic_api_key,
openai_base_url=openai_base_url,
)
def get_from_settings_dialog_or_env(
params: dict[str, str], key: str, env_var: str | None
) -> str | None:
value = params.get(key)
if value:
print(f"Using {key} from client-side settings dialog")
return value
if env_var:
print(f"Using {key} from environment variable")
return env_var
return None
@router.websocket("/generate-code") @router.websocket("/generate-code")
async def stream_code(websocket: WebSocket): async def stream_code(websocket: WebSocket):
await websocket.accept() await websocket.accept()
print("Incoming websocket connection...") print("Incoming websocket connection...")
## Communication protocol setup
async def throw_error( async def throw_error(
message: str, message: str,
): ):
print(message)
await websocket.send_json({"type": "error", "value": message}) await websocket.send_json({"type": "error", "value": message})
await websocket.close(APP_ERROR_WEB_SOCKET_CODE) await websocket.close()
async def send_message(
type: Literal["chunk", "status", "setCode", "error"],
value: str,
variantIndex: int,
):
# Print for debugging on the backend
if type == "error":
print(f"Error (variant {variantIndex}): {value}")
elif type == "status":
print(f"Status (variant {variantIndex}): {value}")
await websocket.send_json(
{"type": type, "value": value, "variantIndex": variantIndex}
)
## Parameter extract and validation
# TODO: Are the values always strings? # TODO: Are the values always strings?
params: dict[str, str] = await websocket.receive_json() params: Dict[str, str] = await websocket.receive_json()
print("Received params") print("Received params")
extracted_params = await extract_params(params, throw_error) # Read the code config settings from the request. Fall back to default if not provided.
stack = extracted_params.stack generated_code_config = ""
input_mode = extracted_params.input_mode if "generatedCodeConfig" in params and params["generatedCodeConfig"]:
code_generation_model = extracted_params.code_generation_model generated_code_config = params["generatedCodeConfig"]
openai_api_key = extracted_params.openai_api_key if not generated_code_config in get_args(Stack):
openai_base_url = extracted_params.openai_base_url await throw_error(f"Invalid generated code config: {generated_code_config}")
anthropic_api_key = extracted_params.anthropic_api_key return
should_generate_images = extracted_params.should_generate_images # Cast the variable to the Stack type
valid_stack = cast(Stack, generated_code_config)
# Auto-upgrade usage of older models # Validate the input mode
code_generation_model = auto_upgrade_model(code_generation_model) input_mode = params.get("inputMode")
if not input_mode in get_args(InputMode):
await throw_error(f"Invalid input mode: {input_mode}")
raise Exception(f"Invalid input mode: {input_mode}")
# Cast the variable to the right type
validated_input_mode = cast(InputMode, input_mode)
# Read the model from the request. Fall back to default if not provided.
code_generation_model = params.get("codeGenerationModel", "gpt_4_vision")
if code_generation_model not in CODE_GENERATION_MODELS:
await throw_error(f"Invalid model: {code_generation_model}")
raise Exception(f"Invalid model: {code_generation_model}")
print( print(
f"Generating {stack} code in {input_mode} mode using {code_generation_model}..." f"Generating {generated_code_config} code for uploaded {input_mode} using {code_generation_model} model..."
) )
for i in range(NUM_VARIANTS): # Get the OpenAI API key from the request. Fall back to environment variable if not provided.
await send_message("status", "Generating code...", i) # 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")
### Prompt creation 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
# 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 for updates so that we don't have to regenerate images
image_cache: Dict[str, str] = {} image_cache: Dict[str, str] = {}
try: # If this generation started off with imported code, we need to assemble the prompt differently
prompt_messages, image_cache = await create_prompt(params, stack, input_mode) if params.get("isImportedFromCode") and params["isImportedFromCode"]:
except: original_imported_code = params["history"][0]
await throw_error( prompt_messages = assemble_imported_code_prompt(
"Error assembling prompt. Contact support at support@picoapps.xyz" original_imported_code, valid_stack
) )
raise 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])
if validated_input_mode == "video":
video_data_url = params["image"]
prompt_messages = await assemble_claude_prompt_video(video_data_url)
# pprint_prompt(prompt_messages) # type: ignore # pprint_prompt(prompt_messages) # type: ignore
### Code generation
async def process_chunk(content: str, variantIndex: int):
await send_message("chunk", content, variantIndex)
if SHOULD_MOCK_AI_RESPONSE: if SHOULD_MOCK_AI_RESPONSE:
completions = [await mock_completion(process_chunk, input_mode=input_mode)] completion = await mock_completion(
process_chunk, input_mode=validated_input_mode
)
else: else:
try: try:
if input_mode == "video": if validated_input_mode == "video":
if not anthropic_api_key: if not ANTHROPIC_API_KEY:
await throw_error( await throw_error(
"Video only works with Anthropic models. No Anthropic API key found. Please add the environment variable ANTHROPIC_API_KEY to backend/.env or in the settings dialog" "No Anthropic API key found. Please add the environment variable ANTHROPIC_API_KEY to backend/.env"
) )
raise Exception("No Anthropic key") raise Exception("No Anthropic key")
completions = [ completion = await stream_claude_response_native(
await stream_claude_response_native(
system_prompt=VIDEO_PROMPT, system_prompt=VIDEO_PROMPT,
messages=prompt_messages, # type: ignore messages=prompt_messages, # type: ignore
api_key=anthropic_api_key, api_key=ANTHROPIC_API_KEY,
callback=lambda x: process_chunk(x, 0), callback=lambda x: process_chunk(x),
model=Llm.CLAUDE_3_OPUS, model=MODEL_CLAUDE_OPUS,
include_thinking=True, include_thinking=True,
) )
] elif code_generation_model == "claude_3_sonnet":
else: if not ANTHROPIC_API_KEY:
# Depending on the presence and absence of various keys,
# we decide which models to run
variant_models = []
if openai_api_key and anthropic_api_key:
variant_models = ["anthropic", "openai"]
elif openai_api_key:
variant_models = ["openai", "openai"]
elif anthropic_api_key:
variant_models = ["anthropic", "anthropic"]
else:
await throw_error( await throw_error(
"No OpenAI or Anthropic API key found. Please add the environment variable OPENAI_API_KEY or ANTHROPIC_API_KEY to backend/.env or in the settings dialog. If you add it to .env, make sure to restart the backend server." "No Anthropic API key found. Please add the environment variable ANTHROPIC_API_KEY to backend/.env"
) )
raise Exception("No OpenAI or Anthropic key") raise Exception("No Anthropic key")
tasks: List[Coroutine[Any, Any, str]] = [] completion = await stream_claude_response(
for index, model in enumerate(variant_models): prompt_messages, # type: ignore
if model == "openai": api_key=ANTHROPIC_API_KEY,
if openai_api_key is None: callback=lambda x: process_chunk(x),
await throw_error("OpenAI API key is missing.") )
raise Exception("OpenAI API key is missing.") else:
completion = await stream_openai_response(
tasks.append( prompt_messages, # type: ignore
stream_openai_response(
prompt_messages,
api_key=openai_api_key, api_key=openai_api_key,
base_url=openai_base_url, base_url=openai_base_url,
callback=lambda x, i=index: process_chunk(x, i), callback=lambda x: process_chunk(x),
model=Llm.GPT_4O_2024_05_13,
) )
)
elif model == "anthropic":
if anthropic_api_key is None:
await throw_error("Anthropic API key is missing.")
raise Exception("Anthropic API key is missing.")
tasks.append(
stream_claude_response(
prompt_messages,
api_key=anthropic_api_key,
callback=lambda x, i=index: process_chunk(x, i),
model=Llm.CLAUDE_3_5_SONNET_2024_06_20,
)
)
# Run the models in parallel and capture exceptions if any
completions = await asyncio.gather(*tasks, return_exceptions=True)
# If all generations failed, throw an error
all_generations_failed = all(
isinstance(completion, Exception) for completion in completions
)
if all_generations_failed:
await throw_error("Error generating code. Please contact support.")
# Print the all the underlying exceptions for debugging
for completion in completions:
traceback.print_exception(
type(completion), completion, completion.__traceback__
)
raise Exception("All generations failed")
# If some completions failed, replace them with empty strings
for index, completion in enumerate(completions):
if isinstance(completion, Exception):
completions[index] = ""
print("Generation failed for variant", index)
print("Models used for generation: ", variant_models)
except openai.AuthenticationError as e: except openai.AuthenticationError as e:
print("[GENERATE_CODE] Authentication failed", e) print("[GENERATE_CODE] Authentication failed", e)
error_message = ( error_message = (
@ -373,34 +295,37 @@ async def stream_code(websocket: WebSocket):
) )
return await throw_error(error_message) return await throw_error(error_message)
## Post-processing if validated_input_mode == "video":
completion = extract_tag_content("html", completion)
# Strip the completion of everything except the HTML content
completions = [extract_html_content(completion) for completion in completions]
# Write the messages dict into a log so that we can debug later # Write the messages dict into a log so that we can debug later
write_logs(prompt_messages, completions[0]) write_logs(prompt_messages, completion) # type: ignore
## Image Generation try:
if should_generate_images:
for index, _ in enumerate(completions): await websocket.send_json(
await send_message("status", "Generating images...", index) {"type": "status", "value": "Generating images..."}
)
image_generation_tasks = [ updated_html = await generate_images(
perform_image_generation( completion,
completion, api_key=openai_api_key,
should_generate_images, base_url=openai_base_url,
openai_api_key, image_cache=image_cache,
openai_base_url, )
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."}
) )
for completion in completions
]
updated_completions = await asyncio.gather(*image_generation_tasks)
for index, updated_html in enumerate(updated_completions):
await send_message("setCode", updated_html, index)
await send_message("status", "Code generation complete.", index)
await websocket.close() await websocket.close()

View File

@ -1,8 +1,6 @@
# Load environment variables first # Load environment variables first
from dotenv import load_dotenv from dotenv import load_dotenv
from llm import Llm
load_dotenv() load_dotenv()
import os import os
@ -10,12 +8,10 @@ from typing import Any, Coroutine
import asyncio import asyncio
from evals.config import EVALS_DIR from evals.config import EVALS_DIR
from evals.core import generate_code_for_image from evals.core import generate_code_core
from evals.utils import image_to_data_url from evals.utils import image_to_data_url
STACK = "html_tailwind" STACK = "html_tailwind"
# MODEL = Llm.CLAUDE_3_5_SONNET_2024_06_20
N = 2 # Number of outputs to generate
async def main(): async def main():
@ -29,79 +25,19 @@ async def main():
for filename in evals: for filename in evals:
filepath = os.path.join(INPUT_DIR, filename) filepath = os.path.join(INPUT_DIR, filename)
data_url = await image_to_data_url(filepath) data_url = await image_to_data_url(filepath)
for n in range(N): # Generate N tasks for each input task = generate_code_core(data_url, STACK)
if n == 0:
task = generate_code_for_image(
image_url=data_url,
stack=STACK,
model=Llm.CLAUDE_3_5_SONNET_2024_06_20,
)
else:
task = generate_code_for_image(
image_url=data_url, stack=STACK, model=Llm.GPT_4O_2024_05_13
)
tasks.append(task) tasks.append(task)
print(f"Generating {len(tasks)} codes")
results = await asyncio.gather(*tasks) results = await asyncio.gather(*tasks)
os.makedirs(OUTPUT_DIR, exist_ok=True) os.makedirs(OUTPUT_DIR, exist_ok=True)
for i, content in enumerate(results): for filename, content in zip(evals, results):
# Calculate index for filename and output number # File name is derived from the original filename in evals
eval_index = i // N output_filename = f"{os.path.splitext(filename)[0]}.html"
output_number = i % N
filename = evals[eval_index]
# File name is derived from the original filename in evals with an added output number
output_filename = f"{os.path.splitext(filename)[0]}_{output_number}.html"
output_filepath = os.path.join(OUTPUT_DIR, output_filename) output_filepath = os.path.join(OUTPUT_DIR, output_filename)
with open(output_filepath, "w") as file: with open(output_filepath, "w") as file:
file.write(content) file.write(content)
# async def text_main():
# OUTPUT_DIR = EVALS_DIR + "/outputs"
# GENERAL_TEXT_V1 = [
# "Login form",
# "Simple notification",
# "button",
# "saas dashboard",
# "landing page for barber shop",
# ]
# tasks: list[Coroutine[Any, Any, str]] = []
# for prompt in GENERAL_TEXT_V1:
# for n in range(N): # Generate N tasks for each input
# if n == 0:
# task = generate_code_for_text(
# text=prompt,
# stack=STACK,
# model=Llm.CLAUDE_3_5_SONNET_2024_06_20,
# )
# else:
# task = generate_code_for_text(
# text=prompt, stack=STACK, model=Llm.GPT_4O_2024_05_13
# )
# tasks.append(task)
# print(f"Generating {len(tasks)} codes")
# results = await asyncio.gather(*tasks)
# os.makedirs(OUTPUT_DIR, exist_ok=True)
# for i, content in enumerate(results):
# # Calculate index for filename and output number
# eval_index = i // N
# output_number = i % N
# filename = GENERAL_TEXT_V1[eval_index]
# # File name is derived from the original filename in evals with an added output number
# output_filename = f"{os.path.splitext(filename)[0]}_{output_number}.html"
# output_filepath = os.path.join(OUTPUT_DIR, output_filename)
# with open(output_filepath, "w") as file:
# file.write(content)
asyncio.run(main()) asyncio.run(main())

View File

@ -1,85 +0,0 @@
import asyncio
import os
from typing import List, Optional, Literal
from dotenv import load_dotenv
import aiohttp
from image_generation.core import process_tasks
EVALS = [
"Romantic Background",
"Company logo: A stylized green sprout emerging from a circle",
"Placeholder image of a PDF cover with abstract design",
"A complex bubble diagram showing various interconnected features and aspects of FestivalPro, with a large central bubble surrounded by smaller bubbles of different colors representing different categories and functionalities",
"A vibrant, abstract visualization of the RhythmRise experience ecosystem, featuring interconnected neon elements representing music, technology, and human connection",
"Banner with text 'LiblibAI学院 课程入口'",
"Profile picture of Pierre-Louis Labonne",
"Two hands holding iPhone 14 models with colorful displays",
"Portrait of a woman with long dark hair smiling at the camera",
"Threadless logo on a gradient background from light pink to coral",
"Jordan Schlansky Shows Conan His Favorite Nose Hair Trimmer",
"Team Coco",
"Intro to Large Language Models",
"Andrej Karpathy",
"He built a $200 million toy company",
"CNBC International",
"What will happen in year three of the war?",
"Channel",
"This is it",
"How ASML Dominates Chip Machines",
]
# Load environment variables
load_dotenv()
# Get API keys from environment variables
OPENAI_API_KEY: Optional[str] = os.getenv("OPENAI_API_KEY")
REPLICATE_API_TOKEN: Optional[str] = os.getenv("REPLICATE_API_TOKEN")
# Directory to save generated images
OUTPUT_DIR: str = "generated_images"
async def generate_and_save_images(
prompts: List[str],
model: Literal["dalle3", "flux"],
api_key: Optional[str],
) -> None:
# Ensure the output directory exists
os.makedirs(OUTPUT_DIR, exist_ok=True)
if api_key is None:
raise ValueError(f"API key for {model} is not set in the environment variables")
# Generate images
results: List[Optional[str]] = await process_tasks(
prompts, api_key, None, model=model
)
# Save images to disk
async with aiohttp.ClientSession() as session:
for i, image_url in enumerate(results):
if image_url:
# Get the image data
async with session.get(image_url) as response:
image_data: bytes = await response.read()
# Save the image with a filename based on the input eval
prefix = "replicate_" if model == "flux" else "dalle3_"
filename: str = (
f"{prefix}{prompts[i][:50].replace(' ', '_').replace(':', '')}.png"
)
filepath: str = os.path.join(OUTPUT_DIR, filename)
with open(filepath, "wb") as f:
f.write(image_data)
print(f"Saved {model} image: {filepath}")
else:
print(f"Failed to generate {model} image for prompt: {prompts[i]}")
async def main() -> None:
# await generate_and_save_images(EVALS, "dalle3", OPENAI_API_KEY)
await generate_and_save_images(EVALS, "flux", REPLICATE_API_TOKEN)
if __name__ == "__main__":
asyncio.run(main())

View File

@ -1,41 +0,0 @@
import unittest
from llm import convert_frontend_str_to_llm, Llm
class TestConvertFrontendStrToLlm(unittest.TestCase):
def test_convert_valid_strings(self):
self.assertEqual(
convert_frontend_str_to_llm("gpt_4_vision"),
Llm.GPT_4_VISION,
"Should convert 'gpt_4_vision' to Llm.GPT_4_VISION",
)
self.assertEqual(
convert_frontend_str_to_llm("claude_3_sonnet"),
Llm.CLAUDE_3_SONNET,
"Should convert 'claude_3_sonnet' to Llm.CLAUDE_3_SONNET",
)
self.assertEqual(
convert_frontend_str_to_llm("claude-3-opus-20240229"),
Llm.CLAUDE_3_OPUS,
"Should convert 'claude-3-opus-20240229' to Llm.CLAUDE_3_OPUS",
)
self.assertEqual(
convert_frontend_str_to_llm("gpt-4-turbo-2024-04-09"),
Llm.GPT_4_TURBO_2024_04_09,
"Should convert 'gpt-4-turbo-2024-04-09' to Llm.GPT_4_TURBO_2024_04_09",
)
self.assertEqual(
convert_frontend_str_to_llm("gpt-4o-2024-05-13"),
Llm.GPT_4O_2024_05_13,
"Should convert 'gpt-4o-2024-05-13' to Llm.GPT_4O_2024_05_13",
)
def test_convert_invalid_string_raises_exception(self):
with self.assertRaises(ValueError):
convert_frontend_str_to_llm("invalid_string")
with self.assertRaises(ValueError):
convert_frontend_str_to_llm("another_invalid_string")
if __name__ == "__main__":
unittest.main()

View File

@ -5,7 +5,7 @@ import mimetypes
import os import os
import tempfile import tempfile
import uuid import uuid
from typing import Any, Union, cast from typing import Union, cast
from moviepy.editor import VideoFileClip # type: ignore from moviepy.editor import VideoFileClip # type: ignore
from PIL import Image from PIL import Image
import math import math
@ -17,7 +17,7 @@ TARGET_NUM_SCREENSHOTS = (
) )
async def assemble_claude_prompt_video(video_data_url: str) -> list[Any]: async def assemble_claude_prompt_video(video_data_url: str):
images = split_video_into_screenshots(video_data_url) images = split_video_into_screenshots(video_data_url)
# Save images to tmp if we're debugging # Save images to tmp if we're debugging
@ -28,7 +28,7 @@ async def assemble_claude_prompt_video(video_data_url: str) -> list[Any]:
print(f"Number of frames extracted from video: {len(images)}") print(f"Number of frames extracted from video: {len(images)}")
if len(images) > 20: if len(images) > 20:
print(f"Too many screenshots: {len(images)}") print(f"Too many screenshots: {len(images)}")
raise ValueError("Too many screenshots extracted from video") return
# Convert images to the message format for Claude # Convert images to the message format for Claude
content_messages: list[dict[str, Union[dict[str, str], str]]] = [] content_messages: list[dict[str, Union[dict[str, str], str]]] = []

View File

@ -17,7 +17,8 @@ from utils import pprint_prompt
from config import ANTHROPIC_API_KEY from config import ANTHROPIC_API_KEY
from video.utils import extract_tag_content, assemble_claude_prompt_video from video.utils import extract_tag_content, assemble_claude_prompt_video
from llm import ( from llm import (
Llm, MODEL_CLAUDE_OPUS,
# MODEL_CLAUDE_SONNET,
stream_claude_response_native, stream_claude_response_native,
) )
@ -86,7 +87,7 @@ async def main():
messages=prompt_messages, messages=prompt_messages,
api_key=ANTHROPIC_API_KEY, api_key=ANTHROPIC_API_KEY,
callback=lambda x: process_chunk(x), callback=lambda x: process_chunk(x),
model=Llm.CLAUDE_3_OPUS, model=MODEL_CLAUDE_OPUS,
include_thinking=True, include_thinking=True,
) )

View File

View File

@ -1,2 +0,0 @@
# WebSocket protocol (RFC 6455) allows for the use of custom close codes in the range 4000-4999
APP_ERROR_WEB_SOCKET_CODE = 4332

View File

@ -1,24 +1,21 @@
# Claude 3 for converting screenshots to code # Evaluating Claude 3 at converting screenshots to code
Claude 3 dropped yesterday, claiming to rival GPT-4 on a wide variety of tasks. I maintain a very popular open source project called “screenshot-to-code” (this one!) that uses GPT-4 vision to convert screenshots/designs into clean code. Naturally, I was excited to see how good Claude 3 was at this task. [video running both]
**TLDR:** Claude 3 is on par with GPT-4 vision for screenshot to code, better in some ways but worse in others. Claude 3 dropped yesterday, claiming to rival GPT-4 on a wide variety of tasks. I maintain a very popular open source project (41k+ Github stars) called “screenshot-to-code” (this one!) that uses GPT-4 vision to convert screenshots/designs into clean code. Naturally, I was excited to see how good Claude 3 was at this task!
## Evaluation Setup ## Evaluation Setup
I dont know of a public benchmark for “screenshot to code” so I created simple evaluation setup for the purposes of testing: I dont know of a public benchmark for the “screenshot to code” task so I created simple evaluation set up for the purposes of testing\*:
- **Evaluation Dataset**: 16 screenshots with a mix of UI elements, landing pages, dashboards and popular websites. - **Evaluation Dataset**: 16 screenshots with a mix of UI elements, landing pages, dashboards and popular websites.
<img width="784" alt="Screenshot 2024-03-05 at 3 05 52PM" src="https://github.com/abi/screenshot-to-code/assets/23818/c32af2db-eb5a-44c1-9a19-2f0c3dd11ab4"> - **Evaluation Metric**: Replication accuracy, as in “How close does the generated code look to the screenshot?” There are other metrics that are important for sure but this is by far the #1 thing that most developers and users of the repo care about. Each output is subjectively rated by a human (me!) on a rating scale from 0 to 4. 4 = very close to an exact replica while 0 = nothing like the screenshot. With 16 screenshots, the maximum any model can score is 64. I like to compare the percentage of the maximum possible score for each run.
- **Evaluation Metric**: Replication accuracy, as in “How close does the generated code look to the screenshot?” While there are other metrics that are important like code quality, speed and so on, this is by far the #1 thing most users of the repo care about. \*Ive used it mostly for testing prompt variations until now since no model has come close to GPT-4 vision yet.
- **Evaluation Mechanism**: Each output is subjectively rated by a human on a rating scale from 0 to 4. 4 = very close to an exact replica while 0 = nothing like the screenshot. With 16 screenshots, the maximum any model can score is 64.
To make the evaluation process easy, I created [a Python script](https://github.com/abi/screenshot-to-code/blob/main/backend/run_evals.py) that runs code for all the inputs in parallel. I also have a simple UI to do a side-by-side comparison of the input and output, shown below.
To make the evaluation process easy, I created [a Python script](https://github.com/abi/screenshot-to-code/blob/main/backend/run_evals.py) that runs code for all the inputs in parallel. I also made a simple UI to do a side-by-side comparison of the input and output. [video of input/output]
![Google Chrome](https://github.com/abi/screenshot-to-code/assets/23818/38126f8f-205d-4ed1-b8cf-039e81dcc3d0)
## Results ## Results
@ -29,31 +26,21 @@ I only ran the evals on HTML/Tailwind here which is the stack where GPT-4 vision
Here are the results (average of 3 runs for each model): Here are the results (average of 3 runs for each model):
- GPT-4 Vision obtains a score of **65.10%** - this is what were trying to beat - GPT-4 Vision obtains a score of **65.10%** - this is what were trying to beat
- Claude 3 Sonnet receives a score of **70.31%**, which is a bit better. - Claude 3 Sonnet receives a score of **70.31%**, which is ~7.5% better.
- Surprisingly, Claude 3 Opus which is supposed to be the smarter and slower model scores worse than both GPT-4 vision and Claude 3 Sonnet, comes in at **61.46%**. - Surprisingly, Claude 3 Opus which is supposed to be the smarter and slower model scores worse than both GPT-4 vision and Claude 3 Sonnet, coming in at **61.46%**. Strange result.
Overall, a very strong showing for Claude 3. Obviously, there's a lot of subjectivity involved in this evaluation but Claude 3 is definitely on par with GPT-4 Vision, if not better. Overall, a very strong showing for Claude 3! Obviously, there's a lot of subjectivity involved in this evaluation but Claude 3 is definitely on par with GPT-4 Vision, if not better. It's also a lot less lazy than GPT-4 Vision.
You can see the [side-by-side comparison for a run of Claude 3 Sonnet here](https://github.com/abi/screenshot-to-code-files/blob/main/sonnet%20results.png). And for [a run of GPT-4 Vision here](https://github.com/abi/screenshot-to-code-files/blob/main/gpt%204%20vision%20results.png). You can see the results for a run of Claude 3 Sonnet here. You can see the results for a run of GPT-4 Vision here.
Other notes: Some other notes:
- The prompts used are optimized for GPT-4 vision. Adjusting the prompts a bit for Claude did yield a small improvement. But nothing game-changing and potentially not worth the trade-off of maintaining two sets of prompts. - The prompts used are optimized for GPT-4 vision. I played around with adjusting the prompts for Claude and that did yield a small improvement. Nothing game-changing and potentially not worth the trade-off of maintaining two sets of prompts.
- All the models excel at code quality - the quality is usually comparable to a human or better. - All these models excel at code quality - the quality is usually comparable to a human or better
- Claude 3 is much less lazy than GPT-4 Vision. When asked to recreate Hacker News, GPT-4 Vision will only create two items in the list and leave comments in this code like `<!-- Repeat for each news item -->` and `<!-- ... other news items ... -->`. - All the models seem to struggle with side-by-side layouts (the struggle is mostly in seeing, not in implementing) TODO: add iamge
<img width="699" alt="Screenshot 2024-03-05 at 9 25 04PM" src="https://github.com/abi/screenshot-to-code/assets/23818/04b03155-45e0-40b0-8de0-b1f0b4382bee"> - Claude 3 is much less lazy than GPT-4 Vision. TODO: Hacker News example
- Claude 3 Sonnet is faster and more steerable
While Claude 3 Sonnet can sometimes be lazy too, most of the time, it does what you ask it to do. Overall, I'm very impressed with Claude 3 Sonnet as a multimodal model for this use case. I've added it as an alternative to GPT-4 Vision in the open source repo (hosted version update coming soon). I'm sure it's possible to get Opus to be quite good as well.
<img width="904" alt="Screenshot 2024-03-05 at 9 30 23PM" src="https://github.com/abi/screenshot-to-code/assets/23818/b7c7d1ba-47c1-414d-928f-6989e81cf41d"> If youd like to contribute to this effort, I have some documentation on [running these evals yourself here](https://github.com/abi/screenshot-to-code/blob/main/Evaluation.md). If you know of a good tool for running evals like this (image input, code output), it would save me a lot of effort from having to build the tool from scratch.
- For some reason, all the models struggle with side-by-side "flex" layouts
<img width="1090" alt="Screenshot 2024-03-05 at 9 20 58PM" src="https://github.com/abi/screenshot-to-code/assets/23818/8957bb3a-da66-467d-997d-1c7cc24e6d9a">
- Claude 3 Sonnet is a lot faster
- Claude 3 gets background and text colors wrong quite often! (like in the Hacker News image above)
- My suspicion is that Claude 3 Opus results can be improved to be on par with the other models through better prompting
Overall, I'm very impressed with Claude 3 Sonnet for this use case. I've added it as an alternative to GPT-4 Vision in the open source repo (hosted version update coming soon).
If youd like to contribute to this effort, I have some documentation on [running these evals yourself here](https://github.com/abi/screenshot-to-code/blob/main/Evaluation.md). I'm also working on a better evaluation mechanism with Elo ratings and would love some help on that.

3
frontend/.gitignore vendored
View File

@ -25,6 +25,3 @@ dist-ssr
# Env files # Env files
.env* .env*
# Test files
src/tests/results/

View File

@ -1,4 +1,4 @@
FROM node:22-bullseye-slim FROM node:20.9-bullseye-slim
# Set the working directory in the container # Set the working directory in the container
WORKDIR /app WORKDIR /app
@ -6,9 +6,6 @@ WORKDIR /app
# Copy package.json and yarn.lock # Copy package.json and yarn.lock
COPY package.json yarn.lock /app/ COPY package.json yarn.lock /app/
# Set the environment variable to skip Puppeteer download
ENV PUPPETEER_SKIP_DOWNLOAD=true
# Install dependencies # Install dependencies
RUN yarn install RUN yarn install

View File

@ -2,7 +2,11 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/png" href="/favicon/main.png" /> <link
rel="icon"
type="image/svg+xml"
href="https://picoapps.xyz/favicon.png"
/>
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- Google Fonts --> <!-- Google Fonts -->

View File

@ -1,9 +0,0 @@
export default {
preset: "ts-jest",
testEnvironment: "node",
setupFiles: ["<rootDir>/src/setupTests.ts"],
transform: {
"^.+\\.tsx?$": "ts-jest",
},
testTimeout: 30000,
};

View File

@ -10,7 +10,7 @@
"build-hosted": "tsc && vite build --mode prod", "build-hosted": "tsc && vite build --mode prod",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview", "preview": "vite preview",
"test": "jest" "test": "vitest"
}, },
"dependencies": { "dependencies": {
"@codemirror/lang-html": "^6.4.6", "@codemirror/lang-html": "^6.4.6",
@ -36,7 +36,6 @@
"codemirror": "^6.0.1", "codemirror": "^6.0.1",
"copy-to-clipboard": "^3.3.3", "copy-to-clipboard": "^3.3.3",
"html2canvas": "^1.4.1", "html2canvas": "^1.4.1",
"nanoid": "^5.0.7",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-dropzone": "^14.2.3", "react-dropzone": "^14.2.3",
@ -46,29 +45,21 @@
"tailwind-merge": "^2.0.0", "tailwind-merge": "^2.0.0",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"thememirror": "^2.0.1", "thememirror": "^2.0.1",
"vite-plugin-checker": "^0.6.2", "vite-plugin-checker": "^0.6.2"
"webm-duration-fix": "^1.0.4",
"zustand": "^4.5.2"
}, },
"devDependencies": { "devDependencies": {
"@types/jest": "^29.5.12",
"@types/node": "^20.9.0", "@types/node": "^20.9.0",
"@types/puppeteer": "^7.0.4",
"@types/react": "^18.2.15", "@types/react": "^18.2.15",
"@types/react-dom": "^18.2.7", "@types/react-dom": "^18.2.7",
"@typescript-eslint/eslint-plugin": "^6.0.0", "@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0", "@typescript-eslint/parser": "^6.0.0",
"@vitejs/plugin-react": "^4.0.3", "@vitejs/plugin-react": "^4.0.3",
"autoprefixer": "^10.4.16", "autoprefixer": "^10.4.16",
"dotenv": "^16.4.5",
"eslint": "^8.45.0", "eslint": "^8.45.0",
"eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.3", "eslint-plugin-react-refresh": "^0.4.3",
"jest": "^29.7.0",
"postcss": "^8.4.31", "postcss": "^8.4.31",
"puppeteer": "^22.6.4",
"tailwindcss": "^3.3.5", "tailwindcss": "^3.3.5",
"ts-jest": "^29.1.2",
"typescript": "^5.0.2", "typescript": "^5.0.2",
"vite": "^4.4.5", "vite": "^4.4.5",
"vite-plugin-html": "^3.2.0", "vite-plugin-html": "^3.2.0",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

View File

@ -1,2 +0,0 @@
TEST_SCREENSHOTONE_API_KEY=
TEST_ROOT_PATH=

View File

@ -1,100 +1,86 @@
import { useEffect, useRef } from "react"; import { useEffect, useRef, useState } from "react";
import ImageUpload from "./components/ImageUpload";
import CodePreview from "./components/CodePreview";
import Preview from "./components/Preview";
import { generateCode } from "./generateCode"; import { generateCode } from "./generateCode";
import SettingsDialog from "./components/settings/SettingsDialog"; import Spinner from "./components/Spinner";
import classNames from "classnames";
import {
FaCode,
FaDesktop,
FaDownload,
FaMobile,
FaUndo,
} from "react-icons/fa";
import { Switch } from "./components/ui/switch";
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "./components/ui/tabs";
import SettingsDialog from "./components/SettingsDialog";
import { AppState, CodeGenerationParams, EditorTheme, Settings } from "./types"; import { AppState, CodeGenerationParams, EditorTheme, Settings } from "./types";
import { IS_RUNNING_ON_CLOUD } from "./config"; import { IS_RUNNING_ON_CLOUD } from "./config";
import { PicoBadge } from "./components/messages/PicoBadge"; import { PicoBadge } from "./components/PicoBadge";
import { OnboardingNote } from "./components/messages/OnboardingNote"; import { OnboardingNote } from "./components/OnboardingNote";
import { usePersistedState } from "./hooks/usePersistedState"; import { usePersistedState } from "./hooks/usePersistedState";
import { UrlInputSection } from "./components/UrlInputSection";
import TermsOfServiceDialog from "./components/TermsOfServiceDialog"; import TermsOfServiceDialog from "./components/TermsOfServiceDialog";
import html2canvas from "html2canvas";
import { USER_CLOSE_WEB_SOCKET_CODE } from "./constants"; import { USER_CLOSE_WEB_SOCKET_CODE } from "./constants";
import { extractHistory } from "./components/history/utils"; import CodeTab from "./components/CodeTab";
import OutputSettingsSection from "./components/OutputSettingsSection";
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 toast from "react-hot-toast";
import ImportCodeSection from "./components/ImportCodeSection";
import { Stack } from "./lib/stacks"; import { Stack } from "./lib/stacks";
import { CodeGenerationModel } from "./lib/models"; import { CodeGenerationModel } from "./lib/models";
import useBrowserTabIndicator from "./hooks/useBrowserTabIndicator"; import ModelSettingsSection from "./components/ModelSettingsSection";
import TipLink from "./components/messages/TipLink";
import { useAppStore } from "./store/app-store"; const IS_OPENAI_DOWN = false;
import { useProjectStore } from "./store/project-store";
import Sidebar from "./components/sidebar/Sidebar";
import PreviewPane from "./components/preview/PreviewPane";
import DeprecationMessage from "./components/messages/DeprecationMessage";
import { GenerationSettings } from "./components/settings/GenerationSettings";
import StartPane from "./components/start-pane/StartPane";
import { takeScreenshot } from "./lib/takeScreenshot";
import { Commit } from "./components/commits/types";
import { createCommit } from "./components/commits/utils";
function App() { function App() {
const { const [appState, setAppState] = useState<AppState>(AppState.INITIAL);
// Inputs const [generatedCode, setGeneratedCode] = useState<string>("");
inputMode,
setInputMode,
isImportedFromCode,
setIsImportedFromCode,
referenceImages,
setReferenceImages,
head, const [inputMode, setInputMode] = useState<"image" | "video">("image");
commits,
addCommit,
removeCommit,
setHead,
appendCommitCode,
setCommitCode,
resetCommits,
resetHead,
// Outputs const [referenceImages, setReferenceImages] = useState<string[]>([]);
appendExecutionConsole, const [executionConsole, setExecutionConsole] = useState<string[]>([]);
resetExecutionConsoles, const [updateInstruction, setUpdateInstruction] = useState("");
} = useProjectStore(); const [isImportedFromCode, setIsImportedFromCode] = useState<boolean>(false);
const {
disableInSelectAndEditMode,
setUpdateInstruction,
appState,
setAppState,
shouldIncludeResultImage,
setShouldIncludeResultImage,
} = useAppStore();
// Settings // Settings
const [settings, setSettings] = usePersistedState<Settings>( const [settings, setSettings] = usePersistedState<Settings>(
{ {
openAiApiKey: null, openAiApiKey: null,
openAiBaseURL: null, openAiBaseURL: null,
anthropicApiKey: null,
screenshotOneApiKey: null, screenshotOneApiKey: null,
isImageGenerationEnabled: true, isImageGenerationEnabled: true,
editorTheme: EditorTheme.COBALT, editorTheme: EditorTheme.COBALT,
generatedCodeConfig: Stack.HTML_TAILWIND, generatedCodeConfig: Stack.HTML_TAILWIND,
codeGenerationModel: CodeGenerationModel.CLAUDE_3_5_SONNET_2024_06_20, codeGenerationModel: CodeGenerationModel.GPT_4_VISION,
// Only relevant for hosted version // Only relevant for hosted version
isTermOfServiceAccepted: false, isTermOfServiceAccepted: false,
accessCode: null,
}, },
"setting" "setting"
); );
const wsRef = useRef<WebSocket>(null);
// Code generation model from local storage or the default value // Code generation model from local storage or the default value
const model = const selectedCodeGenerationModel =
settings.codeGenerationModel || CodeGenerationModel.GPT_4_VISION; settings.codeGenerationModel || CodeGenerationModel.GPT_4_VISION;
const showBetterModelMessage = // App history
model !== CodeGenerationModel.GPT_4O_2024_05_13 && const [appHistory, setAppHistory] = useState<History>([]);
model !== CodeGenerationModel.CLAUDE_3_5_SONNET_2024_06_20 && // Tracks the currently shown version from app history
appState === AppState.INITIAL; const [currentVersion, setCurrentVersion] = useState<number | null>(null);
const showSelectAndEditFeature = const [shouldIncludeResultImage, setShouldIncludeResultImage] =
(model === CodeGenerationModel.GPT_4O_2024_05_13 || useState<boolean>(false);
model === CodeGenerationModel.CLAUDE_3_5_SONNET_2024_06_20) &&
(settings.generatedCodeConfig === Stack.HTML_TAILWIND ||
settings.generatedCodeConfig === Stack.HTML_CSS);
// Indicate coding state using the browser tab's favicon and title const wsRef = useRef<WebSocket>(null);
useBrowserTabIndicator(appState === AppState.CODING);
// When the user already has the settings in local storage, newly added keys // 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 // do not get added to the settings so if it's falsy, we populate it with the default
@ -108,122 +94,128 @@ function App() {
} }
}, [settings.generatedCodeConfig, setSettings]); }, [settings.generatedCodeConfig, setSettings]);
// Functions const takeScreenshot = async (): Promise<string> => {
const iframeElement = document.querySelector(
"#preview-desktop"
) as HTMLIFrameElement;
if (!iframeElement?.contentWindow?.document.body) {
return "";
}
const canvas = await html2canvas(iframeElement.contentWindow.document.body);
const png = canvas.toDataURL("image/png");
return png;
};
const downloadCode = () => {
// Create a blob from the generated code
const blob = new Blob([generatedCode], { type: "text/html" });
const url = URL.createObjectURL(blob);
// Create an anchor element and set properties for download
const a = document.createElement("a");
a.href = url;
a.download = "index.html"; // Set the file name for download
document.body.appendChild(a); // Append to the document
a.click(); // Programmatically click the anchor to trigger download
// Clean up by removing the anchor and revoking the Blob URL
document.body.removeChild(a);
URL.revokeObjectURL(url);
};
const reset = () => { const reset = () => {
setAppState(AppState.INITIAL); setAppState(AppState.INITIAL);
setShouldIncludeResultImage(false); setGeneratedCode("");
setUpdateInstruction("");
disableInSelectAndEditMode();
resetExecutionConsoles();
resetCommits();
resetHead();
// Inputs
setInputMode("image");
setReferenceImages([]); setReferenceImages([]);
setExecutionConsole([]);
setUpdateInstruction("");
setIsImportedFromCode(false); setIsImportedFromCode(false);
setAppHistory([]);
setCurrentVersion(null);
setShouldIncludeResultImage(false);
}; };
const regenerate = () => {
if (head === null) {
toast.error(
"No current version set. Please contact support via chat or Github."
);
throw new Error("Regenerate called with no head");
}
// Retrieve the previous command
const currentCommit = commits[head];
if (currentCommit.type !== "ai_create") {
toast.error("Only the first version can be regenerated.");
return;
}
// Re-run the create
doCreate(referenceImages, inputMode);
};
// Used when the user cancels the code generation
const cancelCodeGeneration = () => { const cancelCodeGeneration = () => {
wsRef.current?.close?.(USER_CLOSE_WEB_SOCKET_CODE); wsRef.current?.close?.(USER_CLOSE_WEB_SOCKET_CODE);
// make sure stop can correct the state even if the websocket is already closed
cancelCodeGenerationAndReset();
}; };
// Used for code generation failure as well const shouldDisablePreview = inputMode === "video";
const cancelCodeGenerationAndReset = (commit: Commit) => { const previewCode =
// When the current commit is the first version, reset the entire app state shouldDisablePreview && appState === AppState.CODING ? "" : generatedCode;
if (commit.type === "ai_create") {
const cancelCodeGenerationAndReset = () => {
// When this is the first version, reset the entire app state
if (currentVersion === null) {
reset(); reset();
} else { } else {
// Otherwise, remove current commit from commits // Otherwise, revert to the last version
removeCommit(commit.hash); setGeneratedCode(appHistory[currentVersion].code);
// Revert to parent commit
const parentCommitHash = commit.parentHash;
if (parentCommitHash) {
setHead(parentCommitHash);
} else {
throw new Error("Parent commit not found");
}
setAppState(AppState.CODE_READY); setAppState(AppState.CODE_READY);
} }
}; };
function doGenerateCode(params: CodeGenerationParams) { function doGenerateCode(
// Reset the execution console params: CodeGenerationParams,
resetExecutionConsoles(); parentVersion: number | null
) {
// Set the app state setExecutionConsole([]);
setAppState(AppState.CODING); setAppState(AppState.CODING);
// Merge settings with params // Merge settings with params
const updatedParams = { ...params, ...settings }; const updatedParams = { ...params, ...settings };
const baseCommitObject = {
variants: [{ code: "" }, { code: "" }],
};
const commitInputObject =
params.generationType === "create"
? {
...baseCommitObject,
type: "ai_create" as const,
parentHash: null,
inputs: { image_url: referenceImages[0] },
}
: {
...baseCommitObject,
type: "ai_edit" as const,
parentHash: head,
inputs: {
prompt: params.history
? params.history[params.history.length - 1]
: "",
},
};
// Create a new commit and set it as the head
const commit = createCommit(commitInputObject);
addCommit(commit);
setHead(commit.hash);
generateCode( generateCode(
wsRef, wsRef,
updatedParams, updatedParams,
// On change // On change
(token, variantIndex) => { (token) => setGeneratedCode((prev) => prev + token),
appendCommitCode(commit.hash, variantIndex, token);
},
// On set code // On set code
(code, variantIndex) => { (code) => {
setCommitCode(commit.hash, variantIndex, code); setGeneratedCode(code);
if (params.generationType === "create") {
setAppHistory([
{
type: "ai_create",
parentIndex: null,
code,
inputs: { image_url: referenceImages[0] },
},
]);
setCurrentVersion(0);
} else {
setAppHistory((prev) => {
// Validate parent version
if (parentVersion === null) {
toast.error(
"No parent version set. Contact support or open a Github issue."
);
return prev;
}
const newHistory: History = [
...prev,
{
type: "ai_edit",
parentIndex: parentVersion,
code,
inputs: {
prompt: updateInstruction,
},
},
];
setCurrentVersion(newHistory.length - 1);
return newHistory;
});
}
}, },
// On status update // On status update
(line, variantIndex) => appendExecutionConsole(variantIndex, line), (line) => setExecutionConsole((prev) => [...prev, line]),
// On cancel // On cancel
() => { () => {
cancelCodeGenerationAndReset(commit); cancelCodeGenerationAndReset();
}, },
// On complete // On complete
() => { () => {
@ -237,71 +229,68 @@ function App() {
// Reset any existing state // Reset any existing state
reset(); reset();
// Set the input states
setReferenceImages(referenceImages); setReferenceImages(referenceImages);
setInputMode(inputMode); setInputMode(inputMode);
// Kick off the code generation
if (referenceImages.length > 0) { if (referenceImages.length > 0) {
doGenerateCode({ doGenerateCode(
{
generationType: "create", generationType: "create",
image: referenceImages[0], image: referenceImages[0],
inputMode, inputMode,
}); },
currentVersion
);
} }
} }
// Subsequent updates // Subsequent updates
async function doUpdate( async function doUpdate() {
updateInstruction: string, if (currentVersion === null) {
selectedElement?: HTMLElement
) {
if (updateInstruction.trim() === "") {
toast.error("Please include some instructions for AI on what to update.");
return;
}
if (head === null) {
toast.error( toast.error(
"No current version set. Contact support or open a Github issue." "No current version set. Contact support or open a Github issue."
); );
throw new Error("Update called with no head"); return;
} }
let historyTree; let historyTree;
try { try {
historyTree = extractHistory(head, commits); historyTree = extractHistoryTree(appHistory, currentVersion);
} catch { } catch {
toast.error( toast.error(
"Version history is invalid. This shouldn't happen. Please contact support or open a Github issue." "Version history is invalid. This shouldn't happen. Please contact support or open a Github issue."
); );
throw new Error("Invalid version history"); return;
} }
let modifiedUpdateInstruction = updateInstruction; const updatedHistory = [...historyTree, updateInstruction];
// Send in a reference to the selected element if it exists if (shouldIncludeResultImage) {
if (selectedElement) { const resultImage = await takeScreenshot();
modifiedUpdateInstruction = doGenerateCode(
updateInstruction + {
" referring to this element specifically: " +
selectedElement.outerHTML;
}
const updatedHistory = [...historyTree, modifiedUpdateInstruction];
const resultImage = shouldIncludeResultImage
? await takeScreenshot()
: undefined;
doGenerateCode({
generationType: "update", generationType: "update",
inputMode, inputMode,
image: referenceImages[0], image: referenceImages[0],
resultImage, resultImage: resultImage,
history: updatedHistory, history: updatedHistory,
isImportedFromCode, isImportedFromCode,
}); },
currentVersion
);
} else {
doGenerateCode(
{
generationType: "update",
inputMode,
image: referenceImages[0],
history: updatedHistory,
isImportedFromCode,
},
currentVersion
);
}
setGeneratedCode("");
setUpdateInstruction(""); setUpdateInstruction("");
} }
@ -319,30 +308,35 @@ function App() {
})); }));
} }
function setCodeGenerationModel(codeGenerationModel: CodeGenerationModel) {
setSettings((prev) => ({
...prev,
codeGenerationModel,
}));
}
function importFromCode(code: string, stack: Stack) { function importFromCode(code: string, stack: Stack) {
// Set input state
setIsImportedFromCode(true); setIsImportedFromCode(true);
// Set up this project // Set up this project
setGeneratedCode(code);
setStack(stack); setStack(stack);
setAppHistory([
// Create a new commit and set it as the head {
const commit = createCommit({
type: "code_create", type: "code_create",
parentHash: null, parentIndex: null,
variants: [{ code }], code,
inputs: null, inputs: { code },
}); },
addCommit(commit); ]);
setHead(commit.hash); setCurrentVersion(0);
// Set the app state
setAppState(AppState.CODE_READY); setAppState(AppState.CODE_READY);
} }
return ( return (
<div className="mt-2 dark:bg-black dark:text-white"> <div className="mt-2 dark:bg-black dark:text-white">
{IS_RUNNING_ON_CLOUD && <PicoBadge />} {IS_RUNNING_ON_CLOUD && <PicoBadge settings={settings} />}
{IS_RUNNING_ON_CLOUD && ( {IS_RUNNING_ON_CLOUD && (
<TermsOfServiceDialog <TermsOfServiceDialog
open={!settings.isTermOfServiceAccepted} open={!settings.isTermOfServiceAccepted}
@ -351,47 +345,204 @@ function App() {
)} )}
<div className="lg:fixed lg:inset-y-0 lg:z-40 lg:flex lg:w-96 lg:flex-col"> <div className="lg:fixed lg:inset-y-0 lg:z-40 lg:flex lg:w-96 lg:flex-col">
<div className="flex grow flex-col gap-y-2 overflow-y-auto border-r border-gray-200 bg-white px-6 dark:bg-zinc-950 dark:text-white"> <div className="flex grow flex-col gap-y-2 overflow-y-auto border-r border-gray-200 bg-white px-6 dark:bg-zinc-950 dark:text-white">
{/* Header with access to settings */}
<div className="flex items-center justify-between mt-10 mb-2"> <div className="flex items-center justify-between mt-10 mb-2">
<h1 className="text-2xl ">Screenshot to Code</h1> <h1 className="text-2xl ">Screenshot to Code</h1>
<SettingsDialog settings={settings} setSettings={setSettings} /> <SettingsDialog settings={settings} setSettings={setSettings} />
</div> </div>
{/* Generation settings like stack and model */} <OutputSettingsSection
<GenerationSettings settings={settings} setSettings={setSettings} /> stack={settings.generatedCodeConfig}
setStack={(config) => setStack(config)}
shouldDisableUpdates={
appState === AppState.CODING || appState === AppState.CODE_READY
}
/>
{/* Show auto updated message when older models are choosen */} <ModelSettingsSection
{showBetterModelMessage && <DeprecationMessage />} codeGenerationModel={selectedCodeGenerationModel}
setCodeGenerationModel={setCodeGenerationModel}
shouldDisableUpdates={
appState === AppState.CODING || appState === AppState.CODE_READY
}
/>
{/* Show tip link until coding is complete */} {IS_RUNNING_ON_CLOUD &&
{appState !== AppState.CODE_READY && <TipLink />} !(settings.openAiApiKey || settings.accessCode) && (
<OnboardingNote />
)}
{IS_RUNNING_ON_CLOUD && !settings.openAiApiKey && <OnboardingNote />} {IS_OPENAI_DOWN && (
<div className="bg-black text-white dark:bg-white dark:text-black p-3 rounded">
OpenAI API is currently down. Try back in 30 minutes or later. We
apologize for the inconvenience.
</div>
)}
{/* Rest of the sidebar when we're not in the initial state */}
{(appState === AppState.CODING || {(appState === AppState.CODING ||
appState === AppState.CODE_READY) && ( appState === AppState.CODE_READY) && (
<Sidebar <>
showSelectAndEditFeature={showSelectAndEditFeature} {/* Show code preview only when coding */}
doUpdate={doUpdate} {appState === AppState.CODING && (
regenerate={regenerate} <div className="flex flex-col">
cancelCodeGeneration={cancelCodeGeneration} <div className="flex items-center gap-x-1">
/> <Spinner />
{executionConsole.slice(-1)[0]}
</div>
<div className="flex mt-4 w-full">
<Button
onClick={cancelCodeGeneration}
className="w-full dark:text-white dark:bg-gray-700"
>
Cancel
</Button>
</div>
<CodePreview code={generatedCode} />
</div>
)} )}
{appState === AppState.CODE_READY && (
<div>
<div className="grid w-full gap-2">
<Textarea
placeholder="Tell the AI what to change..."
onChange={(e) => setUpdateInstruction(e.target.value)}
value={updateInstruction}
/>
<div className="flex justify-between items-center gap-x-2">
<div className="font-500 text-xs text-slate-700 dark:text-white">
Include screenshot of current version?
</div>
<Switch
checked={shouldIncludeResultImage}
onCheckedChange={setShouldIncludeResultImage}
className="dark:bg-gray-700"
/>
</div>
<Button
onClick={doUpdate}
className="dark:text-white dark:bg-gray-700"
>
Update
</Button>
</div>
<div className="flex items-center gap-x-2 mt-2">
<Button
onClick={downloadCode}
className="flex items-center gap-x-2 dark:text-white dark:bg-gray-700"
>
<FaDownload /> Download
</Button>
<Button
onClick={reset}
className="flex items-center gap-x-2 dark:text-white dark:bg-gray-700"
>
<FaUndo />
Reset
</Button>
</div>
</div>
)}
{/* Reference image display */}
<div className="flex gap-x-2 mt-2">
{referenceImages.length > 0 && (
<div className="flex flex-col">
<div
className={classNames({
"scanning relative": appState === AppState.CODING,
})}
>
<img
className="w-[340px] border border-gray-200 rounded-md"
src={referenceImages[0]}
alt="Reference"
/>
</div>
<div className="text-gray-400 uppercase text-sm text-center mt-1">
Original Screenshot
</div>
</div>
)}
<div className="bg-gray-400 px-4 py-2 rounded text-sm hidden">
<h2 className="text-lg mb-4 border-b border-gray-800">
Console
</h2>
{executionConsole.map((line, index) => (
<div
key={index}
className="border-b border-gray-400 mb-2 text-gray-600 font-mono"
>
{line}
</div>
))}
</div>
</div>
</>
)}
{
<HistoryDisplay
history={appHistory}
currentVersion={currentVersion}
revertToVersion={(index) => {
if (
index < 0 ||
index >= appHistory.length ||
!appHistory[index]
)
return;
setCurrentVersion(index);
setGeneratedCode(appHistory[index].code);
}}
shouldDisableReverts={appState === AppState.CODING}
/>
}
</div> </div>
</div> </div>
<main className="py-2 lg:pl-96"> <main className="py-2 lg:pl-96">
{appState === AppState.INITIAL && ( {appState === AppState.INITIAL && (
<StartPane <div className="flex flex-col justify-center items-center gap-y-10">
<ImageUpload setReferenceImages={doCreate} />
<UrlInputSection
doCreate={doCreate} doCreate={doCreate}
importFromCode={importFromCode} screenshotOneApiKey={settings.screenshotOneApiKey}
settings={settings}
/> />
<ImportCodeSection importFromCode={importFromCode} />
</div>
)} )}
{(appState === AppState.CODING || appState === AppState.CODE_READY) && ( {(appState === AppState.CODING || appState === AppState.CODE_READY) && (
<PreviewPane doUpdate={doUpdate} reset={reset} settings={settings} /> <div className="ml-4">
<Tabs defaultValue="desktop">
<div className="flex justify-end mr-8 mb-4">
<TabsList>
<TabsTrigger value="desktop" className="flex gap-x-2">
<FaDesktop /> Desktop
</TabsTrigger>
<TabsTrigger value="mobile" className="flex gap-x-2">
<FaMobile /> Mobile
</TabsTrigger>
<TabsTrigger value="code" className="flex gap-x-2">
<FaCode />
Code
</TabsTrigger>
</TabsList>
</div>
<TabsContent value="desktop">
<Preview code={previewCode} device="desktop" />
</TabsContent>
<TabsContent value="mobile">
<Preview code={previewCode} device="mobile" />
</TabsContent>
<TabsContent value="code">
<CodeTab
code={previewCode}
setCode={setGeneratedCode}
settings={settings}
/>
</TabsContent>
</Tabs>
</div>
)} )}
</main> </main>
</div> </div>

View File

@ -1,7 +1,7 @@
import { FaCopy } from "react-icons/fa"; import { FaCopy } from "react-icons/fa";
import CodeMirror from "./CodeMirror"; import CodeMirror from "./CodeMirror";
import { Button } from "../ui/button"; import { Button } from "./ui/button";
import { Settings } from "../../types"; import { Settings } from "../types";
import copy from "copy-to-clipboard"; import copy from "copy-to-clipboard";
import { useCallback } from "react"; import { useCallback } from "react";
import toast from "react-hot-toast"; import toast from "react-hot-toast";

View File

@ -3,10 +3,6 @@ import { useState, useEffect, useMemo } from "react";
import { useDropzone } from "react-dropzone"; import { useDropzone } from "react-dropzone";
// import { PromptImage } from "../../../types"; // import { PromptImage } from "../../../types";
import { toast } from "react-hot-toast"; import { toast } from "react-hot-toast";
import { URLS } from "../urls";
import { Badge } from "./ui/badge";
import ScreenRecorder from "./recording/ScreenRecorder";
import { ScreenRecorderState } from "../types";
const baseStyle = { const baseStyle = {
flex: 1, flex: 1,
@ -63,23 +59,16 @@ interface Props {
function ImageUpload({ setReferenceImages }: Props) { function ImageUpload({ setReferenceImages }: Props) {
const [files, setFiles] = useState<FileWithPreview[]>([]); const [files, setFiles] = useState<FileWithPreview[]>([]);
// TODO: Switch to Zustand
const [screenRecorderState, setScreenRecorderState] =
useState<ScreenRecorderState>(ScreenRecorderState.INITIAL);
const { getRootProps, getInputProps, isFocused, isDragAccept, isDragReject } = const { getRootProps, getInputProps, isFocused, isDragAccept, isDragReject } =
useDropzone({ useDropzone({
maxFiles: 1, maxFiles: 1,
maxSize: 1024 * 1024 * 20, // 20 MB maxSize: 1024 * 1024 * 20, // 20 MB
accept: { accept: {
// Image formats
"image/png": [".png"], "image/png": [".png"],
"image/jpeg": [".jpeg"], "image/jpeg": [".jpeg"],
"image/jpg": [".jpg"], "image/jpg": [".jpg"],
// Video formats
"video/quicktime": [".mov"], "video/quicktime": [".mov"],
"video/mp4": [".mp4"], "video/mp4": [".mp4"],
"video/webm": [".webm"],
}, },
onDrop: (acceptedFiles) => { onDrop: (acceptedFiles) => {
// Set up the preview thumbnail images // Set up the preview thumbnail images
@ -163,34 +152,15 @@ function ImageUpload({ setReferenceImages }: Props) {
return ( return (
<section className="container"> <section className="container">
{screenRecorderState === ScreenRecorderState.INITIAL && ( {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
<div {...getRootProps({ style: style as any })}> <div {...getRootProps({ style: style as any })}>
<input {...getInputProps()} className="file-input" /> <input {...getInputProps()} />
<p className="text-slate-700 text-lg"> <p className="text-slate-700 text-lg">
Drag & drop a screenshot here, <br /> Drag & drop a screenshot here, <br />
or paste from clipboard, <br />
or click to upload or click to upload
</p> </p>
</div> </div>
)}
{screenRecorderState === ScreenRecorderState.INITIAL && (
<div className="text-center text-sm text-slate-800 mt-4">
<Badge>New!</Badge> Upload a screen recording (.mp4, .mov) or record
your screen to clone a whole app (experimental).{" "}
<a
className="underline"
href={URLS["intro-to-video"]}
target="_blank"
>
Learn more.
</a>
</div>
)}
<ScreenRecorder
screenRecorderState={screenRecorderState}
setScreenRecorderState={setScreenRecorderState}
generateCode={setReferenceImages}
/>
</section> </section>
); );
} }

View File

@ -10,7 +10,7 @@ import {
DialogTrigger, DialogTrigger,
} from "./ui/dialog"; } from "./ui/dialog";
import { Textarea } from "./ui/textarea"; import { Textarea } from "./ui/textarea";
import OutputSettingsSection from "./settings/OutputSettingsSection"; import OutputSettingsSection from "./OutputSettingsSection";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import { Stack } from "../lib/stacks"; import { Stack } from "../lib/stacks";
@ -38,9 +38,7 @@ function ImportCodeSection({ importFromCode }: Props) {
return ( return (
<Dialog> <Dialog>
<DialogTrigger asChild> <DialogTrigger asChild>
<Button className="import-from-code-btn" variant="secondary"> <Button variant="secondary">Import from Code</Button>
Import from Code
</Button>
</DialogTrigger> </DialogTrigger>
<DialogContent className="sm:max-w-[425px]"> <DialogContent className="sm:max-w-[425px]">
<DialogHeader> <DialogHeader>
@ -64,7 +62,7 @@ function ImportCodeSection({ importFromCode }: Props) {
/> />
<DialogFooter> <DialogFooter>
<Button className="import-btn" type="submit" onClick={doImport}> <Button type="submit" onClick={doImport}>
Import Import
</Button> </Button>
</DialogFooter> </DialogFooter>

View File

@ -0,0 +1,65 @@
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
} from "./ui/select";
import {
CODE_GENERATION_MODEL_DESCRIPTIONS,
CodeGenerationModel,
} from "../lib/models";
import { Badge } from "./ui/badge";
interface Props {
codeGenerationModel: CodeGenerationModel;
setCodeGenerationModel: (codeGenerationModel: CodeGenerationModel) => void;
shouldDisableUpdates?: boolean;
}
function ModelSettingsSection({
codeGenerationModel,
setCodeGenerationModel,
shouldDisableUpdates = false,
}: Props) {
return (
<div className="flex flex-col gap-y-2 justify-between text-sm">
<div className="grid grid-cols-3 items-center gap-4">
<span>Model:</span>
<Select
value={codeGenerationModel}
onValueChange={(value: string) =>
setCodeGenerationModel(value as CodeGenerationModel)
}
disabled={shouldDisableUpdates}
>
<SelectTrigger className="col-span-2" id="output-settings-js">
<span className="font-semibold">
{CODE_GENERATION_MODEL_DESCRIPTIONS[codeGenerationModel].name}
</span>
</SelectTrigger>
<SelectContent>
<SelectGroup>
{Object.values(CodeGenerationModel).map((model) => (
<SelectItem key={model} value={model}>
<div className="flex items-center">
<span className="font-semibold">
{CODE_GENERATION_MODEL_DESCRIPTIONS[model].name}
</span>
{CODE_GENERATION_MODEL_DESCRIPTIONS[model].inBeta && (
<Badge className="ml-2" variant="secondary">
Beta
</Badge>
)}
</div>
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
</div>
</div>
);
}
export default ModelSettingsSection;

View File

@ -5,9 +5,9 @@ import {
SelectGroup, SelectGroup,
SelectItem, SelectItem,
SelectTrigger, SelectTrigger,
} from "../ui/select"; } from "./ui/select";
import { Badge } from "../ui/badge"; import { Badge } from "./ui/badge";
import { Stack, STACK_DESCRIPTIONS } from "../../lib/stacks"; import { Stack, STACK_DESCRIPTIONS } from "../lib/stacks";
function generateDisplayComponent(stack: Stack) { function generateDisplayComponent(stack: Stack) {
const stackComponents = STACK_DESCRIPTIONS[stack].components; const stackComponents = STACK_DESCRIPTIONS[stack].components;

View File

@ -0,0 +1,39 @@
import { Settings } from "../types";
export function PicoBadge({ settings }: { settings: Settings }) {
return (
<>
<a
href="https://screenshot-to-code.canny.io/feature-requests"
target="_blank"
>
<div
className="fixed z-50 bottom-16 right-5 rounded-md shadow bg-black
text-white px-4 text-xs py-3 cursor-pointer"
>
feature requests?
</div>
</a>
{!settings.accessCode && (
<a href="https://picoapps.xyz?ref=screenshot-to-code" target="_blank">
<div
className="fixed z-50 bottom-5 right-5 rounded-md shadow text-black
bg-white px-4 text-xs py-3 cursor-pointer"
>
an open source project by Pico
</div>
</a>
)}
{settings.accessCode && (
<a href="mailto:support@picoapps.xyz" target="_blank">
<div
className="fixed z-50 bottom-5 right-5 rounded-md shadow text-black
bg-white px-4 text-xs py-3 cursor-pointer"
>
email support
</div>
</a>
)}
</>
);
}

View File

@ -0,0 +1,44 @@
import { useEffect, useRef } from "react";
import classNames from "classnames";
// import useThrottle from "../hooks/useThrottle";
interface Props {
code: string;
device: "mobile" | "desktop";
}
function Preview({ code, device }: Props) {
const throttledCode = code;
// Temporary disable throttling for the preview not updating when the code changes
// useThrottle(code, 200);
const iframeRef = useRef<HTMLIFrameElement | null>(null);
useEffect(() => {
const iframe = iframeRef.current;
if (iframe && iframe.contentDocument) {
iframe.contentDocument.open();
iframe.contentDocument.write(throttledCode);
iframe.contentDocument.close();
}
}, [throttledCode]);
return (
<div className="flex justify-center mx-2">
<iframe
id={`preview-${device}`}
ref={iframeRef}
title="Preview"
className={classNames(
"border-[4px] border-black rounded-[20px] shadow-lg",
"transform scale-[0.9] origin-top",
{
"w-full h-[832px]": device === "desktop",
"w-[400px] h-[832px]": device === "mobile",
}
)}
></iframe>
</div>
);
}
export default Preview;

View File

@ -9,19 +9,20 @@ import {
DialogTrigger, DialogTrigger,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { FaCog } from "react-icons/fa"; import { FaCog } from "react-icons/fa";
import { EditorTheme, Settings } from "../../types"; import { EditorTheme, Settings } from "../types";
import { Switch } from "../ui/switch"; import { Switch } from "./ui/switch";
import { Label } from "../ui/label"; import { Label } from "./ui/label";
import { Input } from "../ui/input"; import { Input } from "./ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger } from "../ui/select"; import { Select, SelectContent, SelectItem, SelectTrigger } from "./ui/select";
import { capitalize } from "../../lib/utils"; import { capitalize } from "../lib/utils";
import { IS_RUNNING_ON_CLOUD } from "../../config"; import { IS_RUNNING_ON_CLOUD } from "../config";
import { import {
Accordion, Accordion,
AccordionContent, AccordionContent,
AccordionItem, AccordionItem,
AccordionTrigger, AccordionTrigger,
} from "../ui/accordion"; } from "./ui/accordion";
import AccessCodeSection from "./settings/AccessCodeSection";
interface Props { interface Props {
settings: Settings; settings: Settings;
@ -46,10 +47,15 @@ function SettingsDialog({ settings, setSettings }: Props) {
<DialogTitle className="mb-4">Settings</DialogTitle> <DialogTitle className="mb-4">Settings</DialogTitle>
</DialogHeader> </DialogHeader>
{/* Access code */}
{IS_RUNNING_ON_CLOUD && (
<AccessCodeSection settings={settings} setSettings={setSettings} />
)}
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<Label htmlFor="image-generation"> <Label htmlFor="image-generation">
<div>DALL-E Placeholder Image Generation</div> <div>DALL-E Placeholder Image Generation</div>
<div className="font-light mt-2 text-xs"> <div className="font-light mt-2">
More fun with it but if you want to save money, turn it off. More fun with it but if you want to save money, turn it off.
</div> </div>
</Label> </Label>
@ -64,11 +70,10 @@ function SettingsDialog({ settings, setSettings }: Props) {
} }
/> />
</div> </div>
<div className="flex flex-col space-y-6"> <div className="flex flex-col space-y-4">
<div>
<Label htmlFor="openai-api-key"> <Label htmlFor="openai-api-key">
<div>OpenAI API key</div> <div>OpenAI API key</div>
<div className="font-light mt-1 mb-2 text-xs leading-relaxed"> <div className="font-light mt-2 leading-relaxed">
Only stored in your browser. Never stored on servers. Overrides Only stored in your browser. Never stored on servers. Overrides
your .env config. your .env config.
</div> </div>
@ -85,10 +90,9 @@ function SettingsDialog({ settings, setSettings }: Props) {
})) }))
} }
/> />
</div>
{!IS_RUNNING_ON_CLOUD && ( {!IS_RUNNING_ON_CLOUD && (
<div> <>
<Label htmlFor="openai-api-key"> <Label htmlFor="openai-api-key">
<div>OpenAI Base URL (optional)</div> <div>OpenAI Base URL (optional)</div>
<div className="font-light mt-2 leading-relaxed"> <div className="font-light mt-2 leading-relaxed">
@ -107,31 +111,9 @@ function SettingsDialog({ settings, setSettings }: Props) {
})) }))
} }
/> />
</div> </>
)} )}
<div>
<Label htmlFor="anthropic-api-key">
<div>Anthropic API key</div>
<div className="font-light mt-1 text-xs leading-relaxed">
Only stored in your browser. Never stored on servers. Overrides
your .env config.
</div>
</Label>
<Input
id="anthropic-api-key"
placeholder="Anthropic API key"
value={settings.anthropicApiKey || ""}
onChange={(e) =>
setSettings((s) => ({
...s,
anthropicApiKey: e.target.value,
}))
}
/>
</div>
<Accordion type="single" collapsible className="w-full"> <Accordion type="single" collapsible className="w-full">
<AccordionItem value="item-1"> <AccordionItem value="item-1">
<AccordionTrigger>Screenshot by URL Config</AccordionTrigger> <AccordionTrigger>Screenshot by URL Config</AccordionTrigger>

View File

@ -69,7 +69,7 @@ export function UrlInputSection({ doCreate, screenshotOneApiKey }: Props) {
<Button <Button
onClick={takeScreenshot} onClick={takeScreenshot}
disabled={isLoading} disabled={isLoading}
className="bg-slate-400 capture-btn" className="bg-slate-400"
> >
{isLoading ? "Capturing..." : "Capture"} {isLoading ? "Capturing..." : "Capture"}
</Button> </Button>

View File

@ -1,37 +0,0 @@
export type CommitHash = string;
export type Variant = {
code: string;
};
export type BaseCommit = {
hash: CommitHash;
parentHash: CommitHash | null;
dateCreated: Date;
isCommitted: boolean;
variants: Variant[];
selectedVariantIndex: number;
};
export type CommitType = "ai_create" | "ai_edit" | "code_create";
export type AiCreateCommit = BaseCommit & {
type: "ai_create";
inputs: {
image_url: string;
};
};
export type AiEditCommit = BaseCommit & {
type: "ai_edit";
inputs: {
prompt: string;
};
};
export type CodeCreateCommit = BaseCommit & {
type: "code_create";
inputs: null;
};
export type Commit = AiCreateCommit | AiEditCommit | CodeCreateCommit;

View File

@ -1,32 +0,0 @@
import { nanoid } from "nanoid";
import {
AiCreateCommit,
AiEditCommit,
CodeCreateCommit,
Commit,
} from "./types";
export function createCommit(
commit:
| Omit<
AiCreateCommit,
"hash" | "dateCreated" | "selectedVariantIndex" | "isCommitted"
>
| Omit<
AiEditCommit,
"hash" | "dateCreated" | "selectedVariantIndex" | "isCommitted"
>
| Omit<
CodeCreateCommit,
"hash" | "dateCreated" | "selectedVariantIndex" | "isCommitted"
>
): Commit {
const hash = nanoid();
return {
...commit,
hash,
isCommitted: false,
dateCreated: new Date(),
selectedVariantIndex: 0,
};
}

View File

@ -1,25 +0,0 @@
import React from "react";
import { BsArrowReturnLeft } from "react-icons/bs";
interface KeyboardShortcutBadgeProps {
letter: string;
}
const KeyboardShortcutBadge: React.FC<KeyboardShortcutBadgeProps> = ({
letter,
}) => {
const icon =
letter.toLowerCase() === "enter" || letter.toLowerCase() === "return" ? (
<BsArrowReturnLeft />
) : (
letter.toUpperCase()
);
return (
<span className="font-mono text-xs ml-2 rounded bg-gray-700 dark:bg-gray-900 text-white py-[2px] px-2">
{icon}
</span>
);
};
export default KeyboardShortcutBadge;

View File

@ -4,7 +4,7 @@ import RatingPicker from "./RatingPicker";
interface Eval { interface Eval {
input: string; input: string;
outputs: string[]; output: string;
} }
function EvalsPage() { function EvalsPage() {
@ -38,22 +38,18 @@ function EvalsPage() {
<div className="flex flex-col gap-y-4 mt-4 mx-auto justify-center"> <div className="flex flex-col gap-y-4 mt-4 mx-auto justify-center">
{evals.map((e, index) => ( {evals.map((e, index) => (
<div className="flex flex-col justify-center" key={index}> <div className="flex flex-col justify-center" key={index}>
<h2 className="font-bold text-lg ml-4">{index}</h2> <div className="flex gap-x-2 justify-center">
<div className="flex gap-x-2 justify-center ml-4">
{/* Update w if N changes to a fixed number like w-[600px] */}
<div className="w-1/2 p-1 border"> <div className="w-1/2 p-1 border">
<img src={e.input} alt={`Input for eval ${index}`} /> <img src={e.input} />
</div> </div>
{e.outputs.map((output, outputIndex) => ( <div className="w-1/2 p-1 border">
<div className="w-1/2 p-1 border" key={outputIndex}>
{/* Put output into an iframe */} {/* Put output into an iframe */}
<iframe <iframe
srcDoc={output} srcDoc={e.output}
className="w-[1200px] h-[800px] transform scale-[0.60]" className="w-[1200px] h-[800px] transform scale-[0.60]"
style={{ transformOrigin: "top left" }} style={{ transformOrigin: "top left" }}
></iframe> ></iframe>
</div> </div>
))}
</div> </div>
<div className="ml-8 mt-4 flex justify-center"> <div className="ml-8 mt-4 flex justify-center">
<RatingPicker <RatingPicker

View File

@ -1,3 +1,4 @@
import { History } from "./history_types";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import classNames from "classnames"; import classNames from "classnames";
@ -10,23 +11,21 @@ import {
} from "../ui/collapsible"; } from "../ui/collapsible";
import { Button } from "../ui/button"; import { Button } from "../ui/button";
import { CaretSortIcon } from "@radix-ui/react-icons"; import { CaretSortIcon } from "@radix-ui/react-icons";
import { useProjectStore } from "../../store/project-store";
interface Props { interface Props {
history: History;
currentVersion: number | null;
revertToVersion: (version: number) => void;
shouldDisableReverts: boolean; shouldDisableReverts: boolean;
} }
export default function HistoryDisplay({ shouldDisableReverts }: Props) { export default function HistoryDisplay({
const { commits, head, setHead } = useProjectStore(); history,
currentVersion,
// Put all commits into an array and sort by created date (oldest first) revertToVersion,
const flatHistory = Object.values(commits).sort( shouldDisableReverts,
(a, b) => }: Props) {
new Date(a.dateCreated).getTime() - new Date(b.dateCreated).getTime() const renderedHistory = renderHistory(history, currentVersion);
);
// Annotate history items with a summary, parent version, etc.
const renderedHistory = renderHistory(flatHistory);
return renderedHistory.length === 0 ? null : ( return renderedHistory.length === 0 ? null : (
<div className="flex flex-col h-screen"> <div className="flex flex-col h-screen">
@ -40,8 +39,8 @@ export default function HistoryDisplay({ shouldDisableReverts }: Props) {
"flex items-center justify-between space-x-2 w-full pr-2", "flex items-center justify-between space-x-2 w-full pr-2",
"border-b cursor-pointer", "border-b cursor-pointer",
{ {
" hover:bg-black hover:text-white": item.hash === head, " hover:bg-black hover:text-white": !item.isActive,
"bg-slate-500 text-white": item.hash === head, "bg-slate-500 text-white": item.isActive,
} }
)} )}
> >
@ -52,14 +51,14 @@ export default function HistoryDisplay({ shouldDisableReverts }: Props) {
? toast.error( ? toast.error(
"Please wait for code generation to complete before viewing an older version." "Please wait for code generation to complete before viewing an older version."
) )
: setHead(item.hash) : revertToVersion(index)
} }
> >
<div className="flex gap-x-1 truncate"> <div className="flex gap-x-1 truncate">
<h2 className="text-sm truncate">{item.summary}</h2> <h2 className="text-sm truncate">{item.summary}</h2>
{item.parentVersion !== null && ( {item.parentVersion !== null && (
<h2 className="text-sm"> <h2 className="text-sm">
(parent: v{item.parentVersion}) (parent: {item.parentVersion})
</h2> </h2>
)} )}
</div> </div>

View File

@ -0,0 +1,41 @@
export type HistoryItemType = "ai_create" | "ai_edit" | "code_create";
type CommonHistoryItem = {
parentIndex: null | number;
code: string;
};
export type HistoryItem =
| ({
type: "ai_create";
inputs: AiCreateInputs;
} & CommonHistoryItem)
| ({
type: "ai_edit";
inputs: AiEditInputs;
} & CommonHistoryItem)
| ({
type: "code_create";
inputs: CodeCreateInputs;
} & CommonHistoryItem);
export type AiCreateInputs = {
image_url: string;
};
export type AiEditInputs = {
prompt: string;
};
export type CodeCreateInputs = {
code: string;
};
export type History = HistoryItem[];
export type RenderedHistoryItem = {
type: string;
summary: string;
parentVersion: string | null;
isActive: boolean;
};

View File

@ -1,125 +1,91 @@
import { extractHistory, renderHistory } from "./utils"; import { expect, test } from "vitest";
import { Commit, CommitHash } from "../commits/types"; import { extractHistoryTree, renderHistory } from "./utils";
import type { History } from "./history_types";
const basicLinearHistory: Record<CommitHash, Commit> = { const basicLinearHistory: History = [
"0": { {
hash: "0",
dateCreated: new Date(),
isCommitted: false,
type: "ai_create", type: "ai_create",
parentHash: null, parentIndex: null,
variants: [{ code: "<html>1. create</html>" }], code: "<html>1. create</html>",
selectedVariantIndex: 0,
inputs: { inputs: {
image_url: "", image_url: "",
}, },
}, },
"1": { {
hash: "1",
dateCreated: new Date(),
isCommitted: false,
type: "ai_edit", type: "ai_edit",
parentHash: "0", parentIndex: 0,
variants: [{ code: "<html>2. edit with better icons</html>" }], code: "<html>2. edit with better icons</html>",
selectedVariantIndex: 0,
inputs: { inputs: {
prompt: "use better icons", prompt: "use better icons",
}, },
}, },
"2": { {
hash: "2",
dateCreated: new Date(),
isCommitted: false,
type: "ai_edit", type: "ai_edit",
parentHash: "1", parentIndex: 1,
variants: [{ code: "<html>3. edit with better icons and red text</html>" }], code: "<html>3. edit with better icons and red text</html>",
selectedVariantIndex: 0,
inputs: { inputs: {
prompt: "make text red", prompt: "make text red",
}, },
}, },
}; ];
const basicLinearHistoryWithCode: Record<CommitHash, Commit> = { const basicLinearHistoryWithCode: History = [
"0": { {
hash: "0",
dateCreated: new Date(),
isCommitted: false,
type: "code_create", type: "code_create",
parentHash: null, parentIndex: null,
variants: [{ code: "<html>1. create</html>" }], code: "<html>1. create</html>",
selectedVariantIndex: 0, inputs: {
inputs: null, code: "<html>1. create</html>",
}, },
...Object.fromEntries(Object.entries(basicLinearHistory).slice(1)), },
}; ...basicLinearHistory.slice(1),
];
const basicBranchingHistory: Record<CommitHash, Commit> = { const basicBranchingHistory: History = [
...basicLinearHistory, ...basicLinearHistory,
"3": { {
hash: "3",
dateCreated: new Date(),
isCommitted: false,
type: "ai_edit", type: "ai_edit",
parentHash: "1", parentIndex: 1,
variants: [ code: "<html>4. edit with better icons and green text</html>",
{ code: "<html>4. edit with better icons and green text</html>" },
],
selectedVariantIndex: 0,
inputs: { inputs: {
prompt: "make text green", prompt: "make text green",
}, },
}, },
}; ];
const longerBranchingHistory: Record<CommitHash, Commit> = { const longerBranchingHistory: History = [
...basicBranchingHistory, ...basicBranchingHistory,
"4": { {
hash: "4",
dateCreated: new Date(),
isCommitted: false,
type: "ai_edit", type: "ai_edit",
parentHash: "3", parentIndex: 3,
variants: [ code: "<html>5. edit with better icons and green, bold text</html>",
{ code: "<html>5. edit with better icons and green, bold text</html>" },
],
selectedVariantIndex: 0,
inputs: { inputs: {
prompt: "make text bold", prompt: "make text bold",
}, },
}, },
}; ];
const basicBadHistory: Record<CommitHash, Commit> = { const basicBadHistory: History = [
"0": { {
hash: "0",
dateCreated: new Date(),
isCommitted: false,
type: "ai_create", type: "ai_create",
parentHash: null, parentIndex: null,
variants: [{ code: "<html>1. create</html>" }], code: "<html>1. create</html>",
selectedVariantIndex: 0,
inputs: { inputs: {
image_url: "", image_url: "",
}, },
}, },
"1": { {
hash: "1",
dateCreated: new Date(),
isCommitted: false,
type: "ai_edit", type: "ai_edit",
parentHash: "2", // <- Bad parent hash parentIndex: 2, // <- Bad parent index
variants: [{ code: "<html>2. edit with better icons</html>" }], code: "<html>2. edit with better icons</html>",
selectedVariantIndex: 0,
inputs: { inputs: {
prompt: "use better icons", prompt: "use better icons",
}, },
}, },
}; ];
describe("History Utils", () => { test("should correctly extract the history tree", () => {
test("should correctly extract the history tree", () => { expect(extractHistoryTree(basicLinearHistory, 2)).toEqual([
expect(extractHistory("2", basicLinearHistory)).toEqual([
"<html>1. create</html>", "<html>1. create</html>",
"use better icons", "use better icons",
"<html>2. edit with better icons</html>", "<html>2. edit with better icons</html>",
@ -127,12 +93,12 @@ describe("History Utils", () => {
"<html>3. edit with better icons and red text</html>", "<html>3. edit with better icons and red text</html>",
]); ]);
expect(extractHistory("0", basicLinearHistory)).toEqual([ expect(extractHistoryTree(basicLinearHistory, 0)).toEqual([
"<html>1. create</html>", "<html>1. create</html>",
]); ]);
// Test branching // Test branching
expect(extractHistory("3", basicBranchingHistory)).toEqual([ expect(extractHistoryTree(basicBranchingHistory, 3)).toEqual([
"<html>1. create</html>", "<html>1. create</html>",
"use better icons", "use better icons",
"<html>2. edit with better icons</html>", "<html>2. edit with better icons</html>",
@ -140,7 +106,7 @@ describe("History Utils", () => {
"<html>4. edit with better icons and green text</html>", "<html>4. edit with better icons and green text</html>",
]); ]);
expect(extractHistory("4", longerBranchingHistory)).toEqual([ expect(extractHistoryTree(longerBranchingHistory, 4)).toEqual([
"<html>1. create</html>", "<html>1. create</html>",
"use better icons", "use better icons",
"<html>2. edit with better icons</html>", "<html>2. edit with better icons</html>",
@ -150,7 +116,7 @@ describe("History Utils", () => {
"<html>5. edit with better icons and green, bold text</html>", "<html>5. edit with better icons and green, bold text</html>",
]); ]);
expect(extractHistory("2", longerBranchingHistory)).toEqual([ expect(extractHistoryTree(longerBranchingHistory, 2)).toEqual([
"<html>1. create</html>", "<html>1. create</html>",
"use better icons", "use better icons",
"<html>2. edit with better icons</html>", "<html>2. edit with better icons</html>",
@ -160,83 +126,105 @@ describe("History Utils", () => {
// Errors // Errors
// Bad hash // Bad index
expect(() => extractHistory("100", basicLinearHistory)).toThrow(); expect(() => extractHistoryTree(basicLinearHistory, 100)).toThrow();
expect(() => extractHistoryTree(basicLinearHistory, -2)).toThrow();
// Bad tree // Bad tree
expect(() => extractHistory("1", basicBadHistory)).toThrow(); expect(() => extractHistoryTree(basicBadHistory, 1)).toThrow();
}); });
test("should correctly render the history tree", () => { test("should correctly render the history tree", () => {
expect(renderHistory(Object.values(basicLinearHistory))).toEqual([ expect(renderHistory(basicLinearHistory, 2)).toEqual([
{ {
...basicLinearHistory["0"], isActive: false,
type: "Create", parentVersion: null,
summary: "Create", summary: "Create",
parentVersion: null, type: "Create",
}, },
{ {
...basicLinearHistory["1"], isActive: false,
type: "Edit", parentVersion: null,
summary: "use better icons", summary: "use better icons",
parentVersion: null, type: "Edit",
}, },
{ {
...basicLinearHistory["2"], isActive: true,
type: "Edit",
summary: "make text red",
parentVersion: null, parentVersion: null,
summary: "make text red",
type: "Edit",
},
]);
// Current version is the first version
expect(renderHistory(basicLinearHistory, 0)).toEqual([
{
isActive: true,
parentVersion: null,
summary: "Create",
type: "Create",
},
{
isActive: false,
parentVersion: null,
summary: "use better icons",
type: "Edit",
},
{
isActive: false,
parentVersion: null,
summary: "make text red",
type: "Edit",
}, },
]); ]);
// Render a history with code // Render a history with code
expect(renderHistory(Object.values(basicLinearHistoryWithCode))).toEqual([ expect(renderHistory(basicLinearHistoryWithCode, 0)).toEqual([
{ {
...basicLinearHistoryWithCode["0"], isActive: true,
type: "Imported from code", parentVersion: null,
summary: "Imported from code", summary: "Imported from code",
parentVersion: null, type: "Imported from code",
}, },
{ {
...basicLinearHistoryWithCode["1"], isActive: false,
type: "Edit", parentVersion: null,
summary: "use better icons", summary: "use better icons",
parentVersion: null, type: "Edit",
}, },
{ {
...basicLinearHistoryWithCode["2"], isActive: false,
type: "Edit",
summary: "make text red",
parentVersion: null, parentVersion: null,
summary: "make text red",
type: "Edit",
}, },
]); ]);
// Render a non-linear history // Render a non-linear history
expect(renderHistory(Object.values(basicBranchingHistory))).toEqual([ expect(renderHistory(basicBranchingHistory, 3)).toEqual([
{ {
...basicBranchingHistory["0"], isActive: false,
type: "Create", parentVersion: null,
summary: "Create", summary: "Create",
parentVersion: null, type: "Create",
}, },
{ {
...basicBranchingHistory["1"], isActive: false,
type: "Edit", parentVersion: null,
summary: "use better icons", summary: "use better icons",
parentVersion: null, type: "Edit",
}, },
{ {
...basicBranchingHistory["2"], isActive: false,
type: "Edit", parentVersion: null,
summary: "make text red", summary: "make text red",
parentVersion: null, type: "Edit",
}, },
{ {
...basicBranchingHistory["3"], isActive: true,
type: "Edit", parentVersion: "v2",
summary: "make text green", summary: "make text green",
parentVersion: 2, type: "Edit",
}, },
]); ]);
});
}); });

View File

@ -1,25 +1,33 @@
import { Commit, CommitHash, CommitType } from "../commits/types"; import {
History,
HistoryItem,
HistoryItemType,
RenderedHistoryItem,
} from "./history_types";
export function extractHistory( export function extractHistoryTree(
hash: CommitHash, history: History,
commits: Record<CommitHash, Commit> version: number
): string[] { ): string[] {
const flatHistory: string[] = []; const flatHistory: string[] = [];
let currentCommitHash: CommitHash | null = hash; let currentIndex: number | null = version;
while (currentCommitHash !== null) { while (currentIndex !== null) {
const commit: Commit | null = commits[currentCommitHash]; const item: HistoryItem = history[currentIndex];
if (commit) { if (item) {
flatHistory.unshift(commit.variants[commit.selectedVariantIndex].code); if (item.type === "ai_create") {
// Don't include the image for ai_create
// For edits, add the prompt to the history flatHistory.unshift(item.code);
if (commit.type === "ai_edit") { } else if (item.type === "ai_edit") {
flatHistory.unshift(commit.inputs.prompt); flatHistory.unshift(item.code);
flatHistory.unshift(item.inputs.prompt);
} else if (item.type === "code_create") {
flatHistory.unshift(item.code);
} }
// Move to the parent of the current item // Move to the parent of the current item
currentCommitHash = commit.parentHash; currentIndex = item.parentIndex;
} else { } else {
throw new Error("Malformed history: missing parent index"); throw new Error("Malformed history: missing parent index");
} }
@ -28,7 +36,7 @@ export function extractHistory(
return flatHistory; return flatHistory;
} }
function displayHistoryItemType(itemType: CommitType) { function displayHistoryItemType(itemType: HistoryItemType) {
switch (itemType) { switch (itemType) {
case "ai_create": case "ai_create":
return "Create"; return "Create";
@ -43,48 +51,44 @@ function displayHistoryItemType(itemType: CommitType) {
} }
} }
const setParentVersion = (commit: Commit, history: Commit[]) => { function summarizeHistoryItem(item: HistoryItem) {
// If the commit has no parent, return null const itemType = item.type;
if (!commit.parentHash) return null; switch (itemType) {
const parentIndex = history.findIndex(
(item) => item.hash === commit.parentHash
);
const currentIndex = history.findIndex((item) => item.hash === commit.hash);
// Only set parent version if the parent is not the previous commit
// and parent exists
return parentIndex !== -1 && parentIndex != currentIndex - 1
? parentIndex + 1
: null;
};
export function summarizeHistoryItem(commit: Commit) {
const commitType = commit.type;
switch (commitType) {
case "ai_create": case "ai_create":
return "Create"; return "Create";
case "ai_edit": case "ai_edit":
return commit.inputs.prompt; return item.inputs.prompt;
case "code_create": case "code_create":
return "Imported from code"; return "Imported from code";
default: { default: {
const exhaustiveCheck: never = commitType; const exhaustiveCheck: never = itemType;
throw new Error(`Unhandled case: ${exhaustiveCheck}`); throw new Error(`Unhandled case: ${exhaustiveCheck}`);
} }
} }
} }
export const renderHistory = (history: Commit[]) => { export const renderHistory = (
const renderedHistory = []; history: History,
currentVersion: number | null
) => {
const renderedHistory: RenderedHistoryItem[] = [];
for (let i = 0; i < history.length; i++) { for (let i = 0; i < history.length; i++) {
const commit = history[i]; const item = history[i];
// Only show the parent version if it's not the previous version
// (i.e. it's the branching point) and if it's not the first version
const parentVersion =
item.parentIndex !== null && item.parentIndex !== i - 1
? `v${(item.parentIndex || 0) + 1}`
: null;
const type = displayHistoryItemType(item.type);
const isActive = i === currentVersion;
const summary = summarizeHistoryItem(item);
renderedHistory.push({ renderedHistory.push({
...commit, isActive,
type: displayHistoryItemType(commit.type), summary: summary,
summary: summarizeHistoryItem(commit), parentVersion,
parentVersion: setParentVersion(commit, history), type,
}); });
} }

View File

@ -1,16 +0,0 @@
import React from "react";
interface DeprecationMessageProps {}
const DeprecationMessage: React.FC<DeprecationMessageProps> = () => {
return (
<div className="rounded-lg p-2 bg-fuchsia-200">
<p className="text-gray-800 text-sm">
We no longer support this model. Instead, code generation will use
GPT-4o or Claude Sonnet 3.5, the 2 state-of-the-art models.
</p>
</div>
);
};
export default DeprecationMessage;

View File

@ -1,25 +0,0 @@
export function PicoBadge() {
return (
<>
<a
href="https://screenshot-to-code.canny.io/feature-requests"
target="_blank"
>
<div
className="fixed z-50 bottom-16 right-5 rounded-md shadow bg-black
text-white px-4 text-xs py-3 cursor-pointer"
>
feature requests?
</div>
</a>
<a href="https://picoapps.xyz?ref=screenshot-to-code" target="_blank">
<div
className="fixed z-50 bottom-5 right-5 rounded-md shadow text-black
bg-white px-4 text-xs py-3 cursor-pointer"
>
an open source project by Pico
</div>
</a>
</>
);
}

View File

@ -1,16 +0,0 @@
import { URLS } from "../../urls";
function TipLink() {
return (
<a
className="text-xs underline text-gray-500 text-right"
href={URLS.tips}
target="_blank"
rel="noopener"
>
Tips for better results
</a>
);
}
export default TipLink;

View File

@ -1,56 +0,0 @@
import { useEffect, useRef, useState } from "react";
import classNames from "classnames";
import useThrottle from "../../hooks/useThrottle";
import EditPopup from "../select-and-edit/EditPopup";
interface Props {
code: string;
device: "mobile" | "desktop";
doUpdate: (updateInstruction: string, selectedElement?: HTMLElement) => void;
}
function PreviewComponent({ code, device, doUpdate }: Props) {
const iframeRef = useRef<HTMLIFrameElement | null>(null);
// Don't update code more often than every 200ms.
const throttledCode = useThrottle(code, 200);
// Select and edit functionality
const [clickEvent, setClickEvent] = useState<MouseEvent | null>(null);
useEffect(() => {
const iframe = iframeRef.current;
if (iframe) {
iframe.srcdoc = throttledCode;
// Set up click handler for select and edit funtionality
iframe.addEventListener("load", function () {
iframe.contentWindow?.document.body.addEventListener(
"click",
setClickEvent
);
});
}
}, [throttledCode]);
return (
<div className="flex justify-center mx-2">
<iframe
id={`preview-${device}`}
ref={iframeRef}
title="Preview"
className={classNames(
"border-[4px] border-black rounded-[20px] shadow-lg",
"transform scale-[0.9] origin-top",
{
"w-full h-[832px]": device === "desktop",
"w-[400px] h-[832px]": device === "mobile",
}
)}
></iframe>
<EditPopup event={clickEvent} iframeRef={iframeRef} doUpdate={doUpdate} />
</div>
);
}
export default PreviewComponent;

View File

@ -1,99 +0,0 @@
import { Tabs, TabsList, TabsTrigger, TabsContent } from "../ui/tabs";
import {
FaUndo,
FaDownload,
FaDesktop,
FaMobile,
FaCode,
} from "react-icons/fa";
import { AppState, Settings } from "../../types";
import CodeTab from "./CodeTab";
import { Button } from "../ui/button";
import { useAppStore } from "../../store/app-store";
import { useProjectStore } from "../../store/project-store";
import { extractHtml } from "./extractHtml";
import PreviewComponent from "./PreviewComponent";
import { downloadCode } from "./download";
interface Props {
doUpdate: (instruction: string) => void;
reset: () => void;
settings: Settings;
}
function PreviewPane({ doUpdate, reset, settings }: Props) {
const { appState } = useAppStore();
const { inputMode, head, commits } = useProjectStore();
const currentCommit = head && commits[head] ? commits[head] : "";
const currentCode = currentCommit
? currentCommit.variants[currentCommit.selectedVariantIndex].code
: "";
const previewCode =
inputMode === "video" && appState === AppState.CODING
? extractHtml(currentCode)
: currentCode;
return (
<div className="ml-4">
<Tabs defaultValue="desktop">
<div className="flex justify-between mr-8 mb-4">
<div className="flex items-center gap-x-2">
{appState === AppState.CODE_READY && (
<>
<Button
onClick={reset}
className="flex items-center ml-4 gap-x-2 dark:text-white dark:bg-gray-700"
>
<FaUndo />
Reset
</Button>
<Button
onClick={() => downloadCode(previewCode)}
variant="secondary"
className="flex items-center gap-x-2 mr-4 dark:text-white dark:bg-gray-700 download-btn"
>
<FaDownload /> Download
</Button>
</>
)}
</div>
<div className="flex items-center">
<TabsList>
<TabsTrigger value="desktop" className="flex gap-x-2">
<FaDesktop /> Desktop
</TabsTrigger>
<TabsTrigger value="mobile" className="flex gap-x-2">
<FaMobile /> Mobile
</TabsTrigger>
<TabsTrigger value="code" className="flex gap-x-2">
<FaCode />
Code
</TabsTrigger>
</TabsList>
</div>
</div>
<TabsContent value="desktop">
<PreviewComponent
code={previewCode}
device="desktop"
doUpdate={doUpdate}
/>
</TabsContent>
<TabsContent value="mobile">
<PreviewComponent
code={previewCode}
device="mobile"
doUpdate={doUpdate}
/>
</TabsContent>
<TabsContent value="code">
<CodeTab code={previewCode} setCode={() => {}} settings={settings} />
</TabsContent>
</Tabs>
</div>
);
}
export default PreviewPane;

View File

@ -1,16 +0,0 @@
export const downloadCode = (code: string) => {
// Create a blob from the generated code
const blob = new Blob([code], { type: "text/html" });
const url = URL.createObjectURL(blob);
// Create an anchor element and set properties for download
const a = document.createElement("a");
a.href = url;
a.download = "index.html"; // Set the file name for download
document.body.appendChild(a); // Append to the document
a.click(); // Programmatically click the anchor to trigger download
// Clean up by removing the anchor and revoking the Blob URL
document.body.removeChild(a);
URL.revokeObjectURL(url);
};

View File

@ -1,16 +0,0 @@
// Not robust enough to support <html lang='en'> for instance
export function extractHtml(code: string): string {
const lastHtmlStartIndex = code.lastIndexOf("<html>");
let htmlEndIndex = code.indexOf("</html>", lastHtmlStartIndex);
if (lastHtmlStartIndex !== -1) {
// If "</html>" is found, adjust htmlEndIndex to include the "</html>" tag
if (htmlEndIndex !== -1) {
htmlEndIndex += "</html>".length;
return code.slice(lastHtmlStartIndex, htmlEndIndex);
}
// If "</html>" is not found, return the rest of the string starting from the last "<html>"
return code.slice(lastHtmlStartIndex);
}
return "";
}

View File

@ -1,10 +0,0 @@
export function simpleHash(str: string, seed = 0) {
let hash = seed;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = (hash << 5) - hash + char;
hash |= 0; // Convert to 32bit integer
}
return hash;
}

View File

@ -1,145 +0,0 @@
import { useState } from "react";
import { Button } from "../ui/button";
import { ScreenRecorderState } from "../../types";
import { blobToBase64DataUrl } from "./utils";
import fixWebmDuration from "webm-duration-fix";
import toast from "react-hot-toast";
interface Props {
screenRecorderState: ScreenRecorderState;
setScreenRecorderState: (state: ScreenRecorderState) => void;
generateCode: (
referenceImages: string[],
inputMode: "image" | "video"
) => void;
}
function ScreenRecorder({
screenRecorderState,
setScreenRecorderState,
generateCode,
}: Props) {
const [mediaStream, setMediaStream] = useState<MediaStream | null>(null);
const [mediaRecorder, setMediaRecorder] = useState<MediaRecorder | null>(
null
);
const [screenRecordingDataUrl, setScreenRecordingDataUrl] = useState<
string | null
>(null);
const startScreenRecording = async () => {
try {
// Get the screen recording stream
const stream = await navigator.mediaDevices.getDisplayMedia({
video: true,
audio: { echoCancellation: true },
});
setMediaStream(stream);
// TODO: Test across different browsers
// Create the media recorder
const options = { mimeType: "video/webm" };
const mediaRecorder = new MediaRecorder(stream, options);
setMediaRecorder(mediaRecorder);
const chunks: BlobPart[] = [];
// Accumalate chunks as data is available
mediaRecorder.ondataavailable = (e: BlobEvent) => chunks.push(e.data);
// When media recorder is stopped, create a data URL
mediaRecorder.onstop = async () => {
// TODO: Do I need to fix duration if it's not a webm?
const completeBlob = await fixWebmDuration(
new Blob(chunks, {
type: options.mimeType,
})
);
const dataUrl = await blobToBase64DataUrl(completeBlob);
setScreenRecordingDataUrl(dataUrl);
setScreenRecorderState(ScreenRecorderState.FINISHED);
};
// Start recording
mediaRecorder.start();
setScreenRecorderState(ScreenRecorderState.RECORDING);
} catch (error) {
toast.error("Could not start screen recording");
throw error;
}
};
const stopScreenRecording = () => {
// Stop the recorder
if (mediaRecorder) {
mediaRecorder.stop();
setMediaRecorder(null);
}
// Stop the screen sharing stream
if (mediaStream) {
mediaStream.getTracks().forEach((track) => {
track.stop();
});
}
};
const kickoffGeneration = () => {
if (screenRecordingDataUrl) {
generateCode([screenRecordingDataUrl], "video");
} else {
toast.error("Screen recording does not exist. Please try again.");
throw new Error("No screen recording data url");
}
};
return (
<div className="flex items-center justify-center my-3">
{screenRecorderState === ScreenRecorderState.INITIAL && (
<Button onClick={startScreenRecording}>Record Screen</Button>
)}
{screenRecorderState === ScreenRecorderState.RECORDING && (
<div className="flex items-center flex-col gap-y-4">
<div className="flex items-center mr-2 text-xl gap-x-1">
<span className="block h-10 w-10 bg-red-600 rounded-full mr-1 animate-pulse"></span>
<span>Recording...</span>
</div>
<Button onClick={stopScreenRecording}>Finish Recording</Button>
</div>
)}
{screenRecorderState === ScreenRecorderState.FINISHED && (
<div className="flex items-center flex-col gap-y-4">
<div className="flex items-center mr-2 text-xl gap-x-1">
<span>Screen Recording Captured.</span>
</div>
{screenRecordingDataUrl && (
<video
muted
autoPlay
loop
className="w-[340px] border border-gray-200 rounded-md"
src={screenRecordingDataUrl}
/>
)}
<div className="flex gap-x-2">
<Button
variant="secondary"
onClick={() =>
setScreenRecorderState(ScreenRecorderState.INITIAL)
}
>
Re-record
</Button>
<Button onClick={kickoffGeneration}>Generate</Button>
</div>
</div>
)}
</div>
);
}
export default ScreenRecorder;

View File

@ -1,31 +0,0 @@
export function downloadBlob(blob: Blob) {
// Create a URL for the blob object
const videoURL = URL.createObjectURL(blob);
// Create a temporary anchor element and trigger the download
const a = document.createElement("a");
a.href = videoURL;
a.download = "recording.webm";
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
// Clear object URL
URL.revokeObjectURL(videoURL);
}
export function blobToBase64DataUrl(blob: Blob): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onloadend = () => {
if (reader.result) {
resolve(reader.result as string);
} else {
reject(new Error("FileReader did not return a result."));
}
};
reader.onerror = () =>
reject(new Error("FileReader encountered an error."));
reader.readAsDataURL(blob);
});
}

View File

@ -1,150 +0,0 @@
import React, { useEffect, useRef, useState } from "react";
import { Textarea } from "../ui/textarea";
import { Button } from "../ui/button";
import { addHighlight, getAdjustedCoordinates, removeHighlight } from "./utils";
import { useAppStore } from "../../store/app-store";
import KeyboardShortcutBadge from "../core/KeyboardShortcutBadge";
interface EditPopupProps {
event: MouseEvent | null;
iframeRef: React.RefObject<HTMLIFrameElement>;
doUpdate: (updateInstruction: string, selectedElement?: HTMLElement) => void;
}
const EditPopup: React.FC<EditPopupProps> = ({
event,
iframeRef,
doUpdate,
}) => {
// App state
const { inSelectAndEditMode } = useAppStore();
// Create a wrapper ref to store inSelectAndEditMode so the value is not stale
// in a event listener
const inSelectAndEditModeRef = useRef(inSelectAndEditMode);
// Update the ref whenever the state changes
useEffect(() => {
inSelectAndEditModeRef.current = inSelectAndEditMode;
}, [inSelectAndEditMode]);
// Popup state
const [popupVisible, setPopupVisible] = useState(false);
const [popupPosition, setPopupPosition] = useState({ x: 0, y: 0 });
// Edit state
const [selectedElement, setSelectedElement] = useState<
HTMLElement | undefined
>(undefined);
const [updateText, setUpdateText] = useState("");
// Textarea ref for focusing
const textareaRef = useRef<HTMLTextAreaElement | null>(null);
function onUpdate(updateText: string) {
// Perform the update
doUpdate(
updateText,
selectedElement ? removeHighlight(selectedElement) : selectedElement
);
// Unselect the element
setSelectedElement(undefined);
// Hide the popup
setPopupVisible(false);
}
// Remove highlight and reset state when not in select and edit mode
useEffect(() => {
if (!inSelectAndEditMode) {
if (selectedElement) removeHighlight(selectedElement);
setSelectedElement(undefined);
setPopupVisible(false);
}
}, [inSelectAndEditMode, selectedElement]);
// Handle the click event
useEffect(() => {
// Return if not in select and edit mode
if (!inSelectAndEditModeRef.current || !event) {
return;
}
// Prevent default to avoid issues like label clicks triggering textareas, etc.
event.preventDefault();
const targetElement = event.target as HTMLElement;
// Return if no target element
if (!targetElement) return;
// Highlight and set the selected element
setSelectedElement((prev) => {
// Remove style from previous element
if (prev) {
removeHighlight(prev);
}
return addHighlight(targetElement);
});
// Calculate adjusted coordinates
const adjustedCoordinates = getAdjustedCoordinates(
event.clientX,
event.clientY,
iframeRef.current?.getBoundingClientRect()
);
// Show the popup at the click position
setPopupVisible(true);
setPopupPosition({ x: adjustedCoordinates.x, y: adjustedCoordinates.y });
// Reset the update text
setUpdateText("");
// Focus the textarea
textareaRef.current?.focus();
}, [event, iframeRef]);
// Focus the textarea when the popup is visible (we can't do this only when handling the click event
// because the textarea is not rendered yet)
// We need to also do it in the click event because popupVisible doesn't change values in that event
useEffect(() => {
if (popupVisible) {
textareaRef.current?.focus();
}
}, [popupVisible]);
if (!popupVisible) return;
return (
<div
className="absolute bg-white dark:bg-gray-800 p-4 border border-gray-300 dark:border-gray-600 rounded shadow-lg w-60"
style={{ top: popupPosition.y, left: popupPosition.x }}
>
<Textarea
ref={textareaRef}
value={updateText}
onChange={(e) => setUpdateText(e.target.value)}
placeholder="Tell the AI what to change about this element..."
className="dark:bg-gray-700 dark:text-white"
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
onUpdate(updateText);
}
}}
/>
<div className="flex justify-end mt-2">
<Button
className="dark:bg-gray-700 dark:text-white"
onClick={() => onUpdate(updateText)}
>
Update <KeyboardShortcutBadge letter="enter" />
</Button>
</div>
</div>
);
};
export default EditPopup;

View File

@ -1,22 +0,0 @@
import { GiClick } from "react-icons/gi";
import { useAppStore } from "../../store/app-store";
import { Button } from "../ui/button";
function SelectAndEditModeToggleButton() {
const { inSelectAndEditMode, toggleInSelectAndEditMode } = useAppStore();
return (
<Button
onClick={toggleInSelectAndEditMode}
className="flex items-center gap-x-2 dark:text-white dark:bg-gray-700 regenerate-btn"
variant={inSelectAndEditMode ? "destructive" : "default"}
>
<GiClick className="text-lg" />
<span>
{inSelectAndEditMode ? "Exit selection mode" : "Select and update"}
</span>
</Button>
);
}
export default SelectAndEditModeToggleButton;

View File

@ -1,22 +0,0 @@
export function removeHighlight(element: HTMLElement) {
element.style.outline = "";
element.style.backgroundColor = "";
return element;
}
export function addHighlight(element: HTMLElement) {
element.style.outline = "2px dashed #1846db";
element.style.backgroundColor = "#bfcbf5";
return element;
}
export function getAdjustedCoordinates(
x: number,
y: number,
rect: DOMRect | undefined
) {
const offsetX = rect ? rect.left : 0;
const offsetY = rect ? rect.top : 0;
return { x: x + offsetX, y: y + offsetY };
}

View File

@ -0,0 +1,142 @@
import { useEffect, useState } from "react";
import { Settings } from "../../types";
import { Button } from "../ui/button";
import { Input } from "../ui/input";
import { Label } from "../ui/label";
import useThrottle from "../../hooks/useThrottle";
import { Progress } from "../ui/progress";
import { PICO_BACKEND_FORM_SECRET } from "../../config";
interface Props {
settings: Settings;
setSettings: React.Dispatch<React.SetStateAction<Settings>>;
}
interface UsageResponse {
used_credits: number;
total_credits: number;
is_valid: boolean;
}
enum FetchState {
EMPTY = "EMPTY",
LOADING = "LOADING",
INVALID = "INVALID",
VALID = "VALID",
}
function AccessCodeSection({ settings, setSettings }: Props) {
const [isLoading, setIsLoading] = useState(false);
const [isValid, setIsValid] = useState(false);
const [usedCredits, setUsedCredits] = useState(0);
const [totalCredits, setTotalCredits] = useState(0);
const throttledAccessCode = useThrottle(settings.accessCode || "", 500);
const fetchState = (() => {
if (!settings.accessCode) return FetchState.EMPTY;
if (isLoading) return FetchState.LOADING;
if (!isValid) return FetchState.INVALID;
return FetchState.VALID;
})();
async function fetchUsage(accessCode: string) {
const res = await fetch(
"https://backend.buildpicoapps.com/screenshot_to_code/get_access_code_usage",
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
access_code: accessCode,
secret: PICO_BACKEND_FORM_SECRET,
}),
}
);
const usage = (await res.json()) as UsageResponse;
if (!usage.is_valid) {
setIsValid(false);
} else {
setIsValid(true);
setUsedCredits(usage.used_credits);
setTotalCredits(usage.total_credits);
}
setIsLoading(false);
}
useEffect(() => {
// Don't do anything if access code is empty
if (!throttledAccessCode) return;
setIsLoading(true);
setIsValid(true);
// Wait for 500 ms before fetching usage
setTimeout(async () => {
await fetchUsage(throttledAccessCode);
}, 500);
}, [throttledAccessCode]);
return (
<div className="flex flex-col space-y-4 bg-slate-200 p-4 rounded dark:text-white dark:bg-slate-800">
<Label htmlFor="access-code">
<div>Access Code</div>
</Label>
<Input
id="access-code"
className="border-gray-700 dark:border-gray-700 dark:bg-gray-800 dark:text-white"
placeholder="Enter your Screenshot to Code access code"
value={settings.accessCode || ""}
onChange={(e) =>
setSettings((s) => ({
...s,
accessCode: e.target.value,
}))
}
/>
{fetchState === "EMPTY" && (
<div className="flex items-center justify-between">
<a href="https://buy.stripe.com/8wM6sre70gBW1nqaEE" target="_blank">
<Button size="sm" variant="secondary">
Buy credits
</Button>
</a>
</div>
)}
{fetchState === "LOADING" && (
<div className="flex items-center justify-between">
<span className="text-xs text-gray-700">Loading...</span>
</div>
)}
{fetchState === "INVALID" && (
<>
<div className="flex items-center justify-between">
<span className="text-xs text-gray-700">Invalid access code</span>
</div>
</>
)}
{fetchState === "VALID" && (
<>
<Progress value={(usedCredits / totalCredits) * 100} />
<div className="flex items-center justify-between">
<span className="text-xs text-gray-700">
{usedCredits} out of {totalCredits} credits used
</span>
<a href="https://buy.stripe.com/8wM6sre70gBW1nqaEE" target="_blank">
<Button size="sm">Add credits</Button>
</a>
</div>
</>
)}
</div>
);
}
export default AccessCodeSection;

View File

@ -1,37 +0,0 @@
import React from "react";
import { useAppStore } from "../../store/app-store";
import { AppState, Settings } from "../../types";
import OutputSettingsSection from "./OutputSettingsSection";
import { Stack } from "../../lib/stacks";
interface GenerationSettingsProps {
settings: Settings;
setSettings: React.Dispatch<React.SetStateAction<Settings>>;
}
export const GenerationSettings: React.FC<GenerationSettingsProps> = ({
settings,
setSettings,
}) => {
const { appState } = useAppStore();
function setStack(stack: Stack) {
setSettings((prev: Settings) => ({
...prev,
generatedCodeConfig: stack,
}));
}
const shouldDisableUpdates =
appState === AppState.CODING || appState === AppState.CODE_READY;
return (
<div className="flex flex-col gap-y-2">
<OutputSettingsSection
stack={settings.generatedCodeConfig}
setStack={setStack}
shouldDisableUpdates={shouldDisableUpdates}
/>
</div>
);
};

View File

@ -1,196 +0,0 @@
import { Switch } from "@radix-ui/react-switch";
import classNames from "classnames";
import { useAppStore } from "../../store/app-store";
import { useProjectStore } from "../../store/project-store";
import { AppState } from "../../types";
import CodePreview from "../preview/CodePreview";
import Spinner from "../core/Spinner";
import KeyboardShortcutBadge from "../core/KeyboardShortcutBadge";
import TipLink from "../messages/TipLink";
import SelectAndEditModeToggleButton from "../select-and-edit/SelectAndEditModeToggleButton";
import { Button } from "../ui/button";
import { Textarea } from "../ui/textarea";
import { useEffect, useRef } from "react";
import HistoryDisplay from "../history/HistoryDisplay";
import Variants from "../variants/Variants";
interface SidebarProps {
showSelectAndEditFeature: boolean;
doUpdate: (instruction: string) => void;
regenerate: () => void;
cancelCodeGeneration: () => void;
}
function Sidebar({
showSelectAndEditFeature,
doUpdate,
regenerate,
cancelCodeGeneration,
}: SidebarProps) {
const textareaRef = useRef<HTMLTextAreaElement>(null);
const {
appState,
updateInstruction,
setUpdateInstruction,
shouldIncludeResultImage,
setShouldIncludeResultImage,
} = useAppStore();
const { inputMode, referenceImages, executionConsoles, head, commits } =
useProjectStore();
const viewedCode =
head && commits[head]
? commits[head].variants[commits[head].selectedVariantIndex].code
: "";
const executionConsole =
(head && executionConsoles[commits[head].selectedVariantIndex]) || [];
// When coding is complete, focus on the update instruction textarea
useEffect(() => {
if (appState === AppState.CODE_READY && textareaRef.current) {
textareaRef.current.focus();
}
}, [appState]);
return (
<>
<Variants />
{/* Show code preview only when coding */}
{appState === AppState.CODING && (
<div className="flex flex-col">
{/* Speed disclaimer for video mode */}
{inputMode === "video" && (
<div
className="bg-yellow-100 border-l-4 border-yellow-500 text-yellow-700
p-2 text-xs mb-4 mt-1"
>
Code generation from videos can take 3-4 minutes. We do multiple
passes to get the best result. Please be patient.
</div>
)}
<div className="flex items-center gap-x-1">
<Spinner />
{executionConsole.slice(-1)[0]}
</div>
<CodePreview code={viewedCode} />
<div className="flex w-full">
<Button
onClick={cancelCodeGeneration}
className="w-full dark:text-white dark:bg-gray-700"
>
Cancel
</Button>
</div>
</div>
)}
{appState === AppState.CODE_READY && (
<div>
<div className="grid w-full gap-2">
<Textarea
ref={textareaRef}
placeholder="Tell the AI what to change..."
onChange={(e) => setUpdateInstruction(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter" && !e.shiftKey) {
doUpdate(updateInstruction);
}
}}
value={updateInstruction}
/>
<div className="flex justify-between items-center gap-x-2">
<div className="font-500 text-xs text-slate-700 dark:text-white">
Include screenshot of current version?
</div>
<Switch
checked={shouldIncludeResultImage}
onCheckedChange={setShouldIncludeResultImage}
className="dark:bg-gray-700"
/>
</div>
<Button
onClick={() => doUpdate(updateInstruction)}
className="dark:text-white dark:bg-gray-700 update-btn"
>
Update <KeyboardShortcutBadge letter="enter" />
</Button>
</div>
<div className="flex items-center justify-end gap-x-2 mt-2">
<Button
onClick={regenerate}
className="flex items-center gap-x-2 dark:text-white dark:bg-gray-700 regenerate-btn"
>
🔄 Regenerate
</Button>
{showSelectAndEditFeature && <SelectAndEditModeToggleButton />}
</div>
<div className="flex justify-end items-center mt-2">
<TipLink />
</div>
</div>
)}
{/* Reference image display */}
<div className="flex gap-x-2 mt-2">
{referenceImages.length > 0 && (
<div className="flex flex-col">
<div
className={classNames({
"scanning relative": appState === AppState.CODING,
})}
>
{inputMode === "image" && (
<img
className="w-[340px] border border-gray-200 rounded-md"
src={referenceImages[0]}
alt="Reference"
/>
)}
{inputMode === "video" && (
<video
muted
autoPlay
loop
className="w-[340px] border border-gray-200 rounded-md"
src={referenceImages[0]}
/>
)}
</div>
<div className="text-gray-400 uppercase text-sm text-center mt-1">
{inputMode === "video" ? "Original Video" : "Original Screenshot"}
</div>
</div>
)}
<div className="bg-gray-400 px-4 py-2 rounded text-sm hidden">
<h2 className="text-lg mb-4 border-b border-gray-800">Console</h2>
{Object.entries(executionConsoles).map(([index, lines]) => (
<div key={index}>
{lines.map((line, lineIndex) => (
<div
key={`${index}-${lineIndex}`}
className="border-b border-gray-400 mb-2 text-gray-600 font-mono"
>
<span className="font-bold mr-2">{`${index}:${
lineIndex + 1
}`}</span>
{line}
</div>
))}
</div>
))}
</div>
</div>
<HistoryDisplay shouldDisableReverts={appState === AppState.CODING} />
</>
);
}
export default Sidebar;

View File

@ -1,27 +0,0 @@
import React from "react";
import ImageUpload from "../ImageUpload";
import { UrlInputSection } from "../UrlInputSection";
import ImportCodeSection from "../ImportCodeSection";
import { Settings } from "../../types";
import { Stack } from "../../lib/stacks";
interface Props {
doCreate: (images: string[], inputMode: "image" | "video") => void;
importFromCode: (code: string, stack: Stack) => void;
settings: Settings;
}
const StartPane: React.FC<Props> = ({ doCreate, importFromCode, settings }) => {
return (
<div className="flex flex-col justify-center items-center gap-y-10">
<ImageUpload setReferenceImages={doCreate} />
<UrlInputSection
doCreate={doCreate}
screenshotOneApiKey={settings.screenshotOneApiKey}
/>
<ImportCodeSection importFromCode={importFromCode} />
</div>
);
};
export default StartPane;

View File

@ -1,42 +0,0 @@
import { useProjectStore } from "../../store/project-store";
function Variants() {
const { inputMode, head, commits, updateSelectedVariantIndex } =
useProjectStore();
// If there is no head, don't show the variants
if (head === null) {
return null;
}
const commit = commits[head];
const variants = commit.variants;
const selectedVariantIndex = commit.selectedVariantIndex;
// If there is only one variant or the commit is already committed, don't show the variants
if (variants.length <= 1 || commit.isCommitted || inputMode === "video") {
return <div className="mt-2"></div>;
}
return (
<div className="mt-4 mb-4">
<div className="grid grid-cols-2 gap-2">
{variants.map((_, index) => (
<div
key={index}
className={`p-2 border rounded-md cursor-pointer ${
index === selectedVariantIndex
? "bg-blue-100 dark:bg-blue-900"
: "bg-gray-50 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-700"
}`}
onClick={() => updateSelectedVariantIndex(head, index)}
>
<h3 className="font-medium mb-1">Option {index + 1}</h3>
</div>
))}
</div>
</div>
);
}
export default Variants;

View File

@ -1,3 +1 @@
// WebSocket protocol (RFC 6455) allows for the use of custom close codes in the range 4000-4999
export const APP_ERROR_WEB_SOCKET_CODE = 4332;
export const USER_CLOSE_WEB_SOCKET_CODE = 4333; export const USER_CLOSE_WEB_SOCKET_CODE = 4333;

View File

@ -1,9 +1,6 @@
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import { WS_BACKEND_URL } from "./config"; import { WS_BACKEND_URL } from "./config";
import { import { USER_CLOSE_WEB_SOCKET_CODE } from "./constants";
APP_ERROR_WEB_SOCKET_CODE,
USER_CLOSE_WEB_SOCKET_CODE,
} from "./constants";
import { FullGenerationSettings } from "./types"; import { FullGenerationSettings } from "./types";
const ERROR_MESSAGE = const ERROR_MESSAGE =
@ -11,18 +8,12 @@ const ERROR_MESSAGE =
const CANCEL_MESSAGE = "Code generation cancelled"; const CANCEL_MESSAGE = "Code generation cancelled";
type WebSocketResponse = {
type: "chunk" | "status" | "setCode" | "error";
value: string;
variantIndex: number;
};
export function generateCode( export function generateCode(
wsRef: React.MutableRefObject<WebSocket | null>, wsRef: React.MutableRefObject<WebSocket | null>,
params: FullGenerationSettings, params: FullGenerationSettings,
onChange: (chunk: string, variantIndex: number) => void, onChange: (chunk: string) => void,
onSetCode: (code: string, variantIndex: number) => void, onSetCode: (code: string) => void,
onStatusUpdate: (status: string, variantIndex: number) => void, onStatusUpdate: (status: string) => void,
onCancel: () => void, onCancel: () => void,
onComplete: () => void onComplete: () => void
) { ) {
@ -37,13 +28,13 @@ export function generateCode(
}); });
ws.addEventListener("message", async (event: MessageEvent) => { ws.addEventListener("message", async (event: MessageEvent) => {
const response = JSON.parse(event.data) as WebSocketResponse; const response = JSON.parse(event.data);
if (response.type === "chunk") { if (response.type === "chunk") {
onChange(response.value, response.variantIndex); onChange(response.value);
} else if (response.type === "status") { } else if (response.type === "status") {
onStatusUpdate(response.value, response.variantIndex); onStatusUpdate(response.value);
} else if (response.type === "setCode") { } else if (response.type === "setCode") {
onSetCode(response.value, response.variantIndex); onSetCode(response.value);
} else if (response.type === "error") { } else if (response.type === "error") {
console.error("Error generating code", response.value); console.error("Error generating code", response.value);
toast.error(response.value); toast.error(response.value);
@ -55,13 +46,9 @@ export function generateCode(
if (event.code === USER_CLOSE_WEB_SOCKET_CODE) { if (event.code === USER_CLOSE_WEB_SOCKET_CODE) {
toast.success(CANCEL_MESSAGE); toast.success(CANCEL_MESSAGE);
onCancel(); onCancel();
} else if (event.code === APP_ERROR_WEB_SOCKET_CODE) {
console.error("Known server error", event);
onCancel();
} else if (event.code !== 1000) { } else if (event.code !== 1000) {
console.error("Unknown server or connection error", event); console.error("WebSocket error code", event);
toast.error(ERROR_MESSAGE); toast.error(ERROR_MESSAGE);
onCancel();
} else { } else {
onComplete(); onComplete();
} }

View File

@ -1,29 +0,0 @@
import { useEffect } from "react";
const CODING_SETTINGS = {
title: "Coding...",
favicon: "/favicon/coding.png",
};
const DEFAULT_SETTINGS = {
title: "Screenshot to Code",
favicon: "/favicon/main.png",
};
const useBrowserTabIndicator = (isCoding: boolean) => {
useEffect(() => {
const settings = isCoding ? CODING_SETTINGS : DEFAULT_SETTINGS;
// Set favicon
const faviconEl = document.querySelector(
"link[rel='icon']"
) as HTMLLinkElement | null;
if (faviconEl) {
faviconEl.href = settings.favicon;
}
// Set title
document.title = settings.title;
}, [isCoding]);
};
export default useBrowserTabIndicator;

View File

@ -1,20 +1,12 @@
// Keep in sync with backend (llm.py) // Keep in sync with backend (llm.py)
// Order here matches dropdown order
export enum CodeGenerationModel { export enum CodeGenerationModel {
CLAUDE_3_5_SONNET_2024_06_20 = "claude-3-5-sonnet-20240620",
GPT_4O_2024_05_13 = "gpt-4o-2024-05-13",
GPT_4_TURBO_2024_04_09 = "gpt-4-turbo-2024-04-09",
GPT_4_VISION = "gpt_4_vision", GPT_4_VISION = "gpt_4_vision",
CLAUDE_3_SONNET = "claude_3_sonnet", CLAUDE_3_SONNET = "claude_3_sonnet",
} }
// Will generate a static error if a model in the enum above is not in the descriptions
export const CODE_GENERATION_MODEL_DESCRIPTIONS: { export const CODE_GENERATION_MODEL_DESCRIPTIONS: {
[key in CodeGenerationModel]: { name: string; inBeta: boolean }; [key in CodeGenerationModel]: { name: string; inBeta: boolean };
} = { } = {
"gpt-4o-2024-05-13": { name: "GPT-4o", inBeta: false }, gpt_4_vision: { name: "GPT-4 Vision", inBeta: false },
"claude-3-5-sonnet-20240620": { name: "Claude 3.5 Sonnet", inBeta: false }, claude_3_sonnet: { name: "Claude 3 Sonnet", inBeta: true },
"gpt-4-turbo-2024-04-09": { name: "GPT-4 Turbo (deprecated)", inBeta: false },
gpt_4_vision: { name: "GPT-4 Vision (deprecated)", inBeta: false },
claude_3_sonnet: { name: "Claude 3 (deprecated)", inBeta: false },
}; };

View File

@ -1,8 +1,6 @@
// Keep in sync with backend (prompts/types.py) // Keep in sync with backend (prompts/types.py)
// Order here determines order in dropdown
export enum Stack { export enum Stack {
HTML_TAILWIND = "html_tailwind", HTML_TAILWIND = "html_tailwind",
HTML_CSS = "html_css",
REACT_TAILWIND = "react_tailwind", REACT_TAILWIND = "react_tailwind",
BOOTSTRAP = "bootstrap", BOOTSTRAP = "bootstrap",
VUE_TAILWIND = "vue_tailwind", VUE_TAILWIND = "vue_tailwind",
@ -13,7 +11,6 @@ export enum Stack {
export const STACK_DESCRIPTIONS: { export const STACK_DESCRIPTIONS: {
[key in Stack]: { components: string[]; inBeta: boolean }; [key in Stack]: { components: string[]; inBeta: boolean };
} = { } = {
html_css: { components: ["HTML", "CSS"], inBeta: false },
html_tailwind: { components: ["HTML", "Tailwind"], inBeta: false }, html_tailwind: { components: ["HTML", "Tailwind"], inBeta: false },
react_tailwind: { components: ["React", "Tailwind"], inBeta: false }, react_tailwind: { components: ["React", "Tailwind"], inBeta: false },
bootstrap: { components: ["Bootstrap"], inBeta: false }, bootstrap: { components: ["Bootstrap"], inBeta: false },

View File

@ -1,14 +0,0 @@
import html2canvas from "html2canvas";
export const takeScreenshot = async (): Promise<string> => {
const iframeElement = document.querySelector(
"#preview-desktop"
) as HTMLIFrameElement;
if (!iframeElement?.contentWindow?.document.body) {
return "";
}
const canvas = await html2canvas(iframeElement.contentWindow.document.body);
const png = canvas.toDataURL("image/png");
return png;
};

View File

@ -1,3 +0,0 @@
// So jest test runner can read env vars from .env file
import { config } from "dotenv";
config({ path: ".env.jest" });

View File

@ -1,36 +0,0 @@
import { create } from "zustand";
import { AppState } from "../types";
// Store for app-wide state
interface AppStore {
appState: AppState;
setAppState: (state: AppState) => void;
// UI state
updateInstruction: string;
setUpdateInstruction: (instruction: string) => void;
shouldIncludeResultImage: boolean;
setShouldIncludeResultImage: (shouldInclude: boolean) => void;
inSelectAndEditMode: boolean;
toggleInSelectAndEditMode: () => void;
disableInSelectAndEditMode: () => void;
}
export const useAppStore = create<AppStore>((set) => ({
appState: AppState.INITIAL,
setAppState: (state: AppState) => set({ appState: state }),
// UI state
updateInstruction: "",
setUpdateInstruction: (instruction: string) =>
set({ updateInstruction: instruction }),
shouldIncludeResultImage: true,
setShouldIncludeResultImage: (shouldInclude: boolean) =>
set({ shouldIncludeResultImage: shouldInclude }),
inSelectAndEditMode: false,
toggleInSelectAndEditMode: () =>
set((state) => ({ inSelectAndEditMode: !state.inSelectAndEditMode })),
disableInSelectAndEditMode: () => set({ inSelectAndEditMode: false }),
}));

View File

@ -1,149 +0,0 @@
import { create } from "zustand";
import { Commit, CommitHash } from "../components/commits/types";
// Store for app-wide state
interface ProjectStore {
// Inputs
inputMode: "image" | "video";
setInputMode: (mode: "image" | "video") => void;
isImportedFromCode: boolean;
setIsImportedFromCode: (imported: boolean) => void;
referenceImages: string[];
setReferenceImages: (images: string[]) => void;
// Outputs
commits: Record<string, Commit>;
head: CommitHash | null;
addCommit: (commit: Commit) => void;
removeCommit: (hash: CommitHash) => void;
resetCommits: () => void;
appendCommitCode: (
hash: CommitHash,
numVariant: number,
code: string
) => void;
setCommitCode: (hash: CommitHash, numVariant: number, code: string) => void;
updateSelectedVariantIndex: (hash: CommitHash, index: number) => void;
setHead: (hash: CommitHash) => void;
resetHead: () => void;
executionConsoles: { [key: number]: string[] };
appendExecutionConsole: (variantIndex: number, line: string) => void;
resetExecutionConsoles: () => void;
}
export const useProjectStore = create<ProjectStore>((set) => ({
// Inputs and their setters
inputMode: "image",
setInputMode: (mode) => set({ inputMode: mode }),
isImportedFromCode: false,
setIsImportedFromCode: (imported) => set({ isImportedFromCode: imported }),
referenceImages: [],
setReferenceImages: (images) => set({ referenceImages: images }),
// Outputs
commits: {},
head: null,
addCommit: (commit: Commit) => {
// When adding a new commit, make sure all existing commits are marked as committed
set((state) => ({
commits: {
...Object.fromEntries(
Object.entries(state.commits).map(([hash, existingCommit]) => [
hash,
{ ...existingCommit, isCommitted: true },
])
),
[commit.hash]: commit,
},
}));
},
removeCommit: (hash: CommitHash) => {
set((state) => {
const newCommits = { ...state.commits };
delete newCommits[hash];
return { commits: newCommits };
});
},
resetCommits: () => set({ commits: {} }),
appendCommitCode: (hash: CommitHash, numVariant: number, code: string) =>
set((state) => {
const commit = state.commits[hash];
// Don't update if the commit is already committed
if (commit.isCommitted) {
throw new Error("Attempted to append code to a committed commit");
}
return {
commits: {
...state.commits,
[hash]: {
...commit,
variants: commit.variants.map((variant, index) =>
index === numVariant
? { ...variant, code: variant.code + code }
: variant
),
},
},
};
}),
setCommitCode: (hash: CommitHash, numVariant: number, code: string) =>
set((state) => {
const commit = state.commits[hash];
// Don't update if the commit is already committed
if (commit.isCommitted) {
throw new Error("Attempted to set code of a committed commit");
}
return {
commits: {
...state.commits,
[hash]: {
...commit,
variants: commit.variants.map((variant, index) =>
index === numVariant ? { ...variant, code } : variant
),
},
},
};
}),
updateSelectedVariantIndex: (hash: CommitHash, index: number) =>
set((state) => {
const commit = state.commits[hash];
// Don't update if the commit is already committed
if (commit.isCommitted) {
throw new Error(
"Attempted to update selected variant index of a committed commit"
);
}
return {
commits: {
...state.commits,
[hash]: {
...commit,
selectedVariantIndex: index,
},
},
};
}),
setHead: (hash: CommitHash) => set({ head: hash }),
resetHead: () => set({ head: null }),
executionConsoles: {},
appendExecutionConsole: (variantIndex: number, line: string) =>
set((state) => ({
executionConsoles: {
...state.executionConsoles,
[variantIndex]: [
...(state.executionConsoles[variantIndex] || []),
line,
],
},
})),
resetExecutionConsoles: () => set({ executionConsoles: {} }),
}));

Some files were not shown because too many files have changed in this diff Show More