Compare commits
154 Commits
qa-testing
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a53afd350d | ||
|
|
6899c7792e | ||
|
|
9199fee21d | ||
|
|
8717298def | ||
|
|
a4087e613f | ||
|
|
cf6b94d675 | ||
|
|
589507846b | ||
|
|
24995c302e | ||
|
|
e4b021c2a4 | ||
|
|
b4a9bbd9da | ||
|
|
0db8a206f2 | ||
|
|
c1fc28624d | ||
|
|
c55e5d7923 | ||
|
|
67ce707c3c | ||
|
|
5d33dd6bf3 | ||
|
|
67e7066221 | ||
|
|
e67239745c | ||
|
|
ce6a0622d0 | ||
|
|
6542289931 | ||
|
|
0ff42d9083 | ||
|
|
f09b1c3c7f | ||
|
|
bf3e4eecb2 | ||
|
|
939d4eebf8 | ||
|
|
a243480d78 | ||
|
|
30d5cd2b65 | ||
|
|
637f75b93e | ||
|
|
13f93e259d | ||
|
|
c900201417 | ||
|
|
089f4302d2 | ||
|
|
8e8f0b4b64 | ||
|
|
5f6dd08411 | ||
|
|
b158597d7e | ||
|
|
fb5480b036 | ||
|
|
c76c7c202a | ||
|
|
823bd2e249 | ||
|
|
710f2987a4 | ||
|
|
5c3f915bce | ||
|
|
701d97ec74 | ||
|
|
7b2e2963ad | ||
|
|
637c1b4c1d | ||
|
|
c61a2ac772 | ||
|
|
bcb89a3c23 | ||
|
|
3591588e2b | ||
|
|
dd7a51dd34 | ||
|
|
3fbc0f9458 | ||
|
|
ff12790883 | ||
|
|
0f731598dd | ||
|
|
64dd7d6279 | ||
|
|
96658819f3 | ||
|
|
24a123db36 | ||
|
|
64926408b0 | ||
|
|
0700de7767 | ||
|
|
46c480931a | ||
|
|
f52ca306a5 | ||
|
|
aff9352dc0 | ||
|
|
4b0adc5769 | ||
|
|
52099e0853 | ||
|
|
d7ab620e0b | ||
|
|
21f553a0d8 | ||
|
|
7af2e34415 | ||
|
|
59b031e395 | ||
|
|
88f4e81b96 | ||
|
|
40c0f52075 | ||
|
|
7eac0741e0 | ||
|
|
9b8ec5b67c | ||
|
|
c72484a72c | ||
|
|
0d639b4920 | ||
|
|
1a3086cc9a | ||
|
|
993ff88e2b | ||
|
|
deb2375146 | ||
|
|
dcef298dba | ||
|
|
83f6f00b10 | ||
|
|
98951e0382 | ||
|
|
fd6a8f779d | ||
|
|
ba0d51a34f | ||
|
|
3f6fcdea25 | ||
|
|
54b59c85d6 | ||
|
|
9d11866143 | ||
|
|
9f732c4f5d | ||
|
|
a2d6fda7fd | ||
|
|
329b8e8e77 | ||
|
|
9b99cad286 | ||
|
|
6c2bd1eb64 | ||
|
|
f6da5eb0d4 | ||
|
|
9f034f7dcc | ||
|
|
24a7462c9f | ||
|
|
9ec3adad19 | ||
|
|
d2359f644c | ||
|
|
8e8ad1409f | ||
|
|
606f535bc2 | ||
|
|
38a06517fb | ||
|
|
638053a54c | ||
|
|
ec1bada3e2 | ||
|
|
51e06a4c4b | ||
|
|
d4e182cfe0 | ||
|
|
0200274e61 | ||
|
|
901ce332a5 | ||
|
|
edfd16ef1d | ||
|
|
89e442423c | ||
|
|
9dfb0dc5b8 | ||
|
|
4288cf2088 | ||
|
|
6be83b4a2d | ||
|
|
5843e75760 | ||
|
|
6fe7098201 | ||
|
|
9a4fbd4591 | ||
|
|
f75294ca64 | ||
|
|
49a24d8b0c | ||
|
|
9e2eb0c58c | ||
|
|
392b9849a2 | ||
|
|
f7c9209d18 | ||
|
|
1c13897f45 | ||
|
|
402a279eaf | ||
|
|
465dc64439 | ||
|
|
228870f8fc | ||
|
|
561ac0b088 | ||
|
|
7cd9b294c3 | ||
|
|
8de0e95c54 | ||
|
|
a73b1f6dc3 | ||
|
|
9fe32ba6e8 | ||
|
|
e1265b2b75 | ||
|
|
5f3868f3c9 | ||
|
|
423c74bdc9 | ||
|
|
3fbb8f074a | ||
|
|
a7c905a301 | ||
|
|
5a62fce52d | ||
|
|
b9fb11b6fd | ||
|
|
328dc7931d | ||
|
|
2204896e31 | ||
|
|
22e45cc566 | ||
|
|
f6f8b2f707 | ||
|
|
0959441834 | ||
|
|
c64968b8f0 | ||
|
|
8e579e425e | ||
|
|
9cfcf928d0 | ||
|
|
9e2cbdce94 | ||
|
|
9d1fbc9b23 | ||
|
|
43ae003c83 | ||
|
|
2709312943 | ||
|
|
b0e7ae35af | ||
|
|
f18724104c | ||
|
|
82132d57aa | ||
|
|
608ba5cc6f | ||
|
|
97cdb093a5 | ||
|
|
939539611f | ||
|
|
f0ee686660 | ||
|
|
c5695975c7 | ||
|
|
d31ebcaa27 | ||
|
|
23e631765e | ||
|
|
bef9aa831a | ||
|
|
bf38200d2a | ||
|
|
1f9c5b2c80 | ||
|
|
ac8198e5ba | ||
|
|
06a022a8c0 | ||
|
|
7c1fb7a54d |
1
.github/FUNDING.yml
vendored
Normal file
1
.github/FUNDING.yml
vendored
Normal file
@ -0,0 +1 @@
|
||||
github: [abi]
|
||||
19
README.md
19
README.md
@ -1,12 +1,13 @@
|
||||
# screenshot-to-code
|
||||
|
||||
A simple tool to convert screenshots, mockups and Figma designs into clean, functional code using AI. **Now supporting GPT-4O!**
|
||||
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!**
|
||||
|
||||
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
|
||||
@ -15,7 +16,8 @@ Supported stacks:
|
||||
|
||||
Supported AI models:
|
||||
|
||||
- GPT-4O - Best model!
|
||||
- Claude Sonnet 3.5 - Best model!
|
||||
- GPT-4O - also recommended!
|
||||
- GPT-4 Turbo (Apr 2024)
|
||||
- GPT-4 Vision (Nov 2023)
|
||||
- Claude 3 Sonnet
|
||||
@ -31,13 +33,18 @@ We also just added experimental support for taking a video/screen recording of a
|
||||
|
||||
[Follow me on Twitter for updates](https://twitter.com/_abi_).
|
||||
|
||||
## 🚀 Try It Out without no install
|
||||
## 🚀 Hosted Version
|
||||
|
||||
[Try it live on the hosted version (paid)](https://screenshottocode.com).
|
||||
|
||||
## 🛠 Getting Started
|
||||
|
||||
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 or an Anthropic key if you want to use Claude Sonnet, or for experimental video support.
|
||||
The app has a React/Vite frontend and a FastAPI backend.
|
||||
|
||||
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):
|
||||
|
||||
@ -49,7 +56,7 @@ poetry shell
|
||||
poetry run uvicorn main:app --reload --port 7001
|
||||
```
|
||||
|
||||
If you want to use Anthropic, add the `ANTHROPIC_API_KEY` to `backend/.env` with your API key from Anthropic.
|
||||
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:
|
||||
|
||||
@ -108,5 +115,3 @@ https://github.com/abi/screenshot-to-code/assets/23818/3fec0f77-44e8-4fb3-a769-a
|
||||
## 🌍 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.
|
||||
|
||||
[](https://www.buymeacoffee.com/abiraja)
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
### Getting an OpenAI API key with GPT4-Vision model access
|
||||
### Getting an OpenAI API key with GPT-4 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:
|
||||
|
||||
|
||||
@ -7,19 +7,19 @@ repos:
|
||||
- id: end-of-file-fixer
|
||||
- id: check-yaml
|
||||
- id: check-added-large-files
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: poetry-pytest
|
||||
name: Run pytest with Poetry
|
||||
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
|
||||
# - repo: local
|
||||
# hooks:
|
||||
# - id: poetry-pytest
|
||||
# name: Run pytest with Poetry
|
||||
# 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
|
||||
# # pass_filenames: false
|
||||
# # always_run: true
|
||||
# # files: ^backend/
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
FROM python:3.12-slim-bullseye
|
||||
FROM python:3.12.3-slim-bullseye
|
||||
|
||||
ENV POETRY_VERSION 1.4.1
|
||||
|
||||
|
||||
0
backend/codegen/__init__.py
Normal file
0
backend/codegen/__init__.py
Normal file
57
backend/codegen/test_utils.py
Normal file
57
backend/codegen/test_utils.py
Normal file
@ -0,0 +1,57 @@
|
||||
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()
|
||||
14
backend/codegen/utils.py
Normal file
14
backend/codegen/utils.py
Normal file
@ -0,0 +1,14 @@
|
||||
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
|
||||
@ -3,7 +3,15 @@
|
||||
# TODO: Should only be set to true when value is 'True', not any abitrary truthy value
|
||||
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)
|
||||
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
|
||||
|
||||
|
||||
@ -1,37 +1,40 @@
|
||||
import os
|
||||
from config import ANTHROPIC_API_KEY
|
||||
|
||||
from config import ANTHROPIC_API_KEY, OPENAI_API_KEY
|
||||
from llm import Llm, stream_claude_response, stream_openai_response
|
||||
from prompts import assemble_prompt
|
||||
from prompts.types import Stack
|
||||
from openai.types.chat import ChatCompletionMessageParam
|
||||
|
||||
|
||||
async def generate_code_core(image_url: str, stack: Stack, model: Llm) -> str:
|
||||
async def generate_code_for_image(image_url: str, stack: Stack, model: Llm) -> str:
|
||||
prompt_messages = assemble_prompt(image_url, stack)
|
||||
openai_api_key = os.environ.get("OPENAI_API_KEY")
|
||||
anthropic_api_key = ANTHROPIC_API_KEY
|
||||
openai_base_url = None
|
||||
return await generate_code_core(prompt_messages, model)
|
||||
|
||||
async def process_chunk(content: str):
|
||||
|
||||
async def generate_code_core(
|
||||
prompt_messages: list[ChatCompletionMessageParam], model: Llm
|
||||
) -> str:
|
||||
|
||||
async def process_chunk(_: str):
|
||||
pass
|
||||
|
||||
if model == Llm.CLAUDE_3_SONNET:
|
||||
if not anthropic_api_key:
|
||||
if model == Llm.CLAUDE_3_SONNET or model == Llm.CLAUDE_3_5_SONNET_2024_06_20:
|
||||
if not ANTHROPIC_API_KEY:
|
||||
raise Exception("Anthropic API key not found")
|
||||
|
||||
completion = await stream_claude_response(
|
||||
prompt_messages,
|
||||
api_key=anthropic_api_key,
|
||||
api_key=ANTHROPIC_API_KEY,
|
||||
callback=lambda x: process_chunk(x),
|
||||
model=model,
|
||||
)
|
||||
else:
|
||||
if not openai_api_key:
|
||||
if not OPENAI_API_KEY:
|
||||
raise Exception("OpenAI API key not found")
|
||||
|
||||
completion = await stream_openai_response(
|
||||
prompt_messages,
|
||||
api_key=openai_api_key,
|
||||
base_url=openai_base_url,
|
||||
api_key=OPENAI_API_KEY,
|
||||
base_url=None,
|
||||
callback=lambda x: process_chunk(x),
|
||||
model=model,
|
||||
)
|
||||
|
||||
0
backend/fs_logging/__init__.py
Normal file
0
backend/fs_logging/__init__.py
Normal file
23
backend/fs_logging/core.py
Normal file
23
backend/fs_logging/core.py
Normal file
@ -0,0 +1,23 @@
|
||||
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}))
|
||||
0
backend/image_generation/__init__.py
Normal file
0
backend/image_generation/__init__.py
Normal file
@ -1,40 +1,72 @@
|
||||
import asyncio
|
||||
import re
|
||||
from typing import Dict, List, Union
|
||||
from typing import Dict, List, Literal, Union
|
||||
from openai import AsyncOpenAI
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
from image_generation.replicate import call_replicate
|
||||
|
||||
async def process_tasks(prompts: List[str], api_key: str, base_url: str):
|
||||
tasks = [generate_image(prompt, api_key, base_url) for prompt in prompts]
|
||||
|
||||
async def process_tasks(
|
||||
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)
|
||||
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]] = []
|
||||
for result in results:
|
||||
if isinstance(result, Exception):
|
||||
if isinstance(result, BaseException):
|
||||
print(f"An exception occurred: {result}")
|
||||
processed_results.append(None)
|
||||
else:
|
||||
processed_results.append(result) # type: ignore
|
||||
processed_results.append(result)
|
||||
|
||||
return processed_results
|
||||
|
||||
|
||||
async def generate_image(prompt: str, api_key: str, base_url: str):
|
||||
async def generate_image_dalle(
|
||||
prompt: str, api_key: str, base_url: str | None
|
||||
) -> Union[str, None]:
|
||||
client = AsyncOpenAI(api_key=api_key, base_url=base_url)
|
||||
image_params: Dict[str, Union[str, int]] = {
|
||||
"model": "dall-e-3",
|
||||
"quality": "standard",
|
||||
"style": "natural",
|
||||
"n": 1,
|
||||
"size": "1024x1024",
|
||||
"prompt": prompt,
|
||||
}
|
||||
res = await client.images.generate(**image_params) # type: ignore
|
||||
res = await client.images.generate(
|
||||
model="dall-e-3",
|
||||
quality="standard",
|
||||
style="natural",
|
||||
n=1,
|
||||
size="1024x1024",
|
||||
prompt=prompt,
|
||||
)
|
||||
await client.close()
|
||||
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):
|
||||
# Regular expression to match numbers in the format '300x200'
|
||||
matches = re.findall(r"(\d+)x(\d+)", url)
|
||||
@ -62,14 +94,18 @@ def create_alt_url_mapping(code: str) -> Dict[str, str]:
|
||||
|
||||
|
||||
async def generate_images(
|
||||
code: str, api_key: str, base_url: Union[str, None], image_cache: Dict[str, str]
|
||||
):
|
||||
code: str,
|
||||
api_key: str,
|
||||
base_url: Union[str, None],
|
||||
image_cache: Dict[str, str],
|
||||
model: Literal["dalle3", "flux"] = "dalle3",
|
||||
) -> str:
|
||||
# Find all images
|
||||
soup = BeautifulSoup(code, "html.parser")
|
||||
images = soup.find_all("img")
|
||||
|
||||
# Extract alt texts as image prompts
|
||||
alts = []
|
||||
alts: List[str | None] = []
|
||||
for img in images:
|
||||
# Only include URL if the image starts with https://placehold.co
|
||||
# and it's not already in the image_cache
|
||||
@ -77,26 +113,26 @@ async def generate_images(
|
||||
img["src"].startswith("https://placehold.co")
|
||||
and image_cache.get(img.get("alt")) is None
|
||||
):
|
||||
alts.append(img.get("alt", None)) # type: ignore
|
||||
alts.append(img.get("alt", None))
|
||||
|
||||
# Exclude images with no alt text
|
||||
alts = [alt for alt in alts if alt is not None] # type: ignore
|
||||
filtered_alts: List[str] = [alt for alt in alts if alt is not None]
|
||||
|
||||
# Remove duplicates
|
||||
prompts = list(set(alts)) # type: ignore
|
||||
prompts = list(set(filtered_alts))
|
||||
|
||||
# Return early if there are no images to replace
|
||||
if len(prompts) == 0: # type: ignore
|
||||
if len(prompts) == 0:
|
||||
return code
|
||||
|
||||
# Generate images
|
||||
results = await process_tasks(prompts, api_key, base_url) # type: ignore
|
||||
results = await process_tasks(prompts, api_key, base_url, model)
|
||||
|
||||
# Create a dict mapping alt text to image URL
|
||||
mapped_image_urls = dict(zip(prompts, results)) # type: ignore
|
||||
mapped_image_urls = dict(zip(prompts, results))
|
||||
|
||||
# Merge with image_cache
|
||||
mapped_image_urls = {**mapped_image_urls, **image_cache} # type: ignore
|
||||
mapped_image_urls = {**mapped_image_urls, **image_cache}
|
||||
|
||||
# Replace old image URLs with the generated URLs
|
||||
for img in images:
|
||||
65
backend/image_generation/replicate.py
Normal file
65
backend/image_generation/replicate.py
Normal file
@ -0,0 +1,65 @@
|
||||
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}")
|
||||
0
backend/image_processing/__init__.py
Normal file
0
backend/image_processing/__init__.py
Normal file
80
backend/image_processing/utils.py
Normal file
80
backend/image_processing/utils.py
Normal file
@ -0,0 +1,80 @@
|
||||
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"))
|
||||
@ -1,3 +1,4 @@
|
||||
import copy
|
||||
from enum import Enum
|
||||
from typing import Any, Awaitable, Callable, List, cast
|
||||
from anthropic import AsyncAnthropic
|
||||
@ -5,6 +6,7 @@ from openai import AsyncOpenAI
|
||||
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
|
||||
|
||||
@ -17,6 +19,7 @@ class Llm(Enum):
|
||||
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
|
||||
@ -59,6 +62,12 @@ async def stream_openai_response(
|
||||
full_response = ""
|
||||
async for chunk in stream: # type: ignore
|
||||
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 ""
|
||||
full_response += content
|
||||
await callback(content)
|
||||
@ -73,18 +82,22 @@ async def stream_claude_response(
|
||||
messages: List[ChatCompletionMessageParam],
|
||||
api_key: str,
|
||||
callback: Callable[[str], Awaitable[None]],
|
||||
model: Llm,
|
||||
) -> str:
|
||||
|
||||
client = AsyncAnthropic(api_key=api_key)
|
||||
|
||||
# Base parameters
|
||||
model = Llm.CLAUDE_3_SONNET
|
||||
max_tokens = 4096
|
||||
max_tokens = 8192
|
||||
temperature = 0.0
|
||||
|
||||
# Translate OpenAI messages to Claude messages
|
||||
system_prompt = cast(str, messages[0].get("content"))
|
||||
claude_messages = [dict(message) for message in messages[1:]]
|
||||
|
||||
# Deep copy messages to avoid modifying the original list
|
||||
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:
|
||||
if not isinstance(message["content"], list):
|
||||
continue
|
||||
@ -96,8 +109,10 @@ async def stream_claude_response(
|
||||
# Extract base64 data and media type from data URL
|
||||
# Example base64 data URL: data:image/png;base64,iVBOR...
|
||||
image_data_url = cast(str, content["image_url"]["url"])
|
||||
media_type = image_data_url.split(";")[0].split(":")[1]
|
||||
base64_data = image_data_url.split(",")[1]
|
||||
|
||||
# Process image and split media type and data
|
||||
# so it works with Claude (under 5mb in base64 encoding)
|
||||
(media_type, base64_data) = process_image(image_data_url)
|
||||
|
||||
# Remove OpenAI parameter
|
||||
del content["image_url"]
|
||||
@ -115,6 +130,7 @@ async def stream_claude_response(
|
||||
temperature=temperature,
|
||||
system=system_prompt,
|
||||
messages=claude_messages, # type: ignore
|
||||
extra_headers={"anthropic-beta": "max-tokens-3-5-sonnet-2024-07-15"},
|
||||
) as stream:
|
||||
async for text in stream.text_stream:
|
||||
await callback(text)
|
||||
|
||||
@ -8,7 +8,7 @@ STREAM_CHUNK_SIZE = 20
|
||||
|
||||
|
||||
async def mock_completion(
|
||||
process_chunk: Callable[[str], Awaitable[None]], input_mode: InputMode
|
||||
process_chunk: Callable[[str, int], Awaitable[None]], input_mode: InputMode
|
||||
) -> str:
|
||||
code_to_return = (
|
||||
TALLY_FORM_VIDEO_PROMPT_MOCK
|
||||
@ -17,7 +17,7 @@ async def mock_completion(
|
||||
)
|
||||
|
||||
for i in range(0, len(code_to_return), STREAM_CHUNK_SIZE):
|
||||
await process_chunk(code_to_return[i : i + STREAM_CHUNK_SIZE])
|
||||
await process_chunk(code_to_return[i : i + STREAM_CHUNK_SIZE], 0)
|
||||
await asyncio.sleep(0.01)
|
||||
|
||||
if input_mode == "video":
|
||||
|
||||
581
backend/poetry.lock
generated
581
backend/poetry.lock
generated
@ -1,5 +1,115 @@
|
||||
# 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]]
|
||||
name = "anthropic"
|
||||
version = "0.18.1"
|
||||
@ -46,6 +156,36 @@ 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)"]
|
||||
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]]
|
||||
name = "beautifulsoup4"
|
||||
version = "4.12.3"
|
||||
@ -297,6 +437,92 @@ 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)"]
|
||||
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]]
|
||||
name = "fsspec"
|
||||
version = "2024.2.0"
|
||||
@ -531,6 +757,105 @@ 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"]
|
||||
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]]
|
||||
name = "nodeenv"
|
||||
version = "1.8.0"
|
||||
@ -626,79 +951,80 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "pillow"
|
||||
version = "10.2.0"
|
||||
version = "10.3.0"
|
||||
description = "Python Imaging Library (Fork)"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "pillow-10.2.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:7823bdd049099efa16e4246bdf15e5a13dbb18a51b68fa06d6c1d4d8b99a796e"},
|
||||
{file = "pillow-10.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:83b2021f2ade7d1ed556bc50a399127d7fb245e725aa0113ebd05cfe88aaf588"},
|
||||
{file = "pillow-10.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fad5ff2f13d69b7e74ce5b4ecd12cc0ec530fcee76356cac6742785ff71c452"},
|
||||
{file = "pillow-10.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da2b52b37dad6d9ec64e653637a096905b258d2fc2b984c41ae7d08b938a67e4"},
|
||||
{file = "pillow-10.2.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:47c0995fc4e7f79b5cfcab1fc437ff2890b770440f7696a3ba065ee0fd496563"},
|
||||
{file = "pillow-10.2.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:322bdf3c9b556e9ffb18f93462e5f749d3444ce081290352c6070d014c93feb2"},
|
||||
{file = "pillow-10.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:51f1a1bffc50e2e9492e87d8e09a17c5eea8409cda8d3f277eb6edc82813c17c"},
|
||||
{file = "pillow-10.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:69ffdd6120a4737710a9eee73e1d2e37db89b620f702754b8f6e62594471dee0"},
|
||||
{file = "pillow-10.2.0-cp310-cp310-win32.whl", hash = "sha256:c6dafac9e0f2b3c78df97e79af707cdc5ef8e88208d686a4847bab8266870023"},
|
||||
{file = "pillow-10.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:aebb6044806f2e16ecc07b2a2637ee1ef67a11840a66752751714a0d924adf72"},
|
||||
{file = "pillow-10.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:7049e301399273a0136ff39b84c3678e314f2158f50f517bc50285fb5ec847ad"},
|
||||
{file = "pillow-10.2.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:35bb52c37f256f662abdfa49d2dfa6ce5d93281d323a9af377a120e89a9eafb5"},
|
||||
{file = "pillow-10.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9c23f307202661071d94b5e384e1e1dc7dfb972a28a2310e4ee16103e66ddb67"},
|
||||
{file = "pillow-10.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:773efe0603db30c281521a7c0214cad7836c03b8ccff897beae9b47c0b657d61"},
|
||||
{file = "pillow-10.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11fa2e5984b949b0dd6d7a94d967743d87c577ff0b83392f17cb3990d0d2fd6e"},
|
||||
{file = "pillow-10.2.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:716d30ed977be8b37d3ef185fecb9e5a1d62d110dfbdcd1e2a122ab46fddb03f"},
|
||||
{file = "pillow-10.2.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:a086c2af425c5f62a65e12fbf385f7c9fcb8f107d0849dba5839461a129cf311"},
|
||||
{file = "pillow-10.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c8de2789052ed501dd829e9cae8d3dcce7acb4777ea4a479c14521c942d395b1"},
|
||||
{file = "pillow-10.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:609448742444d9290fd687940ac0b57fb35e6fd92bdb65386e08e99af60bf757"},
|
||||
{file = "pillow-10.2.0-cp311-cp311-win32.whl", hash = "sha256:823ef7a27cf86df6597fa0671066c1b596f69eba53efa3d1e1cb8b30f3533068"},
|
||||
{file = "pillow-10.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:1da3b2703afd040cf65ec97efea81cfba59cdbed9c11d8efc5ab09df9509fc56"},
|
||||
{file = "pillow-10.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:edca80cbfb2b68d7b56930b84a0e45ae1694aeba0541f798e908a49d66b837f1"},
|
||||
{file = "pillow-10.2.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:1b5e1b74d1bd1b78bc3477528919414874748dd363e6272efd5abf7654e68bef"},
|
||||
{file = "pillow-10.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0eae2073305f451d8ecacb5474997c08569fb4eb4ac231ffa4ad7d342fdc25ac"},
|
||||
{file = "pillow-10.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7c2286c23cd350b80d2fc9d424fc797575fb16f854b831d16fd47ceec078f2c"},
|
||||
{file = "pillow-10.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e23412b5c41e58cec602f1135c57dfcf15482013ce6e5f093a86db69646a5aa"},
|
||||
{file = "pillow-10.2.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:52a50aa3fb3acb9cf7213573ef55d31d6eca37f5709c69e6858fe3bc04a5c2a2"},
|
||||
{file = "pillow-10.2.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:127cee571038f252a552760076407f9cff79761c3d436a12af6000cd182a9d04"},
|
||||
{file = "pillow-10.2.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:8d12251f02d69d8310b046e82572ed486685c38f02176bd08baf216746eb947f"},
|
||||
{file = "pillow-10.2.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:54f1852cd531aa981bc0965b7d609f5f6cc8ce8c41b1139f6ed6b3c54ab82bfb"},
|
||||
{file = "pillow-10.2.0-cp312-cp312-win32.whl", hash = "sha256:257d8788df5ca62c980314053197f4d46eefedf4e6175bc9412f14412ec4ea2f"},
|
||||
{file = "pillow-10.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:154e939c5f0053a383de4fd3d3da48d9427a7e985f58af8e94d0b3c9fcfcf4f9"},
|
||||
{file = "pillow-10.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:f379abd2f1e3dddb2b61bc67977a6b5a0a3f7485538bcc6f39ec76163891ee48"},
|
||||
{file = "pillow-10.2.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:8373c6c251f7ef8bda6675dd6d2b3a0fcc31edf1201266b5cf608b62a37407f9"},
|
||||
{file = "pillow-10.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:870ea1ada0899fd0b79643990809323b389d4d1d46c192f97342eeb6ee0b8483"},
|
||||
{file = "pillow-10.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4b6b1e20608493548b1f32bce8cca185bf0480983890403d3b8753e44077129"},
|
||||
{file = "pillow-10.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3031709084b6e7852d00479fd1d310b07d0ba82765f973b543c8af5061cf990e"},
|
||||
{file = "pillow-10.2.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:3ff074fc97dd4e80543a3e91f69d58889baf2002b6be64347ea8cf5533188213"},
|
||||
{file = "pillow-10.2.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:cb4c38abeef13c61d6916f264d4845fab99d7b711be96c326b84df9e3e0ff62d"},
|
||||
{file = "pillow-10.2.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b1b3020d90c2d8e1dae29cf3ce54f8094f7938460fb5ce8bc5c01450b01fbaf6"},
|
||||
{file = "pillow-10.2.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:170aeb00224ab3dc54230c797f8404507240dd868cf52066f66a41b33169bdbe"},
|
||||
{file = "pillow-10.2.0-cp38-cp38-win32.whl", hash = "sha256:c4225f5220f46b2fde568c74fca27ae9771536c2e29d7c04f4fb62c83275ac4e"},
|
||||
{file = "pillow-10.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:0689b5a8c5288bc0504d9fcee48f61a6a586b9b98514d7d29b840143d6734f39"},
|
||||
{file = "pillow-10.2.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:b792a349405fbc0163190fde0dc7b3fef3c9268292586cf5645598b48e63dc67"},
|
||||
{file = "pillow-10.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c570f24be1e468e3f0ce7ef56a89a60f0e05b30a3669a459e419c6eac2c35364"},
|
||||
{file = "pillow-10.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8ecd059fdaf60c1963c58ceb8997b32e9dc1b911f5da5307aab614f1ce5c2fb"},
|
||||
{file = "pillow-10.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c365fd1703040de1ec284b176d6af5abe21b427cb3a5ff68e0759e1e313a5e7e"},
|
||||
{file = "pillow-10.2.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:70c61d4c475835a19b3a5aa42492409878bbca7438554a1f89d20d58a7c75c01"},
|
||||
{file = "pillow-10.2.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:b6f491cdf80ae540738859d9766783e3b3c8e5bd37f5dfa0b76abdecc5081f13"},
|
||||
{file = "pillow-10.2.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9d189550615b4948f45252d7f005e53c2040cea1af5b60d6f79491a6e147eef7"},
|
||||
{file = "pillow-10.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:49d9ba1ed0ef3e061088cd1e7538a0759aab559e2e0a80a36f9fd9d8c0c21591"},
|
||||
{file = "pillow-10.2.0-cp39-cp39-win32.whl", hash = "sha256:babf5acfede515f176833ed6028754cbcd0d206f7f614ea3447d67c33be12516"},
|
||||
{file = "pillow-10.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:0304004f8067386b477d20a518b50f3fa658a28d44e4116970abfcd94fac34a8"},
|
||||
{file = "pillow-10.2.0-cp39-cp39-win_arm64.whl", hash = "sha256:0fb3e7fc88a14eacd303e90481ad983fd5b69c761e9e6ef94c983f91025da869"},
|
||||
{file = "pillow-10.2.0-pp310-pypy310_pp73-macosx_10_10_x86_64.whl", hash = "sha256:322209c642aabdd6207517e9739c704dc9f9db943015535783239022002f054a"},
|
||||
{file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3eedd52442c0a5ff4f887fab0c1c0bb164d8635b32c894bc1faf4c618dd89df2"},
|
||||
{file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb28c753fd5eb3dd859b4ee95de66cc62af91bcff5db5f2571d32a520baf1f04"},
|
||||
{file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:33870dc4653c5017bf4c8873e5488d8f8d5f8935e2f1fb9a2208c47cdd66efd2"},
|
||||
{file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:3c31822339516fb3c82d03f30e22b1d038da87ef27b6a78c9549888f8ceda39a"},
|
||||
{file = "pillow-10.2.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a2b56ba36e05f973d450582fb015594aaa78834fefe8dfb8fcd79b93e64ba4c6"},
|
||||
{file = "pillow-10.2.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:d8e6aeb9201e655354b3ad049cb77d19813ad4ece0df1249d3c793de3774f8c7"},
|
||||
{file = "pillow-10.2.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:2247178effb34a77c11c0e8ac355c7a741ceca0a732b27bf11e747bbc950722f"},
|
||||
{file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15587643b9e5eb26c48e49a7b33659790d28f190fc514a322d55da2fb5c2950e"},
|
||||
{file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753cd8f2086b2b80180d9b3010dd4ed147efc167c90d3bf593fe2af21265e5a5"},
|
||||
{file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:7c8f97e8e7a9009bcacbe3766a36175056c12f9a44e6e6f2d5caad06dcfbf03b"},
|
||||
{file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d1b35bcd6c5543b9cb547dee3150c93008f8dd0f1fef78fc0cd2b141c5baf58a"},
|
||||
{file = "pillow-10.2.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:fe4c15f6c9285dc54ce6553a3ce908ed37c8f3825b5a51a15c91442bb955b868"},
|
||||
{file = "pillow-10.2.0.tar.gz", hash = "sha256:e87f0b2c78157e12d7686b27d63c070fd65d994e8ddae6f328e0dcf4a0cd007e"},
|
||||
{file = "pillow-10.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:90b9e29824800e90c84e4022dd5cc16eb2d9605ee13f05d47641eb183cd73d45"},
|
||||
{file = "pillow-10.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a2c405445c79c3f5a124573a051062300936b0281fee57637e706453e452746c"},
|
||||
{file = "pillow-10.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78618cdbccaa74d3f88d0ad6cb8ac3007f1a6fa5c6f19af64b55ca170bfa1edf"},
|
||||
{file = "pillow-10.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:261ddb7ca91fcf71757979534fb4c128448b5b4c55cb6152d280312062f69599"},
|
||||
{file = "pillow-10.3.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:ce49c67f4ea0609933d01c0731b34b8695a7a748d6c8d186f95e7d085d2fe475"},
|
||||
{file = "pillow-10.3.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:b14f16f94cbc61215115b9b1236f9c18403c15dd3c52cf629072afa9d54c1cbf"},
|
||||
{file = "pillow-10.3.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d33891be6df59d93df4d846640f0e46f1a807339f09e79a8040bc887bdcd7ed3"},
|
||||
{file = "pillow-10.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b50811d664d392f02f7761621303eba9d1b056fb1868c8cdf4231279645c25f5"},
|
||||
{file = "pillow-10.3.0-cp310-cp310-win32.whl", hash = "sha256:ca2870d5d10d8726a27396d3ca4cf7976cec0f3cb706debe88e3a5bd4610f7d2"},
|
||||
{file = "pillow-10.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:f0d0591a0aeaefdaf9a5e545e7485f89910c977087e7de2b6c388aec32011e9f"},
|
||||
{file = "pillow-10.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:ccce24b7ad89adb5a1e34a6ba96ac2530046763912806ad4c247356a8f33a67b"},
|
||||
{file = "pillow-10.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:5f77cf66e96ae734717d341c145c5949c63180842a545c47a0ce7ae52ca83795"},
|
||||
{file = "pillow-10.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e4b878386c4bf293578b48fc570b84ecfe477d3b77ba39a6e87150af77f40c57"},
|
||||
{file = "pillow-10.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdcbb4068117dfd9ce0138d068ac512843c52295ed996ae6dd1faf537b6dbc27"},
|
||||
{file = "pillow-10.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9797a6c8fe16f25749b371c02e2ade0efb51155e767a971c61734b1bf6293994"},
|
||||
{file = "pillow-10.3.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:9e91179a242bbc99be65e139e30690e081fe6cb91a8e77faf4c409653de39451"},
|
||||
{file = "pillow-10.3.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:1b87bd9d81d179bd8ab871603bd80d8645729939f90b71e62914e816a76fc6bd"},
|
||||
{file = "pillow-10.3.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:81d09caa7b27ef4e61cb7d8fbf1714f5aec1c6b6c5270ee53504981e6e9121ad"},
|
||||
{file = "pillow-10.3.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:048ad577748b9fa4a99a0548c64f2cb8d672d5bf2e643a739ac8faff1164238c"},
|
||||
{file = "pillow-10.3.0-cp311-cp311-win32.whl", hash = "sha256:7161ec49ef0800947dc5570f86568a7bb36fa97dd09e9827dc02b718c5643f09"},
|
||||
{file = "pillow-10.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:8eb0908e954d093b02a543dc963984d6e99ad2b5e36503d8a0aaf040505f747d"},
|
||||
{file = "pillow-10.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:4e6f7d1c414191c1199f8996d3f2282b9ebea0945693fb67392c75a3a320941f"},
|
||||
{file = "pillow-10.3.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:e46f38133e5a060d46bd630faa4d9fa0202377495df1f068a8299fd78c84de84"},
|
||||
{file = "pillow-10.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:50b8eae8f7334ec826d6eeffaeeb00e36b5e24aa0b9df322c247539714c6df19"},
|
||||
{file = "pillow-10.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d3bea1c75f8c53ee4d505c3e67d8c158ad4df0d83170605b50b64025917f338"},
|
||||
{file = "pillow-10.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19aeb96d43902f0a783946a0a87dbdad5c84c936025b8419da0a0cd7724356b1"},
|
||||
{file = "pillow-10.3.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:74d28c17412d9caa1066f7a31df8403ec23d5268ba46cd0ad2c50fb82ae40462"},
|
||||
{file = "pillow-10.3.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:ff61bfd9253c3915e6d41c651d5f962da23eda633cf02262990094a18a55371a"},
|
||||
{file = "pillow-10.3.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d886f5d353333b4771d21267c7ecc75b710f1a73d72d03ca06df49b09015a9ef"},
|
||||
{file = "pillow-10.3.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4b5ec25d8b17217d635f8935dbc1b9aa5907962fae29dff220f2659487891cd3"},
|
||||
{file = "pillow-10.3.0-cp312-cp312-win32.whl", hash = "sha256:51243f1ed5161b9945011a7360e997729776f6e5d7005ba0c6879267d4c5139d"},
|
||||
{file = "pillow-10.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:412444afb8c4c7a6cc11a47dade32982439925537e483be7c0ae0cf96c4f6a0b"},
|
||||
{file = "pillow-10.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:798232c92e7665fe82ac085f9d8e8ca98826f8e27859d9a96b41d519ecd2e49a"},
|
||||
{file = "pillow-10.3.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:4eaa22f0d22b1a7e93ff0a596d57fdede2e550aecffb5a1ef1106aaece48e96b"},
|
||||
{file = "pillow-10.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cd5e14fbf22a87321b24c88669aad3a51ec052eb145315b3da3b7e3cc105b9a2"},
|
||||
{file = "pillow-10.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1530e8f3a4b965eb6a7785cf17a426c779333eb62c9a7d1bbcf3ffd5bf77a4aa"},
|
||||
{file = "pillow-10.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d512aafa1d32efa014fa041d38868fda85028e3f930a96f85d49c7d8ddc0383"},
|
||||
{file = "pillow-10.3.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:339894035d0ede518b16073bdc2feef4c991ee991a29774b33e515f1d308e08d"},
|
||||
{file = "pillow-10.3.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:aa7e402ce11f0885305bfb6afb3434b3cd8f53b563ac065452d9d5654c7b86fd"},
|
||||
{file = "pillow-10.3.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:0ea2a783a2bdf2a561808fe4a7a12e9aa3799b701ba305de596bc48b8bdfce9d"},
|
||||
{file = "pillow-10.3.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:c78e1b00a87ce43bb37642c0812315b411e856a905d58d597750eb79802aaaa3"},
|
||||
{file = "pillow-10.3.0-cp38-cp38-win32.whl", hash = "sha256:72d622d262e463dfb7595202d229f5f3ab4b852289a1cd09650362db23b9eb0b"},
|
||||
{file = "pillow-10.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:2034f6759a722da3a3dbd91a81148cf884e91d1b747992ca288ab88c1de15999"},
|
||||
{file = "pillow-10.3.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:2ed854e716a89b1afcedea551cd85f2eb2a807613752ab997b9974aaa0d56936"},
|
||||
{file = "pillow-10.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:dc1a390a82755a8c26c9964d457d4c9cbec5405896cba94cf51f36ea0d855002"},
|
||||
{file = "pillow-10.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4203efca580f0dd6f882ca211f923168548f7ba334c189e9eab1178ab840bf60"},
|
||||
{file = "pillow-10.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3102045a10945173d38336f6e71a8dc71bcaeed55c3123ad4af82c52807b9375"},
|
||||
{file = "pillow-10.3.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:6fb1b30043271ec92dc65f6d9f0b7a830c210b8a96423074b15c7bc999975f57"},
|
||||
{file = "pillow-10.3.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:1dfc94946bc60ea375cc39cff0b8da6c7e5f8fcdc1d946beb8da5c216156ddd8"},
|
||||
{file = "pillow-10.3.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b09b86b27a064c9624d0a6c54da01c1beaf5b6cadfa609cf63789b1d08a797b9"},
|
||||
{file = "pillow-10.3.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d3b2348a78bc939b4fed6552abfd2e7988e0f81443ef3911a4b8498ca084f6eb"},
|
||||
{file = "pillow-10.3.0-cp39-cp39-win32.whl", hash = "sha256:45ebc7b45406febf07fef35d856f0293a92e7417ae7933207e90bf9090b70572"},
|
||||
{file = "pillow-10.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:0ba26351b137ca4e0db0342d5d00d2e355eb29372c05afd544ebf47c0956ffeb"},
|
||||
{file = "pillow-10.3.0-cp39-cp39-win_arm64.whl", hash = "sha256:50fd3f6b26e3441ae07b7c979309638b72abc1a25da31a81a7fbd9495713ef4f"},
|
||||
{file = "pillow-10.3.0-pp310-pypy310_pp73-macosx_10_10_x86_64.whl", hash = "sha256:6b02471b72526ab8a18c39cb7967b72d194ec53c1fd0a70b050565a0f366d355"},
|
||||
{file = "pillow-10.3.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:8ab74c06ffdab957d7670c2a5a6e1a70181cd10b727cd788c4dd9005b6a8acd9"},
|
||||
{file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:048eeade4c33fdf7e08da40ef402e748df113fd0b4584e32c4af74fe78baaeb2"},
|
||||
{file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e2ec1e921fd07c7cda7962bad283acc2f2a9ccc1b971ee4b216b75fad6f0463"},
|
||||
{file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:4c8e73e99da7db1b4cad7f8d682cf6abad7844da39834c288fbfa394a47bbced"},
|
||||
{file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:16563993329b79513f59142a6b02055e10514c1a8e86dca8b48a893e33cf91e3"},
|
||||
{file = "pillow-10.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:dd78700f5788ae180b5ee8902c6aea5a5726bac7c364b202b4b3e3ba2d293170"},
|
||||
{file = "pillow-10.3.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:aff76a55a8aa8364d25400a210a65ff59d0168e0b4285ba6bf2bd83cf675ba32"},
|
||||
{file = "pillow-10.3.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:b7bc2176354defba3edc2b9a777744462da2f8e921fbaf61e52acb95bafa9828"},
|
||||
{file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:793b4e24db2e8742ca6423d3fde8396db336698c55cd34b660663ee9e45ed37f"},
|
||||
{file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d93480005693d247f8346bc8ee28c72a2191bdf1f6b5db469c096c0c867ac015"},
|
||||
{file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c83341b89884e2b2e55886e8fbbf37c3fa5efd6c8907124aeb72f285ae5696e5"},
|
||||
{file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1a1d1915db1a4fdb2754b9de292642a39a7fb28f1736699527bb649484fb966a"},
|
||||
{file = "pillow-10.3.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a0eaa93d054751ee9964afa21c06247779b90440ca41d184aeb5d410f20ff591"},
|
||||
{file = "pillow-10.3.0.tar.gz", hash = "sha256:9d2455fbf44c914840c793e89aa82d0e1763a14253a000743719ae5946814b2d"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
@ -902,6 +1228,7 @@ files = [
|
||||
{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_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-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"},
|
||||
@ -1170,6 +1497,17 @@ notebook = ["ipywidgets (>=6)"]
|
||||
slack = ["slack-sdk"]
|
||||
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]]
|
||||
name = "typing-extensions"
|
||||
version = "4.10.0"
|
||||
@ -1318,7 +1656,110 @@ files = [
|
||||
{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]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.10"
|
||||
content-hash = "274fed55cf4a2f4e9954b3d196c103b72225409c6050759a939f0ca197ae3f79"
|
||||
content-hash = "9cec287b530dcda39515e7c76eed76d88eb0c725f37148af5ef93083cfb46ad8"
|
||||
|
||||
@ -1,10 +1,12 @@
|
||||
from typing import List, NoReturn, Union
|
||||
|
||||
from typing import Union
|
||||
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.screenshot_system_prompts import SYSTEM_PROMPTS
|
||||
from prompts.types import Stack
|
||||
from video.utils import assemble_claude_prompt_video
|
||||
|
||||
|
||||
USER_PROMPT = """
|
||||
@ -16,9 +18,65 @@ 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(
|
||||
code: str, stack: Stack, result_image_data_url: Union[str, None] = None
|
||||
) -> List[ChatCompletionMessageParam]:
|
||||
code: str, stack: Stack
|
||||
) -> list[ChatCompletionMessageParam]:
|
||||
system_content = IMPORTED_CODE_SYSTEM_PROMPTS[stack]
|
||||
|
||||
user_content = (
|
||||
@ -26,15 +84,12 @@ def assemble_imported_code_prompt(
|
||||
if stack != "svg"
|
||||
else "Here is the code of the SVG: " + code
|
||||
)
|
||||
|
||||
return [
|
||||
{
|
||||
"role": "system",
|
||||
"content": system_content,
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": user_content,
|
||||
},
|
||||
"content": system_content + "\n " + user_content,
|
||||
}
|
||||
]
|
||||
# TODO: Use result_image_data_url
|
||||
|
||||
@ -43,11 +98,11 @@ def assemble_prompt(
|
||||
image_data_url: str,
|
||||
stack: Stack,
|
||||
result_image_data_url: Union[str, None] = None,
|
||||
) -> List[ChatCompletionMessageParam]:
|
||||
) -> list[ChatCompletionMessageParam]:
|
||||
system_content = SYSTEM_PROMPTS[stack]
|
||||
user_prompt = USER_PROMPT if stack != "svg" else SVG_USER_PROMPT
|
||||
|
||||
user_content: List[ChatCompletionContentPartParam] = [
|
||||
user_content: list[ChatCompletionContentPartParam] = [
|
||||
{
|
||||
"type": "image_url",
|
||||
"image_url": {"url": image_data_url, "detail": "high"},
|
||||
|
||||
@ -18,6 +18,22 @@ Return only the full code in <html></html> tags.
|
||||
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 = """
|
||||
You are an expert React/Tailwind developer
|
||||
|
||||
@ -128,6 +144,7 @@ Do not include markdown "```" or "```svg" at the start or end.
|
||||
|
||||
IMPORTED_CODE_SYSTEM_PROMPTS = SystemPrompts(
|
||||
html_tailwind=IMPORTED_CODE_TAILWIND_SYSTEM_PROMPT,
|
||||
html_css=IMPORTED_CODE_HTML_CSS_SYSTEM_PROMPT,
|
||||
react_tailwind=IMPORTED_CODE_REACT_TAILWIND_SYSTEM_PROMPT,
|
||||
bootstrap=IMPORTED_CODE_BOOTSTRAP_SYSTEM_PROMPT,
|
||||
ionic_tailwind=IMPORTED_CODE_IONIC_TAILWIND_SYSTEM_PROMPT,
|
||||
|
||||
@ -26,6 +26,30 @@ Return only the full code in <html></html> tags.
|
||||
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 = """
|
||||
You are an expert Bootstrap developer
|
||||
You take screenshots of a reference web page from the user, and then build single page apps
|
||||
@ -176,6 +200,7 @@ Do not include markdown "```" or "```svg" at the start or end.
|
||||
|
||||
|
||||
SYSTEM_PROMPTS = SystemPrompts(
|
||||
html_css=HTML_CSS_SYSTEM_PROMPT,
|
||||
html_tailwind=HTML_TAILWIND_SYSTEM_PROMPT,
|
||||
react_tailwind=REACT_TAILWIND_SYSTEM_PROMPT,
|
||||
bootstrap=BOOTSTRAP_SYSTEM_PROMPT,
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
from llm import Llm
|
||||
from prompts import assemble_imported_code_prompt, assemble_prompt
|
||||
|
||||
TAILWIND_SYSTEM_PROMPT = """
|
||||
@ -25,6 +26,30 @@ Return only the full code in <html></html> tags.
|
||||
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 = """
|
||||
You are an expert Bootstrap developer
|
||||
You take screenshots of a reference web page from the user, and then build single page apps
|
||||
@ -189,6 +214,22 @@ Return only the full code in <html></html> tags.
|
||||
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 = """
|
||||
You are an expert React/Tailwind developer
|
||||
|
||||
@ -314,6 +355,12 @@ def test_prompts():
|
||||
assert tailwind_prompt[0].get("content") == TAILWIND_SYSTEM_PROMPT
|
||||
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(
|
||||
"image_data_url", "react_tailwind", "result_image_data_url"
|
||||
)
|
||||
@ -344,54 +391,81 @@ def test_prompts():
|
||||
|
||||
|
||||
def test_imported_code_prompts():
|
||||
tailwind_prompt = assemble_imported_code_prompt(
|
||||
"code", "html_tailwind", "result_image_data_url"
|
||||
)
|
||||
code = "Sample code"
|
||||
|
||||
tailwind_prompt = assemble_imported_code_prompt(code, "html_tailwind")
|
||||
expected_tailwind_prompt = [
|
||||
{"role": "system", "content": IMPORTED_CODE_TAILWIND_SYSTEM_PROMPT},
|
||||
{"role": "user", "content": "Here is the code of the app: code"},
|
||||
{
|
||||
"role": "system",
|
||||
"content": IMPORTED_CODE_TAILWIND_SYSTEM_PROMPT
|
||||
+ "\n Here is the code of the app: "
|
||||
+ code,
|
||||
}
|
||||
]
|
||||
assert tailwind_prompt == expected_tailwind_prompt
|
||||
|
||||
react_tailwind_prompt = assemble_imported_code_prompt(
|
||||
"code", "react_tailwind", "result_image_data_url"
|
||||
)
|
||||
html_css_prompt = assemble_imported_code_prompt(code, "html_css")
|
||||
expected_html_css_prompt = [
|
||||
{
|
||||
"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 = [
|
||||
{"role": "system", "content": IMPORTED_CODE_REACT_TAILWIND_SYSTEM_PROMPT},
|
||||
{"role": "user", "content": "Here is the code of the app: code"},
|
||||
{
|
||||
"role": "system",
|
||||
"content": IMPORTED_CODE_REACT_TAILWIND_SYSTEM_PROMPT
|
||||
+ "\n Here is the code of the app: "
|
||||
+ code,
|
||||
}
|
||||
]
|
||||
assert react_tailwind_prompt == expected_react_tailwind_prompt
|
||||
|
||||
bootstrap_prompt = assemble_imported_code_prompt(
|
||||
"code", "bootstrap", "result_image_data_url"
|
||||
)
|
||||
bootstrap_prompt = assemble_imported_code_prompt(code, "bootstrap")
|
||||
expected_bootstrap_prompt = [
|
||||
{"role": "system", "content": IMPORTED_CODE_BOOTSTRAP_SYSTEM_PROMPT},
|
||||
{"role": "user", "content": "Here is the code of the app: code"},
|
||||
{
|
||||
"role": "system",
|
||||
"content": IMPORTED_CODE_BOOTSTRAP_SYSTEM_PROMPT
|
||||
+ "\n Here is the code of the app: "
|
||||
+ code,
|
||||
}
|
||||
]
|
||||
assert bootstrap_prompt == expected_bootstrap_prompt
|
||||
|
||||
ionic_tailwind = assemble_imported_code_prompt(
|
||||
"code", "ionic_tailwind", "result_image_data_url"
|
||||
)
|
||||
ionic_tailwind = assemble_imported_code_prompt(code, "ionic_tailwind")
|
||||
expected_ionic_tailwind = [
|
||||
{"role": "system", "content": IMPORTED_CODE_IONIC_TAILWIND_SYSTEM_PROMPT},
|
||||
{"role": "user", "content": "Here is the code of the app: code"},
|
||||
{
|
||||
"role": "system",
|
||||
"content": IMPORTED_CODE_IONIC_TAILWIND_SYSTEM_PROMPT
|
||||
+ "\n Here is the code of the app: "
|
||||
+ code,
|
||||
}
|
||||
]
|
||||
assert ionic_tailwind == expected_ionic_tailwind
|
||||
|
||||
vue_tailwind = assemble_imported_code_prompt(
|
||||
"code", "vue_tailwind", "result_image_data_url"
|
||||
)
|
||||
vue_tailwind = assemble_imported_code_prompt(code, "vue_tailwind")
|
||||
expected_vue_tailwind = [
|
||||
{"role": "system", "content": IMPORTED_CODE_VUE_TAILWIND_PROMPT},
|
||||
{"role": "user", "content": "Here is the code of the app: code"},
|
||||
{
|
||||
"role": "system",
|
||||
"content": IMPORTED_CODE_VUE_TAILWIND_PROMPT
|
||||
+ "\n Here is the code of the app: "
|
||||
+ code,
|
||||
}
|
||||
]
|
||||
assert vue_tailwind == expected_vue_tailwind
|
||||
|
||||
svg = assemble_imported_code_prompt("code", "svg", "result_image_data_url")
|
||||
svg = assemble_imported_code_prompt(code, "svg")
|
||||
expected_svg = [
|
||||
{"role": "system", "content": IMPORTED_CODE_SVG_SYSTEM_PROMPT},
|
||||
{"role": "user", "content": "Here is the code of the SVG: code"},
|
||||
{
|
||||
"role": "system",
|
||||
"content": IMPORTED_CODE_SVG_SYSTEM_PROMPT
|
||||
+ "\n Here is the code of the SVG: "
|
||||
+ code,
|
||||
}
|
||||
]
|
||||
assert svg == expected_svg
|
||||
|
||||
@ -2,6 +2,7 @@ from typing import Literal, TypedDict
|
||||
|
||||
|
||||
class SystemPrompts(TypedDict):
|
||||
html_css: str
|
||||
html_tailwind: str
|
||||
react_tailwind: str
|
||||
bootstrap: str
|
||||
@ -11,6 +12,7 @@ class SystemPrompts(TypedDict):
|
||||
|
||||
|
||||
Stack = Literal[
|
||||
"html_css",
|
||||
"html_tailwind",
|
||||
"react_tailwind",
|
||||
"bootstrap",
|
||||
|
||||
@ -17,6 +17,9 @@ httpx = "^0.25.1"
|
||||
pre-commit = "^3.6.2"
|
||||
anthropic = "^0.18.0"
|
||||
moviepy = "^1.0.3"
|
||||
pillow = "^10.3.0"
|
||||
types-pillow = "^10.2.0.20240520"
|
||||
aiohttp = "^3.9.5"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
pytest = "^7.4.3"
|
||||
|
||||
@ -1,8 +1,18 @@
|
||||
import os
|
||||
import asyncio
|
||||
from dataclasses import dataclass
|
||||
import traceback
|
||||
from fastapi import APIRouter, WebSocket
|
||||
import openai
|
||||
from config import ANTHROPIC_API_KEY, IS_PROD, SHOULD_MOCK_AI_RESPONSE
|
||||
from codegen.utils import extract_html_content
|
||||
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 llm import (
|
||||
Llm,
|
||||
@ -11,76 +21,97 @@ from llm import (
|
||||
stream_claude_response_native,
|
||||
stream_openai_response,
|
||||
)
|
||||
from openai.types.chat import ChatCompletionMessageParam
|
||||
from fs_logging.core import write_logs
|
||||
from mock_llm import mock_completion
|
||||
from typing import Dict, List, cast, get_args
|
||||
from image_generation import create_alt_url_mapping, generate_images
|
||||
from prompts import assemble_imported_code_prompt, assemble_prompt
|
||||
from datetime import datetime
|
||||
import json
|
||||
from typing import Any, Callable, Coroutine, Dict, List, Literal, cast, get_args
|
||||
from image_generation.core import generate_images
|
||||
from prompts import create_prompt
|
||||
from prompts.claude_prompts import VIDEO_PROMPT
|
||||
from prompts.types import Stack
|
||||
|
||||
# from utils import pprint_prompt
|
||||
from video.utils import extract_tag_content, assemble_claude_prompt_video
|
||||
from ws.constants import APP_ERROR_WEB_SOCKET_CODE # type: ignore
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
def write_logs(prompt_messages: List[ChatCompletionMessageParam], completion: str):
|
||||
# Get the logs path from environment, default to the current working directory
|
||||
logs_path = os.environ.get("LOGS_PATH", os.getcwd())
|
||||
|
||||
# Create run_logs directory if it doesn't exist within the specified logs path
|
||||
logs_directory = os.path.join(logs_path, "run_logs")
|
||||
if not os.path.exists(logs_directory):
|
||||
os.makedirs(logs_directory)
|
||||
|
||||
print("Writing to logs directory:", logs_directory)
|
||||
|
||||
# Generate a unique filename using the current timestamp within the logs directory
|
||||
filename = datetime.now().strftime(f"{logs_directory}/messages_%Y%m%d_%H%M%S.json")
|
||||
|
||||
# Write the messages dict into a new file for each run
|
||||
with open(filename, "w") as f:
|
||||
f.write(json.dumps({"prompt": prompt_messages, "completion": completion}))
|
||||
# Auto-upgrade usage of older models
|
||||
def auto_upgrade_model(code_generation_model: Llm) -> Llm:
|
||||
if code_generation_model in {Llm.GPT_4_VISION, Llm.GPT_4_TURBO_2024_04_09}:
|
||||
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
|
||||
|
||||
|
||||
@router.websocket("/generate-code")
|
||||
async def stream_code(websocket: WebSocket):
|
||||
await websocket.accept()
|
||||
# Generate images, if needed
|
||||
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
|
||||
|
||||
print("Incoming websocket connection...")
|
||||
if replicate_api_key:
|
||||
image_generation_model = "flux"
|
||||
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
|
||||
|
||||
async def throw_error(
|
||||
message: str,
|
||||
):
|
||||
await websocket.send_json({"type": "error", "value": message})
|
||||
await websocket.close(APP_ERROR_WEB_SOCKET_CODE)
|
||||
print("Generating images with model: ", image_generation_model)
|
||||
|
||||
# TODO: Are the values always strings?
|
||||
params: Dict[str, str] = await websocket.receive_json()
|
||||
return await generate_images(
|
||||
completion,
|
||||
api_key=api_key,
|
||||
base_url=openai_base_url,
|
||||
image_cache=image_cache,
|
||||
model=image_generation_model,
|
||||
)
|
||||
|
||||
print("Received params")
|
||||
|
||||
# Read the code config settings from the request. Fall back to default if not provided.
|
||||
generated_code_config = ""
|
||||
if "generatedCodeConfig" in params and params["generatedCodeConfig"]:
|
||||
generated_code_config = params["generatedCodeConfig"]
|
||||
if not generated_code_config in get_args(Stack):
|
||||
@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}")
|
||||
return
|
||||
# Cast the variable to the Stack type
|
||||
valid_stack = cast(Stack, 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 not input_mode in get_args(InputMode):
|
||||
if input_mode not 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
|
||||
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.
|
||||
@ -89,173 +120,224 @@ async def stream_code(websocket: WebSocket):
|
||||
)
|
||||
try:
|
||||
code_generation_model = convert_frontend_str_to_llm(code_generation_model_str)
|
||||
except:
|
||||
except ValueError:
|
||||
await throw_error(f"Invalid model: {code_generation_model_str}")
|
||||
raise Exception(f"Invalid model: {code_generation_model_str}")
|
||||
exact_llm_version = None
|
||||
raise ValueError(f"Invalid model: {code_generation_model_str}")
|
||||
|
||||
print(
|
||||
f"Generating {generated_code_config} code for uploaded {input_mode} using {code_generation_model} model..."
|
||||
openai_api_key = get_from_settings_dialog_or_env(
|
||||
params, "openAiApiKey", OPENAI_API_KEY
|
||||
)
|
||||
|
||||
# Get the OpenAI API key from the request. Fall back to environment variable if not provided.
|
||||
# If neither is provided, we throw an error.
|
||||
openai_api_key = None
|
||||
if params["openAiApiKey"]:
|
||||
openai_api_key = params["openAiApiKey"]
|
||||
print("Using OpenAI API key from client-side settings dialog")
|
||||
else:
|
||||
openai_api_key = os.environ.get("OPENAI_API_KEY")
|
||||
if openai_api_key:
|
||||
print("Using OpenAI API key from environment variable")
|
||||
|
||||
if not openai_api_key and (
|
||||
code_generation_model == Llm.GPT_4_VISION
|
||||
or code_generation_model == Llm.GPT_4_TURBO_2024_04_09
|
||||
or code_generation_model == Llm.GPT_4O_2024_05_13
|
||||
):
|
||||
print("OpenAI API key not found")
|
||||
await throw_error(
|
||||
"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."
|
||||
# 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
|
||||
)
|
||||
return
|
||||
|
||||
# Get the OpenAI Base URL from the request. Fall back to environment variable if not provided.
|
||||
openai_base_url = None
|
||||
# Base URL for OpenAI API
|
||||
openai_base_url: str | None = 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 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 = (
|
||||
params["isImageGenerationEnabled"]
|
||||
if "isImageGenerationEnabled" in params
|
||||
else True
|
||||
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,
|
||||
)
|
||||
|
||||
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})
|
||||
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")
|
||||
async def stream_code(websocket: WebSocket):
|
||||
await websocket.accept()
|
||||
print("Incoming websocket connection...")
|
||||
|
||||
## Communication protocol setup
|
||||
async def throw_error(
|
||||
message: str,
|
||||
):
|
||||
print(message)
|
||||
await websocket.send_json({"type": "error", "value": message})
|
||||
await websocket.close(APP_ERROR_WEB_SOCKET_CODE)
|
||||
|
||||
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?
|
||||
params: dict[str, str] = await websocket.receive_json()
|
||||
print("Received params")
|
||||
|
||||
extracted_params = await extract_params(params, throw_error)
|
||||
stack = extracted_params.stack
|
||||
input_mode = extracted_params.input_mode
|
||||
code_generation_model = extracted_params.code_generation_model
|
||||
openai_api_key = extracted_params.openai_api_key
|
||||
openai_base_url = extracted_params.openai_base_url
|
||||
anthropic_api_key = extracted_params.anthropic_api_key
|
||||
should_generate_images = extracted_params.should_generate_images
|
||||
|
||||
# Auto-upgrade usage of older models
|
||||
code_generation_model = auto_upgrade_model(code_generation_model)
|
||||
|
||||
print(
|
||||
f"Generating {stack} code in {input_mode} mode using {code_generation_model}..."
|
||||
)
|
||||
|
||||
for i in range(NUM_VARIANTS):
|
||||
await send_message("status", "Generating code...", i)
|
||||
|
||||
### Prompt creation
|
||||
|
||||
# Image cache for updates so that we don't have to regenerate images
|
||||
image_cache: Dict[str, str] = {}
|
||||
|
||||
# If this generation started off with imported code, we need to assemble the prompt differently
|
||||
if params.get("isImportedFromCode") and params["isImportedFromCode"]:
|
||||
original_imported_code = params["history"][0]
|
||||
prompt_messages = assemble_imported_code_prompt(
|
||||
original_imported_code, valid_stack
|
||||
)
|
||||
for index, text in enumerate(params["history"][1:]):
|
||||
if index % 2 == 0:
|
||||
message: ChatCompletionMessageParam = {
|
||||
"role": "user",
|
||||
"content": text,
|
||||
}
|
||||
else:
|
||||
message: ChatCompletionMessageParam = {
|
||||
"role": "assistant",
|
||||
"content": text,
|
||||
}
|
||||
prompt_messages.append(message)
|
||||
else:
|
||||
# Assemble the prompt
|
||||
try:
|
||||
if params.get("resultImage") and params["resultImage"]:
|
||||
prompt_messages = assemble_prompt(
|
||||
params["image"], valid_stack, params["resultImage"]
|
||||
)
|
||||
else:
|
||||
prompt_messages = assemble_prompt(params["image"], valid_stack)
|
||||
prompt_messages, image_cache = await create_prompt(params, stack, input_mode)
|
||||
except:
|
||||
await websocket.send_json(
|
||||
{
|
||||
"type": "error",
|
||||
"value": "Error assembling prompt. Contact support at support@picoapps.xyz",
|
||||
}
|
||||
await throw_error(
|
||||
"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)
|
||||
raise
|
||||
|
||||
# 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:
|
||||
completion = await mock_completion(
|
||||
process_chunk, input_mode=validated_input_mode
|
||||
)
|
||||
completions = [await mock_completion(process_chunk, input_mode=input_mode)]
|
||||
else:
|
||||
try:
|
||||
if validated_input_mode == "video":
|
||||
if not ANTHROPIC_API_KEY:
|
||||
if input_mode == "video":
|
||||
if not anthropic_api_key:
|
||||
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"
|
||||
"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"
|
||||
)
|
||||
raise Exception("No Anthropic key")
|
||||
|
||||
completion = await stream_claude_response_native(
|
||||
completions = [
|
||||
await stream_claude_response_native(
|
||||
system_prompt=VIDEO_PROMPT,
|
||||
messages=prompt_messages, # type: ignore
|
||||
api_key=ANTHROPIC_API_KEY,
|
||||
callback=lambda x: process_chunk(x),
|
||||
api_key=anthropic_api_key,
|
||||
callback=lambda x: process_chunk(x, 0),
|
||||
model=Llm.CLAUDE_3_OPUS,
|
||||
include_thinking=True,
|
||||
)
|
||||
exact_llm_version = Llm.CLAUDE_3_OPUS
|
||||
elif code_generation_model == Llm.CLAUDE_3_SONNET:
|
||||
if not ANTHROPIC_API_KEY:
|
||||
await throw_error(
|
||||
"No Anthropic API key found. Please add the environment variable ANTHROPIC_API_KEY to backend/.env"
|
||||
)
|
||||
raise Exception("No Anthropic key")
|
||||
|
||||
completion = await stream_claude_response(
|
||||
prompt_messages, # type: ignore
|
||||
api_key=ANTHROPIC_API_KEY,
|
||||
callback=lambda x: process_chunk(x),
|
||||
)
|
||||
exact_llm_version = code_generation_model
|
||||
]
|
||||
else:
|
||||
completion = await stream_openai_response(
|
||||
prompt_messages, # type: ignore
|
||||
|
||||
# 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(
|
||||
"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."
|
||||
)
|
||||
raise Exception("No OpenAI or Anthropic key")
|
||||
|
||||
tasks: List[Coroutine[Any, Any, str]] = []
|
||||
for index, model in enumerate(variant_models):
|
||||
if model == "openai":
|
||||
if openai_api_key is None:
|
||||
await throw_error("OpenAI API key is missing.")
|
||||
raise Exception("OpenAI API key is missing.")
|
||||
|
||||
tasks.append(
|
||||
stream_openai_response(
|
||||
prompt_messages,
|
||||
api_key=openai_api_key,
|
||||
base_url=openai_base_url,
|
||||
callback=lambda x: process_chunk(x),
|
||||
model=code_generation_model,
|
||||
callback=lambda x, i=index: process_chunk(x, i),
|
||||
model=Llm.GPT_4O_2024_05_13,
|
||||
)
|
||||
exact_llm_version = code_generation_model
|
||||
)
|
||||
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:
|
||||
print("[GENERATE_CODE] Authentication failed", e)
|
||||
error_message = (
|
||||
@ -291,39 +373,34 @@ async def stream_code(websocket: WebSocket):
|
||||
)
|
||||
return await throw_error(error_message)
|
||||
|
||||
if validated_input_mode == "video":
|
||||
completion = extract_tag_content("html", completion)
|
||||
## Post-processing
|
||||
|
||||
print("Exact used model for generation: ", exact_llm_version)
|
||||
# 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_logs(prompt_messages, completion) # type: ignore
|
||||
write_logs(prompt_messages, completions[0])
|
||||
|
||||
try:
|
||||
if should_generate_images:
|
||||
await websocket.send_json(
|
||||
{"type": "status", "value": "Generating images..."}
|
||||
)
|
||||
updated_html = await generate_images(
|
||||
## Image Generation
|
||||
|
||||
for index, _ in enumerate(completions):
|
||||
await send_message("status", "Generating images...", index)
|
||||
|
||||
image_generation_tasks = [
|
||||
perform_image_generation(
|
||||
completion,
|
||||
api_key=openai_api_key,
|
||||
base_url=openai_base_url,
|
||||
image_cache=image_cache,
|
||||
)
|
||||
else:
|
||||
updated_html = completion
|
||||
await websocket.send_json({"type": "setCode", "value": updated_html})
|
||||
await websocket.send_json(
|
||||
{"type": "status", "value": "Code generation complete."}
|
||||
)
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
print("Image generation failed", e)
|
||||
# Send set code even if image generation fails since that triggers
|
||||
# the frontend to update history
|
||||
await websocket.send_json({"type": "setCode", "value": completion})
|
||||
await websocket.send_json(
|
||||
{"type": "status", "value": "Image generation failed but code is complete."}
|
||||
should_generate_images,
|
||||
openai_api_key,
|
||||
openai_base_url,
|
||||
image_cache,
|
||||
)
|
||||
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()
|
||||
|
||||
@ -10,12 +10,12 @@ from typing import Any, Coroutine
|
||||
import asyncio
|
||||
|
||||
from evals.config import EVALS_DIR
|
||||
from evals.core import generate_code_core
|
||||
from evals.core import generate_code_for_image
|
||||
from evals.utils import image_to_data_url
|
||||
|
||||
STACK = "ionic_tailwind"
|
||||
MODEL = Llm.GPT_4O_2024_05_13
|
||||
N = 1 # Number of outputs to generate
|
||||
STACK = "html_tailwind"
|
||||
# MODEL = Llm.CLAUDE_3_5_SONNET_2024_06_20
|
||||
N = 2 # Number of outputs to generate
|
||||
|
||||
|
||||
async def main():
|
||||
@ -29,10 +29,21 @@ async def main():
|
||||
for filename in evals:
|
||||
filepath = os.path.join(INPUT_DIR, filename)
|
||||
data_url = await image_to_data_url(filepath)
|
||||
for _ in range(N): # Generate N tasks for each input
|
||||
task = generate_code_core(image_url=data_url, stack=STACK, model=MODEL)
|
||||
for n in range(N): # Generate N tasks for each input
|
||||
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)
|
||||
|
||||
print(f"Generating {len(tasks)} codes")
|
||||
|
||||
results = await asyncio.gather(*tasks)
|
||||
|
||||
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
||||
@ -49,4 +60,48 @@ async def main():
|
||||
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())
|
||||
|
||||
85
backend/run_image_generation_evals.py
Normal file
85
backend/run_image_generation_evals.py
Normal file
@ -0,0 +1,85 @@
|
||||
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())
|
||||
@ -1,4 +1,4 @@
|
||||
FROM node:20.9-bullseye-slim
|
||||
FROM node:22-bullseye-slim
|
||||
|
||||
# Set the working directory in the container
|
||||
WORKDIR /app
|
||||
@ -6,6 +6,9 @@ WORKDIR /app
|
||||
# Copy package.json and yarn.lock
|
||||
COPY package.json yarn.lock /app/
|
||||
|
||||
# Set the environment variable to skip Puppeteer download
|
||||
ENV PUPPETEER_SKIP_DOWNLOAD=true
|
||||
|
||||
# Install dependencies
|
||||
RUN yarn install
|
||||
|
||||
|
||||
@ -36,6 +36,7 @@
|
||||
"codemirror": "^6.0.1",
|
||||
"copy-to-clipboard": "^3.3.3",
|
||||
"html2canvas": "^1.4.1",
|
||||
"nanoid": "^5.0.7",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-dropzone": "^14.2.3",
|
||||
@ -46,7 +47,8 @@
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"thememirror": "^2.0.1",
|
||||
"vite-plugin-checker": "^0.6.2",
|
||||
"webm-duration-fix": "^1.0.4"
|
||||
"webm-duration-fix": "^1.0.4",
|
||||
"zustand": "^4.5.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^29.5.12",
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 16 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 16 KiB |
@ -1,98 +1,98 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import ImageUpload from "./components/ImageUpload";
|
||||
import CodePreview from "./components/CodePreview";
|
||||
import Preview from "./components/Preview";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { generateCode } from "./generateCode";
|
||||
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 SettingsDialog from "./components/settings/SettingsDialog";
|
||||
import { AppState, CodeGenerationParams, EditorTheme, Settings } from "./types";
|
||||
import { IS_RUNNING_ON_CLOUD } from "./config";
|
||||
import { PicoBadge } from "./components/PicoBadge";
|
||||
import { OnboardingNote } from "./components/OnboardingNote";
|
||||
import { PicoBadge } from "./components/messages/PicoBadge";
|
||||
import { OnboardingNote } from "./components/messages/OnboardingNote";
|
||||
import { usePersistedState } from "./hooks/usePersistedState";
|
||||
import { UrlInputSection } from "./components/UrlInputSection";
|
||||
import TermsOfServiceDialog from "./components/TermsOfServiceDialog";
|
||||
import html2canvas from "html2canvas";
|
||||
import { USER_CLOSE_WEB_SOCKET_CODE } from "./constants";
|
||||
import CodeTab from "./components/CodeTab";
|
||||
import OutputSettingsSection from "./components/OutputSettingsSection";
|
||||
import { History } from "./components/history/history_types";
|
||||
import HistoryDisplay from "./components/history/HistoryDisplay";
|
||||
import { extractHistoryTree } from "./components/history/utils";
|
||||
import { extractHistory } from "./components/history/utils";
|
||||
import toast from "react-hot-toast";
|
||||
import ImportCodeSection from "./components/ImportCodeSection";
|
||||
import { Stack } from "./lib/stacks";
|
||||
import { CodeGenerationModel } from "./lib/models";
|
||||
import ModelSettingsSection from "./components/ModelSettingsSection";
|
||||
import { extractHtml } from "./components/preview/extractHtml";
|
||||
import useBrowserTabIndicator from "./hooks/useBrowserTabIndicator";
|
||||
import TipLink from "./components/core/TipLink";
|
||||
|
||||
const IS_OPENAI_DOWN = false;
|
||||
import TipLink from "./components/messages/TipLink";
|
||||
import { useAppStore } from "./store/app-store";
|
||||
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() {
|
||||
const [appState, setAppState] = useState<AppState>(AppState.INITIAL);
|
||||
const [generatedCode, setGeneratedCode] = useState<string>("");
|
||||
const {
|
||||
// Inputs
|
||||
inputMode,
|
||||
setInputMode,
|
||||
isImportedFromCode,
|
||||
setIsImportedFromCode,
|
||||
referenceImages,
|
||||
setReferenceImages,
|
||||
|
||||
const [inputMode, setInputMode] = useState<"image" | "video">("image");
|
||||
head,
|
||||
commits,
|
||||
addCommit,
|
||||
removeCommit,
|
||||
setHead,
|
||||
appendCommitCode,
|
||||
setCommitCode,
|
||||
resetCommits,
|
||||
resetHead,
|
||||
|
||||
const [referenceImages, setReferenceImages] = useState<string[]>([]);
|
||||
const [executionConsole, setExecutionConsole] = useState<string[]>([]);
|
||||
const [updateInstruction, setUpdateInstruction] = useState("");
|
||||
const [isImportedFromCode, setIsImportedFromCode] = useState<boolean>(false);
|
||||
// Outputs
|
||||
appendExecutionConsole,
|
||||
resetExecutionConsoles,
|
||||
} = useProjectStore();
|
||||
|
||||
const {
|
||||
disableInSelectAndEditMode,
|
||||
setUpdateInstruction,
|
||||
appState,
|
||||
setAppState,
|
||||
shouldIncludeResultImage,
|
||||
setShouldIncludeResultImage,
|
||||
} = useAppStore();
|
||||
|
||||
// Settings
|
||||
const [settings, setSettings] = usePersistedState<Settings>(
|
||||
{
|
||||
openAiApiKey: null,
|
||||
openAiBaseURL: null,
|
||||
anthropicApiKey: null,
|
||||
screenshotOneApiKey: null,
|
||||
isImageGenerationEnabled: true,
|
||||
editorTheme: EditorTheme.COBALT,
|
||||
generatedCodeConfig: Stack.HTML_TAILWIND,
|
||||
codeGenerationModel: CodeGenerationModel.GPT_4O_2024_05_13,
|
||||
codeGenerationModel: CodeGenerationModel.CLAUDE_3_5_SONNET_2024_06_20,
|
||||
// Only relevant for hosted version
|
||||
isTermOfServiceAccepted: false,
|
||||
},
|
||||
"setting"
|
||||
);
|
||||
|
||||
// Code generation model from local storage or the default value
|
||||
const selectedCodeGenerationModel =
|
||||
settings.codeGenerationModel || CodeGenerationModel.GPT_4_VISION;
|
||||
|
||||
// App history
|
||||
const [appHistory, setAppHistory] = useState<History>([]);
|
||||
// Tracks the currently shown version from app history
|
||||
const [currentVersion, setCurrentVersion] = useState<number | null>(null);
|
||||
|
||||
const [shouldIncludeResultImage, setShouldIncludeResultImage] =
|
||||
useState<boolean>(false);
|
||||
|
||||
const wsRef = useRef<WebSocket>(null);
|
||||
|
||||
const showReactWarning =
|
||||
selectedCodeGenerationModel ===
|
||||
CodeGenerationModel.GPT_4_TURBO_2024_04_09 &&
|
||||
settings.generatedCodeConfig === Stack.REACT_TAILWIND;
|
||||
// Code generation model from local storage or the default value
|
||||
const model =
|
||||
settings.codeGenerationModel || CodeGenerationModel.GPT_4_VISION;
|
||||
|
||||
const showGpt4OMessage =
|
||||
selectedCodeGenerationModel !== CodeGenerationModel.GPT_4O_2024_05_13 &&
|
||||
const showBetterModelMessage =
|
||||
model !== CodeGenerationModel.GPT_4O_2024_05_13 &&
|
||||
model !== CodeGenerationModel.CLAUDE_3_5_SONNET_2024_06_20 &&
|
||||
appState === AppState.INITIAL;
|
||||
|
||||
const showSelectAndEditFeature =
|
||||
(model === CodeGenerationModel.GPT_4O_2024_05_13 ||
|
||||
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
|
||||
useBrowserTabIndicator(appState === AppState.CODING);
|
||||
|
||||
@ -108,59 +108,34 @@ function App() {
|
||||
}
|
||||
}, [settings.generatedCodeConfig, setSettings]);
|
||||
|
||||
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);
|
||||
};
|
||||
|
||||
// Functions
|
||||
const reset = () => {
|
||||
setAppState(AppState.INITIAL);
|
||||
setGeneratedCode("");
|
||||
setReferenceImages([]);
|
||||
setExecutionConsole([]);
|
||||
setUpdateInstruction("");
|
||||
setIsImportedFromCode(false);
|
||||
setAppHistory([]);
|
||||
setCurrentVersion(null);
|
||||
setShouldIncludeResultImage(false);
|
||||
setUpdateInstruction("");
|
||||
disableInSelectAndEditMode();
|
||||
resetExecutionConsoles();
|
||||
|
||||
resetCommits();
|
||||
resetHead();
|
||||
|
||||
// Inputs
|
||||
setInputMode("image");
|
||||
setReferenceImages([]);
|
||||
setIsImportedFromCode(false);
|
||||
};
|
||||
|
||||
const regenerate = () => {
|
||||
if (currentVersion === null) {
|
||||
if (head === null) {
|
||||
toast.error(
|
||||
"No current version set. Please open a Github issue as this shouldn't happen."
|
||||
"No current version set. Please contact support via chat or Github."
|
||||
);
|
||||
return;
|
||||
throw new Error("Regenerate called with no head");
|
||||
}
|
||||
|
||||
// Retrieve the previous command
|
||||
const previousCommand = appHistory[currentVersion];
|
||||
if (previousCommand.type !== "ai_create") {
|
||||
const currentCommit = commits[head];
|
||||
if (currentCommit.type !== "ai_create") {
|
||||
toast.error("Only the first version can be regenerated.");
|
||||
return;
|
||||
}
|
||||
@ -169,87 +144,86 @@ function App() {
|
||||
doCreate(referenceImages, inputMode);
|
||||
};
|
||||
|
||||
// Used when the user cancels the code generation
|
||||
const cancelCodeGeneration = () => {
|
||||
wsRef.current?.close?.(USER_CLOSE_WEB_SOCKET_CODE);
|
||||
// make sure stop can correct the state even if the websocket is already closed
|
||||
cancelCodeGenerationAndReset();
|
||||
};
|
||||
|
||||
const previewCode =
|
||||
inputMode === "video" && appState === AppState.CODING
|
||||
? extractHtml(generatedCode)
|
||||
: generatedCode;
|
||||
|
||||
const cancelCodeGenerationAndReset = () => {
|
||||
// When this is the first version, reset the entire app state
|
||||
if (currentVersion === null) {
|
||||
// Used for code generation failure as well
|
||||
const cancelCodeGenerationAndReset = (commit: Commit) => {
|
||||
// When the current commit is the first version, reset the entire app state
|
||||
if (commit.type === "ai_create") {
|
||||
reset();
|
||||
} else {
|
||||
// Otherwise, revert to the last version
|
||||
setGeneratedCode(appHistory[currentVersion].code);
|
||||
// Otherwise, remove current commit from commits
|
||||
removeCommit(commit.hash);
|
||||
|
||||
// Revert to parent commit
|
||||
const parentCommitHash = commit.parentHash;
|
||||
if (parentCommitHash) {
|
||||
setHead(parentCommitHash);
|
||||
} else {
|
||||
throw new Error("Parent commit not found");
|
||||
}
|
||||
|
||||
setAppState(AppState.CODE_READY);
|
||||
}
|
||||
};
|
||||
|
||||
function doGenerateCode(
|
||||
params: CodeGenerationParams,
|
||||
parentVersion: number | null
|
||||
) {
|
||||
setExecutionConsole([]);
|
||||
function doGenerateCode(params: CodeGenerationParams) {
|
||||
// Reset the execution console
|
||||
resetExecutionConsoles();
|
||||
|
||||
// Set the app state
|
||||
setAppState(AppState.CODING);
|
||||
|
||||
// Merge settings with params
|
||||
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(
|
||||
wsRef,
|
||||
updatedParams,
|
||||
// On change
|
||||
(token) => setGeneratedCode((prev) => prev + token),
|
||||
(token, variantIndex) => {
|
||||
appendCommitCode(commit.hash, variantIndex, token);
|
||||
},
|
||||
// On set code
|
||||
(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;
|
||||
});
|
||||
}
|
||||
(code, variantIndex) => {
|
||||
setCommitCode(commit.hash, variantIndex, code);
|
||||
},
|
||||
// On status update
|
||||
(line) => setExecutionConsole((prev) => [...prev, line]),
|
||||
(line, variantIndex) => appendExecutionConsole(variantIndex, line),
|
||||
// On cancel
|
||||
() => {
|
||||
cancelCodeGenerationAndReset();
|
||||
cancelCodeGenerationAndReset(commit);
|
||||
},
|
||||
// On complete
|
||||
() => {
|
||||
@ -263,68 +237,71 @@ function App() {
|
||||
// Reset any existing state
|
||||
reset();
|
||||
|
||||
// Set the input states
|
||||
setReferenceImages(referenceImages);
|
||||
setInputMode(inputMode);
|
||||
|
||||
// Kick off the code generation
|
||||
if (referenceImages.length > 0) {
|
||||
doGenerateCode(
|
||||
{
|
||||
doGenerateCode({
|
||||
generationType: "create",
|
||||
image: referenceImages[0],
|
||||
inputMode,
|
||||
},
|
||||
currentVersion
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Subsequent updates
|
||||
async function doUpdate() {
|
||||
if (currentVersion === null) {
|
||||
async function doUpdate(
|
||||
updateInstruction: string,
|
||||
selectedElement?: HTMLElement
|
||||
) {
|
||||
if (updateInstruction.trim() === "") {
|
||||
toast.error("Please include some instructions for AI on what to update.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (head === null) {
|
||||
toast.error(
|
||||
"No current version set. Contact support or open a Github issue."
|
||||
);
|
||||
return;
|
||||
throw new Error("Update called with no head");
|
||||
}
|
||||
|
||||
let historyTree;
|
||||
try {
|
||||
historyTree = extractHistoryTree(appHistory, currentVersion);
|
||||
historyTree = extractHistory(head, commits);
|
||||
} catch {
|
||||
toast.error(
|
||||
"Version history is invalid. This shouldn't happen. Please contact support or open a Github issue."
|
||||
);
|
||||
return;
|
||||
throw new Error("Invalid version history");
|
||||
}
|
||||
|
||||
const updatedHistory = [...historyTree, updateInstruction];
|
||||
let modifiedUpdateInstruction = updateInstruction;
|
||||
|
||||
if (shouldIncludeResultImage) {
|
||||
const resultImage = await takeScreenshot();
|
||||
doGenerateCode(
|
||||
{
|
||||
// Send in a reference to the selected element if it exists
|
||||
if (selectedElement) {
|
||||
modifiedUpdateInstruction =
|
||||
updateInstruction +
|
||||
" referring to this element specifically: " +
|
||||
selectedElement.outerHTML;
|
||||
}
|
||||
|
||||
const updatedHistory = [...historyTree, modifiedUpdateInstruction];
|
||||
const resultImage = shouldIncludeResultImage
|
||||
? await takeScreenshot()
|
||||
: undefined;
|
||||
|
||||
doGenerateCode({
|
||||
generationType: "update",
|
||||
inputMode,
|
||||
image: referenceImages[0],
|
||||
resultImage: resultImage,
|
||||
resultImage,
|
||||
history: updatedHistory,
|
||||
isImportedFromCode,
|
||||
},
|
||||
currentVersion
|
||||
);
|
||||
} else {
|
||||
doGenerateCode(
|
||||
{
|
||||
generationType: "update",
|
||||
inputMode,
|
||||
image: referenceImages[0],
|
||||
history: updatedHistory,
|
||||
isImportedFromCode,
|
||||
},
|
||||
currentVersion
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
setGeneratedCode("");
|
||||
setUpdateInstruction("");
|
||||
}
|
||||
|
||||
@ -342,29 +319,24 @@ function App() {
|
||||
}));
|
||||
}
|
||||
|
||||
function setCodeGenerationModel(codeGenerationModel: CodeGenerationModel) {
|
||||
setSettings((prev) => ({
|
||||
...prev,
|
||||
codeGenerationModel,
|
||||
}));
|
||||
}
|
||||
|
||||
function importFromCode(code: string, stack: Stack) {
|
||||
// Set input state
|
||||
setIsImportedFromCode(true);
|
||||
|
||||
// Set up this project
|
||||
setGeneratedCode(code);
|
||||
setStack(stack);
|
||||
setAppHistory([
|
||||
{
|
||||
type: "code_create",
|
||||
parentIndex: null,
|
||||
code,
|
||||
inputs: { code },
|
||||
},
|
||||
]);
|
||||
setCurrentVersion(0);
|
||||
|
||||
// Create a new commit and set it as the head
|
||||
const commit = createCommit({
|
||||
type: "code_create",
|
||||
parentHash: null,
|
||||
variants: [{ code }],
|
||||
inputs: null,
|
||||
});
|
||||
addCommit(commit);
|
||||
setHead(commit.hash);
|
||||
|
||||
// Set the app state
|
||||
setAppState(AppState.CODE_READY);
|
||||
}
|
||||
|
||||
@ -379,263 +351,47 @@ function App() {
|
||||
)}
|
||||
<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">
|
||||
{/* Header with access to settings */}
|
||||
<div className="flex items-center justify-between mt-10 mb-2">
|
||||
<h1 className="text-2xl ">Screenshot to Code</h1>
|
||||
<SettingsDialog settings={settings} setSettings={setSettings} />
|
||||
</div>
|
||||
|
||||
<OutputSettingsSection
|
||||
stack={settings.generatedCodeConfig}
|
||||
setStack={(config) => setStack(config)}
|
||||
shouldDisableUpdates={
|
||||
appState === AppState.CODING || appState === AppState.CODE_READY
|
||||
}
|
||||
/>
|
||||
{/* Generation settings like stack and model */}
|
||||
<GenerationSettings settings={settings} setSettings={setSettings} />
|
||||
|
||||
<ModelSettingsSection
|
||||
codeGenerationModel={selectedCodeGenerationModel}
|
||||
setCodeGenerationModel={setCodeGenerationModel}
|
||||
shouldDisableUpdates={
|
||||
appState === AppState.CODING || appState === AppState.CODE_READY
|
||||
}
|
||||
/>
|
||||
|
||||
{showReactWarning && (
|
||||
<div className="text-sm bg-yellow-200 rounded p-2">
|
||||
Sorry - React is not currently working with GPT-4 Turbo. Please
|
||||
use GPT-4 Vision or Claude Sonnet. We are working on a fix.
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showGpt4OMessage && (
|
||||
<div className="rounded-lg p-2 bg-fuchsia-200">
|
||||
<p className="text-gray-800 text-sm">
|
||||
Now supporting GPT-4o. Higher quality and 2x faster. Give it a
|
||||
try!
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{/* Show auto updated message when older models are choosen */}
|
||||
{showBetterModelMessage && <DeprecationMessage />}
|
||||
|
||||
{/* Show tip link until coding is complete */}
|
||||
{appState !== AppState.CODE_READY && <TipLink />}
|
||||
|
||||
{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.CODE_READY) && (
|
||||
<>
|
||||
{/* 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={generatedCode} />
|
||||
|
||||
<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
|
||||
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-btn"
|
||||
>
|
||||
Update
|
||||
</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>
|
||||
</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"
|
||||
<Sidebar
|
||||
showSelectAndEditFeature={showSelectAndEditFeature}
|
||||
doUpdate={doUpdate}
|
||||
regenerate={regenerate}
|
||||
cancelCodeGeneration={cancelCodeGeneration}
|
||||
/>
|
||||
)}
|
||||
{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>
|
||||
{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>
|
||||
|
||||
<main className="py-2 lg:pl-96">
|
||||
{appState === AppState.INITIAL && (
|
||||
<div className="flex flex-col justify-center items-center gap-y-10">
|
||||
<ImageUpload setReferenceImages={doCreate} />
|
||||
<UrlInputSection
|
||||
<StartPane
|
||||
doCreate={doCreate}
|
||||
screenshotOneApiKey={settings.screenshotOneApiKey}
|
||||
importFromCode={importFromCode}
|
||||
settings={settings}
|
||||
/>
|
||||
<ImportCodeSection importFromCode={importFromCode} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(appState === AppState.CODING || appState === AppState.CODE_READY) && (
|
||||
<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}
|
||||
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">
|
||||
<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>
|
||||
<PreviewPane doUpdate={doUpdate} reset={reset} settings={settings} />
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@ -10,7 +10,7 @@ import {
|
||||
DialogTrigger,
|
||||
} from "./ui/dialog";
|
||||
import { Textarea } from "./ui/textarea";
|
||||
import OutputSettingsSection from "./OutputSettingsSection";
|
||||
import OutputSettingsSection from "./settings/OutputSettingsSection";
|
||||
import toast from "react-hot-toast";
|
||||
import { Stack } from "../lib/stacks";
|
||||
|
||||
|
||||
@ -1,65 +0,0 @@
|
||||
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>AI 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;
|
||||
@ -1,41 +0,0 @@
|
||||
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 iframeRef = useRef<HTMLIFrameElement | null>(null);
|
||||
|
||||
// Don't update code more often than every 200ms.
|
||||
const throttledCode = useThrottle(code, 200);
|
||||
|
||||
useEffect(() => {
|
||||
if (iframeRef.current) {
|
||||
iframeRef.current.srcdoc = throttledCode;
|
||||
}
|
||||
}, [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;
|
||||
37
frontend/src/components/commits/types.ts
Normal file
37
frontend/src/components/commits/types.ts
Normal file
@ -0,0 +1,37 @@
|
||||
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;
|
||||
32
frontend/src/components/commits/utils.ts
Normal file
32
frontend/src/components/commits/utils.ts
Normal file
@ -0,0 +1,32 @@
|
||||
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,
|
||||
};
|
||||
}
|
||||
25
frontend/src/components/core/KeyboardShortcutBadge.tsx
Normal file
25
frontend/src/components/core/KeyboardShortcutBadge.tsx
Normal file
@ -0,0 +1,25 @@
|
||||
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;
|
||||
@ -1,4 +1,3 @@
|
||||
import { History } from "./history_types";
|
||||
import toast from "react-hot-toast";
|
||||
import classNames from "classnames";
|
||||
|
||||
@ -11,21 +10,23 @@ import {
|
||||
} from "../ui/collapsible";
|
||||
import { Button } from "../ui/button";
|
||||
import { CaretSortIcon } from "@radix-ui/react-icons";
|
||||
import { useProjectStore } from "../../store/project-store";
|
||||
|
||||
interface Props {
|
||||
history: History;
|
||||
currentVersion: number | null;
|
||||
revertToVersion: (version: number) => void;
|
||||
shouldDisableReverts: boolean;
|
||||
}
|
||||
|
||||
export default function HistoryDisplay({
|
||||
history,
|
||||
currentVersion,
|
||||
revertToVersion,
|
||||
shouldDisableReverts,
|
||||
}: Props) {
|
||||
const renderedHistory = renderHistory(history, currentVersion);
|
||||
export default function HistoryDisplay({ shouldDisableReverts }: Props) {
|
||||
const { commits, head, setHead } = useProjectStore();
|
||||
|
||||
// Put all commits into an array and sort by created date (oldest first)
|
||||
const flatHistory = Object.values(commits).sort(
|
||||
(a, b) =>
|
||||
new Date(a.dateCreated).getTime() - new Date(b.dateCreated).getTime()
|
||||
);
|
||||
|
||||
// Annotate history items with a summary, parent version, etc.
|
||||
const renderedHistory = renderHistory(flatHistory);
|
||||
|
||||
return renderedHistory.length === 0 ? null : (
|
||||
<div className="flex flex-col h-screen">
|
||||
@ -39,8 +40,8 @@ export default function HistoryDisplay({
|
||||
"flex items-center justify-between space-x-2 w-full pr-2",
|
||||
"border-b cursor-pointer",
|
||||
{
|
||||
" hover:bg-black hover:text-white": !item.isActive,
|
||||
"bg-slate-500 text-white": item.isActive,
|
||||
" hover:bg-black hover:text-white": item.hash === head,
|
||||
"bg-slate-500 text-white": item.hash === head,
|
||||
}
|
||||
)}
|
||||
>
|
||||
@ -51,14 +52,14 @@ export default function HistoryDisplay({
|
||||
? toast.error(
|
||||
"Please wait for code generation to complete before viewing an older version."
|
||||
)
|
||||
: revertToVersion(index)
|
||||
: setHead(item.hash)
|
||||
}
|
||||
>
|
||||
<div className="flex gap-x-1 truncate">
|
||||
<h2 className="text-sm truncate">{item.summary}</h2>
|
||||
{item.parentVersion !== null && (
|
||||
<h2 className="text-sm">
|
||||
(parent: {item.parentVersion})
|
||||
(parent: v{item.parentVersion})
|
||||
</h2>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -1,41 +0,0 @@
|
||||
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;
|
||||
};
|
||||
@ -1,91 +1,125 @@
|
||||
import { extractHistoryTree, renderHistory } from "./utils";
|
||||
import type { History } from "./history_types";
|
||||
import { extractHistory, renderHistory } from "./utils";
|
||||
import { Commit, CommitHash } from "../commits/types";
|
||||
|
||||
const basicLinearHistory: History = [
|
||||
{
|
||||
const basicLinearHistory: Record<CommitHash, Commit> = {
|
||||
"0": {
|
||||
hash: "0",
|
||||
dateCreated: new Date(),
|
||||
isCommitted: false,
|
||||
type: "ai_create",
|
||||
parentIndex: null,
|
||||
code: "<html>1. create</html>",
|
||||
parentHash: null,
|
||||
variants: [{ code: "<html>1. create</html>" }],
|
||||
selectedVariantIndex: 0,
|
||||
inputs: {
|
||||
image_url: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
"1": {
|
||||
hash: "1",
|
||||
dateCreated: new Date(),
|
||||
isCommitted: false,
|
||||
type: "ai_edit",
|
||||
parentIndex: 0,
|
||||
code: "<html>2. edit with better icons</html>",
|
||||
parentHash: "0",
|
||||
variants: [{ code: "<html>2. edit with better icons</html>" }],
|
||||
selectedVariantIndex: 0,
|
||||
inputs: {
|
||||
prompt: "use better icons",
|
||||
},
|
||||
},
|
||||
{
|
||||
"2": {
|
||||
hash: "2",
|
||||
dateCreated: new Date(),
|
||||
isCommitted: false,
|
||||
type: "ai_edit",
|
||||
parentIndex: 1,
|
||||
code: "<html>3. edit with better icons and red text</html>",
|
||||
parentHash: "1",
|
||||
variants: [{ code: "<html>3. edit with better icons and red text</html>" }],
|
||||
selectedVariantIndex: 0,
|
||||
inputs: {
|
||||
prompt: "make text red",
|
||||
},
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
const basicLinearHistoryWithCode: History = [
|
||||
{
|
||||
const basicLinearHistoryWithCode: Record<CommitHash, Commit> = {
|
||||
"0": {
|
||||
hash: "0",
|
||||
dateCreated: new Date(),
|
||||
isCommitted: false,
|
||||
type: "code_create",
|
||||
parentIndex: null,
|
||||
code: "<html>1. create</html>",
|
||||
inputs: {
|
||||
code: "<html>1. create</html>",
|
||||
parentHash: null,
|
||||
variants: [{ code: "<html>1. create</html>" }],
|
||||
selectedVariantIndex: 0,
|
||||
inputs: null,
|
||||
},
|
||||
},
|
||||
...basicLinearHistory.slice(1),
|
||||
];
|
||||
...Object.fromEntries(Object.entries(basicLinearHistory).slice(1)),
|
||||
};
|
||||
|
||||
const basicBranchingHistory: History = [
|
||||
const basicBranchingHistory: Record<CommitHash, Commit> = {
|
||||
...basicLinearHistory,
|
||||
{
|
||||
"3": {
|
||||
hash: "3",
|
||||
dateCreated: new Date(),
|
||||
isCommitted: false,
|
||||
type: "ai_edit",
|
||||
parentIndex: 1,
|
||||
code: "<html>4. edit with better icons and green text</html>",
|
||||
parentHash: "1",
|
||||
variants: [
|
||||
{ code: "<html>4. edit with better icons and green text</html>" },
|
||||
],
|
||||
selectedVariantIndex: 0,
|
||||
inputs: {
|
||||
prompt: "make text green",
|
||||
},
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
const longerBranchingHistory: History = [
|
||||
const longerBranchingHistory: Record<CommitHash, Commit> = {
|
||||
...basicBranchingHistory,
|
||||
{
|
||||
"4": {
|
||||
hash: "4",
|
||||
dateCreated: new Date(),
|
||||
isCommitted: false,
|
||||
type: "ai_edit",
|
||||
parentIndex: 3,
|
||||
code: "<html>5. edit with better icons and green, bold text</html>",
|
||||
parentHash: "3",
|
||||
variants: [
|
||||
{ code: "<html>5. edit with better icons and green, bold text</html>" },
|
||||
],
|
||||
selectedVariantIndex: 0,
|
||||
inputs: {
|
||||
prompt: "make text bold",
|
||||
},
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
const basicBadHistory: History = [
|
||||
{
|
||||
const basicBadHistory: Record<CommitHash, Commit> = {
|
||||
"0": {
|
||||
hash: "0",
|
||||
dateCreated: new Date(),
|
||||
isCommitted: false,
|
||||
type: "ai_create",
|
||||
parentIndex: null,
|
||||
code: "<html>1. create</html>",
|
||||
parentHash: null,
|
||||
variants: [{ code: "<html>1. create</html>" }],
|
||||
selectedVariantIndex: 0,
|
||||
inputs: {
|
||||
image_url: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
"1": {
|
||||
hash: "1",
|
||||
dateCreated: new Date(),
|
||||
isCommitted: false,
|
||||
type: "ai_edit",
|
||||
parentIndex: 2, // <- Bad parent index
|
||||
code: "<html>2. edit with better icons</html>",
|
||||
parentHash: "2", // <- Bad parent hash
|
||||
variants: [{ code: "<html>2. edit with better icons</html>" }],
|
||||
selectedVariantIndex: 0,
|
||||
inputs: {
|
||||
prompt: "use better icons",
|
||||
},
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
describe("History Utils", () => {
|
||||
test("should correctly extract the history tree", () => {
|
||||
expect(extractHistoryTree(basicLinearHistory, 2)).toEqual([
|
||||
expect(extractHistory("2", basicLinearHistory)).toEqual([
|
||||
"<html>1. create</html>",
|
||||
"use better icons",
|
||||
"<html>2. edit with better icons</html>",
|
||||
@ -93,12 +127,12 @@ describe("History Utils", () => {
|
||||
"<html>3. edit with better icons and red text</html>",
|
||||
]);
|
||||
|
||||
expect(extractHistoryTree(basicLinearHistory, 0)).toEqual([
|
||||
expect(extractHistory("0", basicLinearHistory)).toEqual([
|
||||
"<html>1. create</html>",
|
||||
]);
|
||||
|
||||
// Test branching
|
||||
expect(extractHistoryTree(basicBranchingHistory, 3)).toEqual([
|
||||
expect(extractHistory("3", basicBranchingHistory)).toEqual([
|
||||
"<html>1. create</html>",
|
||||
"use better icons",
|
||||
"<html>2. edit with better icons</html>",
|
||||
@ -106,7 +140,7 @@ describe("History Utils", () => {
|
||||
"<html>4. edit with better icons and green text</html>",
|
||||
]);
|
||||
|
||||
expect(extractHistoryTree(longerBranchingHistory, 4)).toEqual([
|
||||
expect(extractHistory("4", longerBranchingHistory)).toEqual([
|
||||
"<html>1. create</html>",
|
||||
"use better icons",
|
||||
"<html>2. edit with better icons</html>",
|
||||
@ -116,7 +150,7 @@ describe("History Utils", () => {
|
||||
"<html>5. edit with better icons and green, bold text</html>",
|
||||
]);
|
||||
|
||||
expect(extractHistoryTree(longerBranchingHistory, 2)).toEqual([
|
||||
expect(extractHistory("2", longerBranchingHistory)).toEqual([
|
||||
"<html>1. create</html>",
|
||||
"use better icons",
|
||||
"<html>2. edit with better icons</html>",
|
||||
@ -126,105 +160,82 @@ describe("History Utils", () => {
|
||||
|
||||
// Errors
|
||||
|
||||
// Bad index
|
||||
expect(() => extractHistoryTree(basicLinearHistory, 100)).toThrow();
|
||||
expect(() => extractHistoryTree(basicLinearHistory, -2)).toThrow();
|
||||
// Bad hash
|
||||
expect(() => extractHistory("100", basicLinearHistory)).toThrow();
|
||||
|
||||
// Bad tree
|
||||
expect(() => extractHistoryTree(basicBadHistory, 1)).toThrow();
|
||||
expect(() => extractHistory("1", basicBadHistory)).toThrow();
|
||||
});
|
||||
|
||||
test("should correctly render the history tree", () => {
|
||||
expect(renderHistory(basicLinearHistory, 2)).toEqual([
|
||||
expect(renderHistory(Object.values(basicLinearHistory))).toEqual([
|
||||
{
|
||||
isActive: false,
|
||||
parentVersion: null,
|
||||
summary: "Create",
|
||||
...basicLinearHistory["0"],
|
||||
type: "Create",
|
||||
},
|
||||
{
|
||||
isActive: false,
|
||||
parentVersion: null,
|
||||
summary: "use better icons",
|
||||
type: "Edit",
|
||||
},
|
||||
{
|
||||
isActive: true,
|
||||
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",
|
||||
parentVersion: null,
|
||||
},
|
||||
{
|
||||
isActive: false,
|
||||
parentVersion: null,
|
||||
...basicLinearHistory["1"],
|
||||
type: "Edit",
|
||||
summary: "use better icons",
|
||||
type: "Edit",
|
||||
parentVersion: null,
|
||||
},
|
||||
{
|
||||
isActive: false,
|
||||
parentVersion: null,
|
||||
summary: "make text red",
|
||||
...basicLinearHistory["2"],
|
||||
type: "Edit",
|
||||
summary: "make text red",
|
||||
parentVersion: null,
|
||||
},
|
||||
]);
|
||||
|
||||
// Render a history with code
|
||||
expect(renderHistory(basicLinearHistoryWithCode, 0)).toEqual([
|
||||
expect(renderHistory(Object.values(basicLinearHistoryWithCode))).toEqual([
|
||||
{
|
||||
isActive: true,
|
||||
parentVersion: null,
|
||||
summary: "Imported from code",
|
||||
...basicLinearHistoryWithCode["0"],
|
||||
type: "Imported from code",
|
||||
summary: "Imported from code",
|
||||
parentVersion: null,
|
||||
},
|
||||
{
|
||||
isActive: false,
|
||||
parentVersion: null,
|
||||
...basicLinearHistoryWithCode["1"],
|
||||
type: "Edit",
|
||||
summary: "use better icons",
|
||||
type: "Edit",
|
||||
parentVersion: null,
|
||||
},
|
||||
{
|
||||
isActive: false,
|
||||
parentVersion: null,
|
||||
summary: "make text red",
|
||||
...basicLinearHistoryWithCode["2"],
|
||||
type: "Edit",
|
||||
summary: "make text red",
|
||||
parentVersion: null,
|
||||
},
|
||||
]);
|
||||
|
||||
// Render a non-linear history
|
||||
expect(renderHistory(basicBranchingHistory, 3)).toEqual([
|
||||
expect(renderHistory(Object.values(basicBranchingHistory))).toEqual([
|
||||
{
|
||||
isActive: false,
|
||||
parentVersion: null,
|
||||
summary: "Create",
|
||||
...basicBranchingHistory["0"],
|
||||
type: "Create",
|
||||
summary: "Create",
|
||||
parentVersion: null,
|
||||
},
|
||||
{
|
||||
isActive: false,
|
||||
parentVersion: null,
|
||||
...basicBranchingHistory["1"],
|
||||
type: "Edit",
|
||||
summary: "use better icons",
|
||||
type: "Edit",
|
||||
},
|
||||
{
|
||||
isActive: false,
|
||||
parentVersion: null,
|
||||
summary: "make text red",
|
||||
type: "Edit",
|
||||
},
|
||||
{
|
||||
isActive: true,
|
||||
parentVersion: "v2",
|
||||
summary: "make text green",
|
||||
...basicBranchingHistory["2"],
|
||||
type: "Edit",
|
||||
summary: "make text red",
|
||||
parentVersion: null,
|
||||
},
|
||||
{
|
||||
...basicBranchingHistory["3"],
|
||||
type: "Edit",
|
||||
summary: "make text green",
|
||||
parentVersion: 2,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
@ -1,33 +1,25 @@
|
||||
import {
|
||||
History,
|
||||
HistoryItem,
|
||||
HistoryItemType,
|
||||
RenderedHistoryItem,
|
||||
} from "./history_types";
|
||||
import { Commit, CommitHash, CommitType } from "../commits/types";
|
||||
|
||||
export function extractHistoryTree(
|
||||
history: History,
|
||||
version: number
|
||||
export function extractHistory(
|
||||
hash: CommitHash,
|
||||
commits: Record<CommitHash, Commit>
|
||||
): string[] {
|
||||
const flatHistory: string[] = [];
|
||||
|
||||
let currentIndex: number | null = version;
|
||||
while (currentIndex !== null) {
|
||||
const item: HistoryItem = history[currentIndex];
|
||||
let currentCommitHash: CommitHash | null = hash;
|
||||
while (currentCommitHash !== null) {
|
||||
const commit: Commit | null = commits[currentCommitHash];
|
||||
|
||||
if (item) {
|
||||
if (item.type === "ai_create") {
|
||||
// Don't include the image for ai_create
|
||||
flatHistory.unshift(item.code);
|
||||
} else if (item.type === "ai_edit") {
|
||||
flatHistory.unshift(item.code);
|
||||
flatHistory.unshift(item.inputs.prompt);
|
||||
} else if (item.type === "code_create") {
|
||||
flatHistory.unshift(item.code);
|
||||
if (commit) {
|
||||
flatHistory.unshift(commit.variants[commit.selectedVariantIndex].code);
|
||||
|
||||
// For edits, add the prompt to the history
|
||||
if (commit.type === "ai_edit") {
|
||||
flatHistory.unshift(commit.inputs.prompt);
|
||||
}
|
||||
|
||||
// Move to the parent of the current item
|
||||
currentIndex = item.parentIndex;
|
||||
currentCommitHash = commit.parentHash;
|
||||
} else {
|
||||
throw new Error("Malformed history: missing parent index");
|
||||
}
|
||||
@ -36,7 +28,7 @@ export function extractHistoryTree(
|
||||
return flatHistory;
|
||||
}
|
||||
|
||||
function displayHistoryItemType(itemType: HistoryItemType) {
|
||||
function displayHistoryItemType(itemType: CommitType) {
|
||||
switch (itemType) {
|
||||
case "ai_create":
|
||||
return "Create";
|
||||
@ -51,44 +43,48 @@ function displayHistoryItemType(itemType: HistoryItemType) {
|
||||
}
|
||||
}
|
||||
|
||||
function summarizeHistoryItem(item: HistoryItem) {
|
||||
const itemType = item.type;
|
||||
switch (itemType) {
|
||||
const setParentVersion = (commit: Commit, history: Commit[]) => {
|
||||
// If the commit has no parent, return null
|
||||
if (!commit.parentHash) return null;
|
||||
|
||||
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":
|
||||
return "Create";
|
||||
case "ai_edit":
|
||||
return item.inputs.prompt;
|
||||
return commit.inputs.prompt;
|
||||
case "code_create":
|
||||
return "Imported from code";
|
||||
default: {
|
||||
const exhaustiveCheck: never = itemType;
|
||||
const exhaustiveCheck: never = commitType;
|
||||
throw new Error(`Unhandled case: ${exhaustiveCheck}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const renderHistory = (
|
||||
history: History,
|
||||
currentVersion: number | null
|
||||
) => {
|
||||
const renderedHistory: RenderedHistoryItem[] = [];
|
||||
export const renderHistory = (history: Commit[]) => {
|
||||
const renderedHistory = [];
|
||||
|
||||
for (let i = 0; i < history.length; 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);
|
||||
const commit = history[i];
|
||||
renderedHistory.push({
|
||||
isActive,
|
||||
summary: summary,
|
||||
parentVersion,
|
||||
type,
|
||||
...commit,
|
||||
type: displayHistoryItemType(commit.type),
|
||||
summary: summarizeHistoryItem(commit),
|
||||
parentVersion: setParentVersion(commit, history),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
16
frontend/src/components/messages/DeprecationMessage.tsx
Normal file
16
frontend/src/components/messages/DeprecationMessage.tsx
Normal file
@ -0,0 +1,16 @@
|
||||
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;
|
||||
@ -1,7 +1,7 @@
|
||||
import { FaCopy } from "react-icons/fa";
|
||||
import CodeMirror from "./CodeMirror";
|
||||
import { Button } from "./ui/button";
|
||||
import { Settings } from "../types";
|
||||
import { Button } from "../ui/button";
|
||||
import { Settings } from "../../types";
|
||||
import copy from "copy-to-clipboard";
|
||||
import { useCallback } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
56
frontend/src/components/preview/PreviewComponent.tsx
Normal file
56
frontend/src/components/preview/PreviewComponent.tsx
Normal file
@ -0,0 +1,56 @@
|
||||
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;
|
||||
99
frontend/src/components/preview/PreviewPane.tsx
Normal file
99
frontend/src/components/preview/PreviewPane.tsx
Normal file
@ -0,0 +1,99 @@
|
||||
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;
|
||||
16
frontend/src/components/preview/download.ts
Normal file
16
frontend/src/components/preview/download.ts
Normal file
@ -0,0 +1,16 @@
|
||||
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);
|
||||
};
|
||||
150
frontend/src/components/select-and-edit/EditPopup.tsx
Normal file
150
frontend/src/components/select-and-edit/EditPopup.tsx
Normal file
@ -0,0 +1,150 @@
|
||||
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;
|
||||
@ -0,0 +1,22 @@
|
||||
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;
|
||||
22
frontend/src/components/select-and-edit/utils.ts
Normal file
22
frontend/src/components/select-and-edit/utils.ts
Normal file
@ -0,0 +1,22 @@
|
||||
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 };
|
||||
}
|
||||
37
frontend/src/components/settings/GenerationSettings.tsx
Normal file
37
frontend/src/components/settings/GenerationSettings.tsx
Normal file
@ -0,0 +1,37 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
@ -5,9 +5,9 @@ import {
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
} from "./ui/select";
|
||||
import { Badge } from "./ui/badge";
|
||||
import { Stack, STACK_DESCRIPTIONS } from "../lib/stacks";
|
||||
} from "../ui/select";
|
||||
import { Badge } from "../ui/badge";
|
||||
import { Stack, STACK_DESCRIPTIONS } from "../../lib/stacks";
|
||||
|
||||
function generateDisplayComponent(stack: Stack) {
|
||||
const stackComponents = STACK_DESCRIPTIONS[stack].components;
|
||||
@ -9,19 +9,19 @@ import {
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { FaCog } from "react-icons/fa";
|
||||
import { EditorTheme, Settings } from "../types";
|
||||
import { Switch } from "./ui/switch";
|
||||
import { Label } from "./ui/label";
|
||||
import { Input } from "./ui/input";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger } from "./ui/select";
|
||||
import { capitalize } from "../lib/utils";
|
||||
import { IS_RUNNING_ON_CLOUD } from "../config";
|
||||
import { EditorTheme, Settings } from "../../types";
|
||||
import { Switch } from "../ui/switch";
|
||||
import { Label } from "../ui/label";
|
||||
import { Input } from "../ui/input";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger } from "../ui/select";
|
||||
import { capitalize } from "../../lib/utils";
|
||||
import { IS_RUNNING_ON_CLOUD } from "../../config";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "./ui/accordion";
|
||||
} from "../ui/accordion";
|
||||
|
||||
interface Props {
|
||||
settings: Settings;
|
||||
@ -49,7 +49,7 @@ function SettingsDialog({ settings, setSettings }: Props) {
|
||||
<div className="flex items-center space-x-2">
|
||||
<Label htmlFor="image-generation">
|
||||
<div>DALL-E Placeholder Image Generation</div>
|
||||
<div className="font-light mt-2">
|
||||
<div className="font-light mt-2 text-xs">
|
||||
More fun with it but if you want to save money, turn it off.
|
||||
</div>
|
||||
</Label>
|
||||
@ -64,10 +64,11 @@ function SettingsDialog({ settings, setSettings }: Props) {
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col space-y-4">
|
||||
<div className="flex flex-col space-y-6">
|
||||
<div>
|
||||
<Label htmlFor="openai-api-key">
|
||||
<div>OpenAI API key</div>
|
||||
<div className="font-light mt-2 leading-relaxed">
|
||||
<div className="font-light mt-1 mb-2 text-xs leading-relaxed">
|
||||
Only stored in your browser. Never stored on servers. Overrides
|
||||
your .env config.
|
||||
</div>
|
||||
@ -84,9 +85,10 @@ function SettingsDialog({ settings, setSettings }: Props) {
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{!IS_RUNNING_ON_CLOUD && (
|
||||
<>
|
||||
<div>
|
||||
<Label htmlFor="openai-api-key">
|
||||
<div>OpenAI Base URL (optional)</div>
|
||||
<div className="font-light mt-2 leading-relaxed">
|
||||
@ -105,9 +107,31 @@ 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">
|
||||
<AccordionItem value="item-1">
|
||||
<AccordionTrigger>Screenshot by URL Config</AccordionTrigger>
|
||||
196
frontend/src/components/sidebar/Sidebar.tsx
Normal file
196
frontend/src/components/sidebar/Sidebar.tsx
Normal file
@ -0,0 +1,196 @@
|
||||
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;
|
||||
27
frontend/src/components/start-pane/StartPane.tsx
Normal file
27
frontend/src/components/start-pane/StartPane.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
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;
|
||||
42
frontend/src/components/variants/Variants.tsx
Normal file
42
frontend/src/components/variants/Variants.tsx
Normal file
@ -0,0 +1,42 @@
|
||||
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;
|
||||
@ -11,12 +11,18 @@ const ERROR_MESSAGE =
|
||||
|
||||
const CANCEL_MESSAGE = "Code generation cancelled";
|
||||
|
||||
type WebSocketResponse = {
|
||||
type: "chunk" | "status" | "setCode" | "error";
|
||||
value: string;
|
||||
variantIndex: number;
|
||||
};
|
||||
|
||||
export function generateCode(
|
||||
wsRef: React.MutableRefObject<WebSocket | null>,
|
||||
params: FullGenerationSettings,
|
||||
onChange: (chunk: string) => void,
|
||||
onSetCode: (code: string) => void,
|
||||
onStatusUpdate: (status: string) => void,
|
||||
onChange: (chunk: string, variantIndex: number) => void,
|
||||
onSetCode: (code: string, variantIndex: number) => void,
|
||||
onStatusUpdate: (status: string, variantIndex: number) => void,
|
||||
onCancel: () => void,
|
||||
onComplete: () => void
|
||||
) {
|
||||
@ -31,13 +37,13 @@ export function generateCode(
|
||||
});
|
||||
|
||||
ws.addEventListener("message", async (event: MessageEvent) => {
|
||||
const response = JSON.parse(event.data);
|
||||
const response = JSON.parse(event.data) as WebSocketResponse;
|
||||
if (response.type === "chunk") {
|
||||
onChange(response.value);
|
||||
onChange(response.value, response.variantIndex);
|
||||
} else if (response.type === "status") {
|
||||
onStatusUpdate(response.value);
|
||||
onStatusUpdate(response.value, response.variantIndex);
|
||||
} else if (response.type === "setCode") {
|
||||
onSetCode(response.value);
|
||||
onSetCode(response.value, response.variantIndex);
|
||||
} else if (response.type === "error") {
|
||||
console.error("Error generating code", response.value);
|
||||
toast.error(response.value);
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
// Keep in sync with backend (llm.py)
|
||||
// Order here matches dropdown order
|
||||
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",
|
||||
@ -11,8 +12,9 @@ export enum CodeGenerationModel {
|
||||
export const CODE_GENERATION_MODEL_DESCRIPTIONS: {
|
||||
[key in CodeGenerationModel]: { name: string; inBeta: boolean };
|
||||
} = {
|
||||
"gpt-4o-2024-05-13": { name: "GPT-4o 🌟", inBeta: false },
|
||||
"gpt-4-turbo-2024-04-09": { name: "GPT-4 Turbo (Apr 2024)", inBeta: false },
|
||||
gpt_4_vision: { name: "GPT-4 Vision (Nov 2023)", inBeta: false },
|
||||
claude_3_sonnet: { name: "Claude 3 Sonnet", inBeta: false },
|
||||
"gpt-4o-2024-05-13": { name: "GPT-4o", inBeta: false },
|
||||
"claude-3-5-sonnet-20240620": { name: "Claude 3.5 Sonnet", inBeta: false },
|
||||
"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 },
|
||||
};
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
// Keep in sync with backend (prompts/types.py)
|
||||
// Order here determines order in dropdown
|
||||
export enum Stack {
|
||||
HTML_TAILWIND = "html_tailwind",
|
||||
HTML_CSS = "html_css",
|
||||
REACT_TAILWIND = "react_tailwind",
|
||||
BOOTSTRAP = "bootstrap",
|
||||
VUE_TAILWIND = "vue_tailwind",
|
||||
@ -11,6 +13,7 @@ export enum Stack {
|
||||
export const STACK_DESCRIPTIONS: {
|
||||
[key in Stack]: { components: string[]; inBeta: boolean };
|
||||
} = {
|
||||
html_css: { components: ["HTML", "CSS"], inBeta: false },
|
||||
html_tailwind: { components: ["HTML", "Tailwind"], inBeta: false },
|
||||
react_tailwind: { components: ["React", "Tailwind"], inBeta: false },
|
||||
bootstrap: { components: ["Bootstrap"], inBeta: false },
|
||||
|
||||
14
frontend/src/lib/takeScreenshot.ts
Normal file
14
frontend/src/lib/takeScreenshot.ts
Normal file
@ -0,0 +1,14 @@
|
||||
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;
|
||||
};
|
||||
36
frontend/src/store/app-store.ts
Normal file
36
frontend/src/store/app-store.ts
Normal file
@ -0,0 +1,36 @@
|
||||
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 }),
|
||||
}));
|
||||
149
frontend/src/store/project-store.ts
Normal file
149
frontend/src/store/project-store.ts
Normal file
@ -0,0 +1,149 @@
|
||||
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: {} }),
|
||||
}));
|
||||
@ -16,14 +16,16 @@ describe("e2e tests", () => {
|
||||
let browser: Browser;
|
||||
let page: Page;
|
||||
|
||||
const DEBUG = false;
|
||||
const DEBUG = true;
|
||||
const IS_HEADLESS = true;
|
||||
|
||||
const stacks = Object.values(Stack).slice(0, DEBUG ? 1 : undefined);
|
||||
const models = Object.values(CodeGenerationModel).slice(
|
||||
0,
|
||||
DEBUG ? 1 : undefined
|
||||
);
|
||||
const models = DEBUG
|
||||
? [
|
||||
CodeGenerationModel.GPT_4O_2024_05_13,
|
||||
CodeGenerationModel.CLAUDE_3_5_SONNET_2024_06_20,
|
||||
]
|
||||
: Object.values(CodeGenerationModel);
|
||||
|
||||
beforeAll(async () => {
|
||||
browser = await puppeteer.launch({ headless: IS_HEADLESS });
|
||||
|
||||
@ -16,6 +16,7 @@ export interface Settings {
|
||||
codeGenerationModel: CodeGenerationModel;
|
||||
// Only relevant for hosted version
|
||||
isTermOfServiceAccepted: boolean;
|
||||
anthropicApiKey: string | null; // Added property for anthropic API key
|
||||
}
|
||||
|
||||
export enum AppState {
|
||||
|
||||
@ -4441,6 +4441,11 @@ nanoid@^3.3.6, nanoid@^3.3.7:
|
||||
resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz"
|
||||
integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==
|
||||
|
||||
nanoid@^5.0.7:
|
||||
version "5.0.7"
|
||||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-5.0.7.tgz#6452e8c5a816861fd9d2b898399f7e5fd6944cc6"
|
||||
integrity sha512-oLxFY2gd2IqnjcYyOXD8XGCftpGtZP2AbHbOkthDkvRywH5ayNtPVy9YlOPcHckXzbLTCHpkb7FB+yuxKV13pQ==
|
||||
|
||||
natural-compare@^1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz"
|
||||
@ -5642,6 +5647,11 @@ use-sidecar@^1.1.2:
|
||||
detect-node-es "^1.1.0"
|
||||
tslib "^2.0.0"
|
||||
|
||||
use-sync-external-store@1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a"
|
||||
integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==
|
||||
|
||||
util-deprecate@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz"
|
||||
@ -5937,3 +5947,10 @@ zod@3.22.4:
|
||||
version "3.22.4"
|
||||
resolved "https://registry.yarnpkg.com/zod/-/zod-3.22.4.tgz#f31c3a9386f61b1f228af56faa9255e845cf3fff"
|
||||
integrity sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==
|
||||
|
||||
zustand@^4.5.2:
|
||||
version "4.5.2"
|
||||
resolved "https://registry.yarnpkg.com/zustand/-/zustand-4.5.2.tgz#fddbe7cac1e71d45413b3682cdb47b48034c3848"
|
||||
integrity sha512-2cN1tPkDVkwCy5ickKrI7vijSjPksFRfqS6237NzT0vqSsztTNnQdHw9mmN7uBdk3gceVXU0a+21jFzFzAc9+g==
|
||||
dependencies:
|
||||
use-sync-external-store "1.2.0"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user