Compare commits

..

7 Commits

Author SHA1 Message Date
Abi Raja
a53afd350d update sdxl-lightning references to flux 2024-09-20 16:45:16 +02:00
Abi Raja
6899c7792e when all generations fail, print the all the underlying exceptions for debugging 2024-09-20 13:56:20 +02:00
Abi Raja
9199fee21d keep generating even if one of the models fails 2024-09-17 15:56:35 +02:00
Abi Raja
8717298def switch to flux schnell for replicate image gen 2024-09-17 12:20:19 +02:00
Abi Raja
a4087e613f make variant 1 claude 2024-09-16 15:21:57 +02:00
Abi Raja
cf6b94d675 update favicon/branding 2024-09-14 17:44:57 +02:00
Abi Raja
589507846b remove model selection dropdown since that happens on the background now 2024-09-13 14:41:00 +02:00
65 changed files with 573 additions and 2811 deletions

View File

@ -16,10 +16,10 @@ repos:
# 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/
# # - id: poetry-pyright
# # name: Run pyright with Poetry
# # entry: poetry run --directory backend pyright
# # language: system
# # pass_filenames: false
# # always_run: true
# # files: ^backend/

View File

@ -1,7 +1,5 @@
import re
import sentry_sdk
def extract_html_content(text: str):
# Use regex to find content within <html> tags and include the tags themselves
@ -13,8 +11,4 @@ def extract_html_content(text: str):
print(
"[HTML Extraction] No <html> tags found in the generated content: " + text
)
try:
raise Exception("No <html> tags found in the generated content")
except:
sentry_sdk.capture_exception()
return text

View File

@ -22,11 +22,3 @@ DEBUG_DIR = os.environ.get("DEBUG_DIR", "")
# Set to True when running in production (on the hosted version)
# Used as a feature flag to enable or disable certain features
IS_PROD = os.environ.get("IS_PROD", False)
# Hosted version only
PLATFORM_OPENAI_API_KEY = os.environ.get("PLATFORM_OPENAI_API_KEY", "")
PLATFORM_ANTHROPIC_API_KEY = os.environ.get("PLATFORM_ANTHROPIC_API_KEY", "")
PLATFORM_SCREENSHOTONE_API_KEY = os.environ.get("PLATFORM_SCREENSHOTONE_API_KEY", "")
BACKEND_SAAS_URL = os.environ.get("BACKEND_SAAS_URL", "")

View File

@ -4,5 +4,4 @@ from typing import Literal
InputMode = Literal[
"image",
"video",
"text",
]

View File

@ -3,7 +3,6 @@ import re
from typing import Dict, List, Literal, Union
from openai import AsyncOpenAI
from bs4 import BeautifulSoup
import sentry_sdk
from image_generation.replicate import call_replicate
@ -12,7 +11,7 @@ async def process_tasks(
prompts: List[str],
api_key: str,
base_url: str | None,
model: Literal["dalle3", "sdxl-lightning"],
model: Literal["dalle3", "flux"],
):
import time
@ -30,10 +29,6 @@ async def process_tasks(
for result in results:
if isinstance(result, BaseException):
print(f"An exception occurred: {result}")
try:
raise result
except Exception:
sentry_sdk.capture_exception()
processed_results.append(None)
else:
processed_results.append(result)
@ -59,18 +54,14 @@ async def generate_image_dalle(
async def generate_image_replicate(prompt: str, api_key: str) -> str:
# We use SDXL Lightning
# We use Flux Schnell
return await call_replicate(
"5f24084160c9089501c1b3545d9be3c27883ae2239b6f412990e82d4a6210f8f",
{
"width": 1024,
"height": 1024,
"prompt": prompt,
"scheduler": "K_EULER",
"num_outputs": 1,
"guidance_scale": 0,
"negative_prompt": "worst quality, low quality",
"num_inference_steps": 4,
"aspect_ratio": "1:1",
"output_format": "png",
"output_quality": 100,
},
api_key,
)
@ -107,7 +98,7 @@ async def generate_images(
api_key: str,
base_url: Union[str, None],
image_cache: Dict[str, str],
model: Literal["dalle3", "sdxl-lightning"] = "dalle3",
model: Literal["dalle3", "flux"] = "dalle3",
) -> str:
# Find all images
soup = BeautifulSoup(code, "html.parser")

View File

@ -2,20 +2,21 @@ import asyncio
import httpx
async def call_replicate(
replicate_model_version: str, input: dict[str, str | int], api_token: str
) -> str:
url = "https://api.replicate.com/v1/predictions"
async def call_replicate(input: dict[str, str | int], api_token: str) -> str:
headers = {
"Authorization": f"Bearer {api_token}",
"Content-Type": "application/json",
}
data = {"version": replicate_model_version, "input": input}
data = {"input": input}
async with httpx.AsyncClient() as client:
try:
response = await client.post(url, headers=headers, json=data)
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()
@ -24,16 +25,18 @@ async def call_replicate(
if not prediction_id:
raise ValueError("Prediction ID not found in initial response.")
# Polling every 1 second until the status is succeeded or error
# 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.2)
await asyncio.sleep(0.1)
# Check the status
status_check_url = f"{url}/{prediction_id}"
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()

View File

@ -4,7 +4,6 @@ from typing import Any, Awaitable, Callable, List, cast
from anthropic import AsyncAnthropic
from openai import AsyncOpenAI
from openai.types.chat import ChatCompletionMessageParam, ChatCompletionChunk
import sentry_sdk
from config import IS_DEBUG_ENABLED
from debug.DebugFileWriter import DebugFileWriter
from image_processing.utils import process_image
@ -13,7 +12,6 @@ from utils import pprint_prompt
# Actual model versions that are passed to the LLMs and stored in our logs
# Keep in sync with s2c-saas repo & DB column `llm_version`
class Llm(Enum):
GPT_4_VISION = "gpt-4-vision-preview"
GPT_4_TURBO_2024_04_09 = "gpt-4-turbo-2024-04-09"
@ -64,25 +62,6 @@ async def stream_openai_response(
full_response = ""
async for chunk in stream: # type: ignore
assert isinstance(chunk, ChatCompletionChunk)
# Log finish reason for OpenAI but don't halt streaming if it fails
try:
# Print finish reason if it exists
if (
chunk.choices
and len(chunk.choices) > 0
and chunk.choices[0].finish_reason
):
finish_reason = chunk.choices[0].finish_reason
print("[STOP REASON] OpenAI " + finish_reason)
if finish_reason == "length":
try:
raise Exception("OpenAI response too long")
except Exception as e:
sentry_sdk.capture_exception()
except Exception as e:
sentry_sdk.capture_exception(e)
if (
chunk.choices
and len(chunk.choices) > 0
@ -159,14 +138,6 @@ async def stream_claude_response(
# Return final message
response = await stream.get_final_message()
# Log stop reason
print("[STOP REASON] " + str(response.stop_reason))
if response.stop_reason == "max_tokens":
try:
raise Exception("Claude response too long")
except Exception:
sentry_sdk.capture_exception()
# Close the Anthropic client
await client.close()

View File

@ -4,27 +4,10 @@ from dotenv import load_dotenv
load_dotenv()
import os
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from routes import screenshot, generate_code, home, evals
from config import IS_PROD
# Setup Sentry (only relevant in prod)
if IS_PROD:
import sentry_sdk
SENTRY_DSN = os.environ.get("SENTRY_DSN")
if not SENTRY_DSN:
raise Exception("SENTRY_DSN not found in prod environment")
sentry_sdk.init(
dsn=SENTRY_DSN,
traces_sample_rate=0,
profiles_sample_rate=0.1,
)
# Setup FastAPI
app = FastAPI(openapi_url=None, docs_url=None, redoc_url=None)
# Configure CORS settings

782
backend/poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -5,7 +5,6 @@ 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.text_prompts import SYSTEM_PROMPTS as TEXT_SYSTEM_PROMPTS
from prompts.types import Stack
from video.utils import assemble_claude_prompt_video
@ -133,22 +132,3 @@ def assemble_prompt(
"content": user_content,
},
]
def assemble_text_prompt(
text_prompt: str,
stack: Stack,
) -> list[ChatCompletionMessageParam]:
system_content = TEXT_SYSTEM_PROMPTS[stack]
return [
{
"role": "system",
"content": system_content,
},
{
"role": "user",
"content": "Generate UI for " + text_prompt,
},
]

View File

@ -1,37 +0,0 @@
import unittest
from prompts.text_prompts import HTML_TAILWIND_SYSTEM_PROMPT
class TestTextPrompts(unittest.TestCase):
def test_html_tailwind_system_prompt(self):
self.maxDiff = None
print(HTML_TAILWIND_SYSTEM_PROMPT)
expected_prompt = """
You are an expert Tailwind developer.
- Make sure to make it look modern and sleek.
- Use modern, professional fonts and colors.
- Follow UX best practices.
- 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.
- For images, use placeholder images from https://placehold.co and include a detailed description of the image in the alt text so that an image generation AI can generate the image later.
In terms of libraries,
- Use this script to include Tailwind: <script src="https://cdn.tailwindcss.com"></script>
- 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.
Reply with only the code, and no text/explanation before and after the code.
"""
self.assertEqual(HTML_TAILWIND_SYSTEM_PROMPT.strip(), expected_prompt.strip())
if __name__ == "__main__":
unittest.main()

View File

@ -1,126 +0,0 @@
from prompts.types import SystemPrompts
GENERAL_INSTRUCTIONS = """
- Make sure to make it look modern and sleek.
- Use modern, professional fonts and colors.
- Follow UX best practices.
- 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.
- 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."""
LIBRARY_INSTRUCTIONS = """
- 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>"""
FORMAT_INSTRUCTIONS = """
Return only the full code in <html></html> tags.
Do not include markdown "```" or "```html" at the start or end.
Reply with only the code, and no text/explanation before and after the code.
"""
HTML_TAILWIND_SYSTEM_PROMPT = f"""
You are an expert Tailwind developer.
{GENERAL_INSTRUCTIONS}
In terms of libraries,
- Use this script to include Tailwind: <script src="https://cdn.tailwindcss.com"></script>
{LIBRARY_INSTRUCTIONS}
{FORMAT_INSTRUCTIONS}
"""
HTML_CSS_SYSTEM_PROMPT = f"""
You are an expert HTML, CSS and JS developer.
{GENERAL_INSTRUCTIONS}
In terms of libraries,
{LIBRARY_INSTRUCTIONS}
{FORMAT_INSTRUCTIONS}
"""
REACT_TAILWIND_SYSTEM_PROMPT = f"""
You are an expert React/Tailwind developer.
{GENERAL_INSTRUCTIONS}
In terms of libraries,
- Use these script to include React so that it can run on a standalone page:
<script src="https://unpkg.com/react/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.js"></script>
- Use this script to include Tailwind: <script src="https://cdn.tailwindcss.com"></script>
{LIBRARY_INSTRUCTIONS}
{FORMAT_INSTRUCTIONS}
"""
BOOTSTRAP_SYSTEM_PROMPT = f"""
You are an expert Bootstrap, HTML and JS developer.
{GENERAL_INSTRUCTIONS}
In terms of libraries,
- Use this script to include Bootstrap: <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
{LIBRARY_INSTRUCTIONS}
{FORMAT_INSTRUCTIONS}
"""
IONIC_TAILWIND_SYSTEM_PROMPT = f"""
You are an expert Ionic/Tailwind developer.
{GENERAL_INSTRUCTIONS}
In terms of libraries,
- Use these script to include Ionic so that it can run on a standalone page:
<script type="module" src="https://cdn.jsdelivr.net/npm/@ionic/core/dist/ionic/ionic.esm.js"></script>
<script nomodule src="https://cdn.jsdelivr.net/npm/@ionic/core/dist/ionic/ionic.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@ionic/core/css/ionic.bundle.css" />
- Use this script to include Tailwind: <script src="https://cdn.tailwindcss.com"></script>
- You can use Google Fonts
- ionicons for icons, add the following <script > tags near the end of the page, right before the closing </body> tag:
<script type="module">
import ionicons from 'https://cdn.jsdelivr.net/npm/ionicons/+esm'
</script>
<script nomodule src="https://cdn.jsdelivr.net/npm/ionicons/dist/esm/ionicons.min.js"></script>
<link href="https://cdn.jsdelivr.net/npm/ionicons/dist/collection/components/icon/icon.min.css" rel="stylesheet">
{FORMAT_INSTRUCTIONS}
"""
VUE_TAILWIND_SYSTEM_PROMPT = f"""
You are an expert Vue/Tailwind developer.
{GENERAL_INSTRUCTIONS}
In terms of libraries,
- Use these script to include Vue so that it can run on a standalone page:
<script src="https://registry.npmmirror.com/vue/3.3.11/files/dist/vue.global.js"></script>
- Use this script to include Tailwind: <script src="https://cdn.tailwindcss.com"></script>
{LIBRARY_INSTRUCTIONS}
{FORMAT_INSTRUCTIONS}
"""
SVG_SYSTEM_PROMPT = f"""
You are an expert at building SVGs.
{GENERAL_INSTRUCTIONS}
Return only the full code in <svg></svg> tags.
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,
ionic_tailwind=IONIC_TAILWIND_SYSTEM_PROMPT,
vue_tailwind=VUE_TAILWIND_SYSTEM_PROMPT,
svg=SVG_SYSTEM_PROMPT,
)

View File

@ -17,7 +17,6 @@ httpx = "^0.25.1"
pre-commit = "^3.6.2"
anthropic = "^0.18.0"
moviepy = "^1.0.3"
sentry-sdk = {extras = ["fastapi"], version = "^1.38.0"}
pillow = "^10.3.0"
types-pillow = "^10.2.0.20240520"
aiohttp = "^3.9.5"

View File

@ -1,15 +1,15 @@
import asyncio
from dataclasses import dataclass
import os
import traceback
from fastapi import APIRouter, WebSocket
import openai
from codegen.utils import extract_html_content
from config import (
ANTHROPIC_API_KEY,
IS_PROD,
NUM_VARIANTS,
OPENAI_API_KEY,
OPENAI_BASE_URL,
PLATFORM_ANTHROPIC_API_KEY,
PLATFORM_OPENAI_API_KEY,
REPLICATE_API_KEY,
SHOULD_MOCK_AI_RESPONSE,
)
@ -21,11 +21,8 @@ from llm import (
stream_claude_response_native,
stream_openai_response,
)
from fs_logging.core import write_logs
from mock_llm import mock_completion
from typing import Dict, List, cast, get_args
from image_generation.core import generate_images
from routes.logging_utils import PaymentMethod, send_to_saas_backend
from routes.saas_utils import does_user_have_subscription_credits
from typing import Any, Callable, Coroutine, Dict, List, Literal, cast, get_args
from image_generation.core import generate_images
from prompts import create_prompt
@ -67,7 +64,7 @@ async def perform_image_generation(
return completion
if replicate_api_key:
image_generation_model = "sdxl-lightning"
image_generation_model = "flux"
api_key = replicate_api_key
else:
if not openai_api_key:
@ -98,7 +95,6 @@ class ExtractedParams:
openai_api_key: str | None
anthropic_api_key: str | None
openai_base_url: str | None
payment_method: PaymentMethod
async def extract_params(
@ -128,56 +124,14 @@ async def extract_params(
await throw_error(f"Invalid model: {code_generation_model_str}")
raise ValueError(f"Invalid model: {code_generation_model_str}")
# Read the auth token from the request (on the hosted version)
auth_token = params.get("authToken")
if not auth_token:
await throw_error("You need to be logged in to use screenshot to code")
raise Exception("No auth token")
openai_api_key = None
anthropic_api_key = None
# Track how this generation is being paid for
payment_method: PaymentMethod = PaymentMethod.UNKNOWN
# If the user is a subscriber, use the platform API key
# TODO: Rename does_user_have_subscription_credits
res = await does_user_have_subscription_credits(auth_token)
if res.status != "not_subscriber":
if (
res.status == "subscriber_has_credits"
or res.status == "subscriber_is_trialing"
):
payment_method = (
PaymentMethod.SUBSCRIPTION
if res.status == "subscriber_has_credits"
else PaymentMethod.TRIAL
openai_api_key = get_from_settings_dialog_or_env(
params, "openAiApiKey", OPENAI_API_KEY
)
openai_api_key = PLATFORM_OPENAI_API_KEY
anthropic_api_key = PLATFORM_ANTHROPIC_API_KEY
print("Subscription - using platform API key")
elif res.status == "subscriber_has_no_credits":
await throw_error(
"Your subscription has run out of monthly credits. Contact support and we can add more credits to your account for free."
# 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
)
else:
await throw_error("Unknown error occurred. Contact support.")
raise Exception("Unknown error occurred when checking subscription credits")
# Use the user's OpenAI API key from the settings dialog if they are not a subscriber
if not openai_api_key:
openai_api_key = get_from_settings_dialog_or_env(params, "openAiApiKey", None)
if openai_api_key:
payment_method = PaymentMethod.OPENAI_API_KEY
print("Using OpenAI API key from user's settings dialog")
print("Payment method: ", payment_method)
if payment_method is PaymentMethod.UNKNOWN:
await throw_error(
"Please subscribe to a paid plan to generate code. If you are a subscriber and seeing this error, please contact support."
)
raise Exception("No payment method found")
# Base URL for OpenAI API
openai_base_url: str | None = None
@ -200,7 +154,6 @@ async def extract_params(
openai_api_key=openai_api_key,
anthropic_api_key=anthropic_api_key,
openai_base_url=openai_base_url,
payment_method=payment_method,
)
@ -261,7 +214,6 @@ async def stream_code(websocket: WebSocket):
openai_base_url = extracted_params.openai_base_url
anthropic_api_key = extracted_params.anthropic_api_key
should_generate_images = extracted_params.should_generate_images
payment_method = extracted_params.payment_method
# Auto-upgrade usage of older models
code_generation_model = auto_upgrade_model(code_generation_model)
@ -298,9 +250,6 @@ async def stream_code(websocket: WebSocket):
else:
try:
if input_mode == "video":
if IS_PROD:
raise Exception("Video mode is not supported in prod")
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 or in the settings dialog"
@ -318,11 +267,12 @@ async def stream_code(websocket: WebSocket):
)
]
else:
# 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 = ["openai", "anthropic"]
variant_models = ["anthropic", "openai"]
elif openai_api_key:
variant_models = ["openai", "openai"]
elif anthropic_api_key:
@ -333,7 +283,7 @@ async def stream_code(websocket: WebSocket):
)
raise Exception("No OpenAI or Anthropic key")
tasks: list[Coroutine[Any, Any, str]] = []
tasks: List[Coroutine[Any, Any, str]] = []
for index, model in enumerate(variant_models):
if model == "openai":
if openai_api_key is None:
@ -363,7 +313,29 @@ async def stream_code(websocket: WebSocket):
)
)
completions = await asyncio.gather(*tasks)
# 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:
@ -407,30 +379,10 @@ async def stream_code(websocket: WebSocket):
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
if IS_PROD:
# Catch any errors from sending to SaaS backend and continue
try:
# TODO*
# assert exact_llm_version is not None, "exact_llm_version is not set"
await send_to_saas_backend(
prompt_messages,
# TODO*: Store both completions
completions[0],
payment_method=payment_method,
# TODO*
llm_version=Llm.GPT_4O_2024_05_13,
stack=stack,
is_imported_from_code=bool(params.get("isImportedFromCode", False)),
includes_result_image=bool(params.get("resultImage", False)),
input_mode=input_mode,
auth_token=params["authToken"],
)
except Exception as e:
print("Error sending to SaaS backend", e)
write_logs(prompt_messages, completions[0])
## Image Generation
for index, _ in enumerate(completions):
await send_message("status", "Generating images...", index)

View File

@ -1,56 +0,0 @@
from enum import Enum
import httpx
from openai.types.chat import ChatCompletionMessageParam
from typing import List
import json
from config import BACKEND_SAAS_URL, IS_PROD
from custom_types import InputMode
from llm import Llm
from prompts.types import Stack
class PaymentMethod(Enum):
LEGACY = "legacy"
UNKNOWN = "unknown"
OPENAI_API_KEY = "openai_api_key"
SUBSCRIPTION = "subscription"
TRIAL = "trial"
async def send_to_saas_backend(
prompt_messages: List[ChatCompletionMessageParam],
completion: str,
payment_method: PaymentMethod,
llm_version: Llm,
stack: Stack,
is_imported_from_code: bool,
includes_result_image: bool,
input_mode: InputMode,
auth_token: str | None = None,
):
if IS_PROD:
async with httpx.AsyncClient() as client:
url = BACKEND_SAAS_URL + "/generations/store"
data = json.dumps(
{
"prompt": json.dumps(prompt_messages),
"completion": completion,
"payment_method": payment_method.value,
"llm_version": llm_version.value,
"stack": stack,
"is_imported_from_code": is_imported_from_code,
"includes_result_image": includes_result_image,
"input_mode": input_mode,
}
)
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {auth_token}", # Add the auth token to the headers
}
response = await client.post(url, content=data, headers=headers)
response_data = response.json()
return response_data

View File

@ -1,24 +0,0 @@
import httpx
from pydantic import BaseModel
from config import BACKEND_SAAS_URL
class SubscriptionCreditsResponse(BaseModel):
status: str
async def does_user_have_subscription_credits(
auth_token: str,
):
async with httpx.AsyncClient() as client:
url = BACKEND_SAAS_URL + "/credits/has_credits"
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {auth_token}",
}
response = await client.post(url, headers=headers, timeout=60)
parsed_response = SubscriptionCreditsResponse.parse_obj(response.json())
return parsed_response

View File

@ -2,9 +2,6 @@ import base64
from fastapi import APIRouter
from pydantic import BaseModel
import httpx
from config import PLATFORM_SCREENSHOTONE_API_KEY
from routes.saas_utils import does_user_have_subscription_credits
router = APIRouter()
@ -15,31 +12,10 @@ def bytes_to_data_url(image_bytes: bytes, mime_type: str) -> str:
async def capture_screenshot(
target_url: str, api_key: str | None, auth_token: str, device: str = "desktop"
target_url: str, api_key: str, device: str = "desktop"
) -> bytes:
api_base_url = "https://api.screenshotone.com/take"
# Get auth token
if not auth_token:
raise Exception("No auth token with capture_screenshot")
# TODO: Clean up this code and send the users correct error messages
# If API key is not passed in, only use the platform ScreenshotOne API key if the user is a subscriber
if not api_key:
res = await does_user_have_subscription_credits(auth_token)
if res.status == "not_subscriber":
raise Exception(
"capture_screenshot - User is not subscriber and has no API key"
)
elif res.status == "subscriber_has_credits":
api_key = PLATFORM_SCREENSHOTONE_API_KEY
elif res.status == "subscriber_has_no_credits":
raise Exception("capture_screenshot - User has no credits")
else:
raise Exception(
"capture_screenshot - Unknown error occurred when checking subscription credits"
)
params = {
"access_key": api_key,
"url": target_url,
@ -68,8 +44,7 @@ async def capture_screenshot(
class ScreenshotRequest(BaseModel):
url: str
apiKey: str | None
authToken: str
apiKey: str
class ScreenshotResponse(BaseModel):
@ -81,10 +56,9 @@ async def app_screenshot(request: ScreenshotRequest):
# Extract the URL from the request body
url = request.url
api_key = request.apiKey
auth_token = request.authToken
# TODO: Add error handling
image_bytes = await capture_screenshot(url, api_key=api_key, auth_token=auth_token)
image_bytes = await capture_screenshot(url, api_key=api_key)
# Convert the image bytes to a data url
data_url = bytes_to_data_url(image_bytes, "image/png")

View File

@ -41,7 +41,7 @@ OUTPUT_DIR: str = "generated_images"
async def generate_and_save_images(
prompts: List[str],
model: Literal["dalle3", "sdxl-lightning"],
model: Literal["dalle3", "flux"],
api_key: Optional[str],
) -> None:
# Ensure the output directory exists
@ -64,7 +64,7 @@ async def generate_and_save_images(
image_data: bytes = await response.read()
# Save the image with a filename based on the input eval
prefix = "replicate_" if model == "sdxl-lightning" else "dalle3_"
prefix = "replicate_" if model == "flux" else "dalle3_"
filename: str = (
f"{prefix}{prompts[i][:50].replace(' ', '_').replace(':', '')}.png"
)
@ -78,7 +78,7 @@ async def generate_and_save_images(
async def main() -> None:
# await generate_and_save_images(EVALS, "dalle3", OPENAI_API_KEY)
await generate_and_save_images(EVALS, "sdxl-lightning", REPLICATE_API_TOKEN)
await generate_and_save_images(EVALS, "flux", REPLICATE_API_TOKEN)
if __name__ == "__main__":

View File

@ -16,21 +16,6 @@
<!-- Injected code for hosted version -->
<%- injectHead %>
<!-- Google tag (gtag.js) -->
<script
async
src="https://www.googletagmanager.com/gtag/js?id=AW-16649848443"
></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag("js", new Date());
gtag("config", "AW-16649848443");
</script>
<title>Screenshot to Code</title>
<!-- Open Graph Meta Tags -->

View File

@ -13,16 +13,12 @@
"test": "jest"
},
"dependencies": {
"@clerk/clerk-react": "5.4.2",
"@codemirror/lang-html": "^6.4.6",
"@intercom/messenger-js-sdk": "^0.0.11",
"@radix-ui/react-accordion": "^1.1.2",
"@radix-ui/react-alert-dialog": "^1.0.5",
"@radix-ui/react-avatar": "^1.0.4",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-collapsible": "^1.0.3",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-hover-card": "^1.0.7",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.0.2",
@ -34,15 +30,12 @@
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-switch": "^1.0.3",
"@radix-ui/react-tabs": "^1.0.4",
"@stripe/stripe-js": "^2.2.2",
"@types/gtag.js": "^0.0.20",
"class-variance-authority": "^0.7.0",
"classnames": "^2.3.2",
"clsx": "^2.0.0",
"codemirror": "^6.0.1",
"copy-to-clipboard": "^3.3.3",
"html2canvas": "^1.4.1",
"posthog-js": "^1.128.1",
"nanoid": "^5.0.7",
"react": "^18.2.0",
"react-dom": "^18.2.0",
@ -50,8 +43,6 @@
"react-hot-toast": "^2.4.1",
"react-icons": "^4.12.0",
"react-router-dom": "^6.20.1",
"react-tweet": "^3.2.0",
"react-youtube": "^10.1.0",
"tailwind-merge": "^2.0.0",
"tailwindcss-animate": "^1.0.7",
"thememirror": "^2.0.1",

Binary file not shown.

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

View File

@ -1,46 +1,31 @@
import { useEffect, useRef, useState } from "react";
import { useEffect, useRef } from "react";
import { generateCode } from "./generateCode";
import { IS_FREE_TRIAL_ENABLED, IS_RUNNING_ON_CLOUD } from "./config";
import SettingsDialog from "./components/settings/SettingsDialog";
import { AppState, CodeGenerationParams, EditorTheme, Settings } from "./types";
import { IS_RUNNING_ON_CLOUD } from "./config";
import { PicoBadge } from "./components/messages/PicoBadge";
import { OnboardingNote } from "./components/messages/OnboardingNote";
import { usePersistedState } from "./hooks/usePersistedState";
import TermsOfServiceDialog from "./components/TermsOfServiceDialog";
import { USER_CLOSE_WEB_SOCKET_CODE } from "./constants";
import { addEvent } from "./lib/analytics";
import { extractHistory } from "./components/history/utils";
import toast from "react-hot-toast";
import { useAuth } from "@clerk/clerk-react";
import { useStore } from "./store/store";
import { Stack } from "./lib/stacks";
import { CodeGenerationModel } from "./lib/models";
import useBrowserTabIndicator from "./hooks/useBrowserTabIndicator";
import TipLink from "./components/messages/TipLink";
import { useAppStore } from "./store/app-store";
import GenerateFromText from "./components/generate-from-text/GenerateFromText";
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 Sidebar from "./components/sidebar/Sidebar";
import { Commit } from "./components/commits/types";
import { createCommit } from "./components/commits/utils";
interface Props {
navbarComponent?: JSX.Element;
}
function App({ navbarComponent }: Props) {
const [initialPrompt, setInitialPrompt] = useState<string>("");
// Relevant for hosted version only
// TODO: Move to AppContainer
const { getToken } = useAuth();
const subscriberTier = useStore((state) => state.subscriberTier);
function App() {
const {
// Inputs
inputMode,
@ -155,21 +140,12 @@ function App({ navbarComponent }: Props) {
return;
}
addEvent("Regenerate");
// Re-run the create
if (inputMode === "image" || inputMode === "video") {
doCreate(referenceImages, inputMode);
} else {
// TODO: Fix this
doCreateFromText(initialPrompt);
}
};
// Used when the user cancels the code generation
const cancelCodeGeneration = () => {
addEvent("Cancel");
wsRef.current?.close?.(USER_CLOSE_WEB_SOCKET_CODE);
};
@ -194,7 +170,7 @@ function App({ navbarComponent }: Props) {
}
};
async function doGenerateCode(params: CodeGenerationParams) {
function doGenerateCode(params: CodeGenerationParams) {
// Reset the execution console
resetExecutionConsoles();
@ -202,12 +178,7 @@ function App({ navbarComponent }: Props) {
setAppState(AppState.CODING);
// Merge settings with params
const authToken = await getToken();
const updatedParams = {
...params,
...settings,
authToken: authToken || undefined,
};
const updatedParams = { ...params, ...settings };
const baseCommitObject = {
variants: [{ code: "" }, { code: "" }],
@ -262,10 +233,7 @@ function App({ navbarComponent }: Props) {
}
// Initial version creation
async function doCreate(
referenceImages: string[],
inputMode: "image" | "video"
) {
function doCreate(referenceImages: string[], inputMode: "image" | "video") {
// Reset any existing state
reset();
@ -275,7 +243,6 @@ function App({ navbarComponent }: Props) {
// Kick off the code generation
if (referenceImages.length > 0) {
addEvent("Create");
doGenerateCode({
generationType: "create",
image: referenceImages[0],
@ -284,19 +251,6 @@ function App({ navbarComponent }: Props) {
}
}
function doCreateFromText(text: string) {
// Reset any existing state
reset();
setInputMode("text");
setInitialPrompt(text);
doGenerateCode({
generationType: "create",
inputMode: "text",
image: text,
});
}
// Subsequent updates
async function doUpdate(
updateInstruction: string,
@ -318,7 +272,6 @@ function App({ navbarComponent }: Props) {
try {
historyTree = extractHistory(head, commits);
} catch {
addEvent("HistoryTreeFailed");
toast.error(
"Version history is invalid. This shouldn't happen. Please contact support or open a Github issue."
);
@ -392,7 +345,7 @@ function App({ navbarComponent }: Props) {
{IS_RUNNING_ON_CLOUD && <PicoBadge />}
{IS_RUNNING_ON_CLOUD && (
<TermsOfServiceDialog
open={false}
open={!settings.isTermOfServiceAccepted}
onOpenChange={handleTermDialogOpenChange}
/>
)}
@ -405,11 +358,7 @@ function App({ navbarComponent }: Props) {
</div>
{/* Generation settings like stack and model */}
<GenerationSettings
settings={settings}
setSettings={setSettings}
selectedCodeGenerationModel={model}
/>
<GenerationSettings settings={settings} setSettings={setSettings} />
{/* Show auto updated message when older models are choosen */}
{showBetterModelMessage && <DeprecationMessage />}
@ -417,14 +366,7 @@ function App({ navbarComponent }: Props) {
{/* Show tip link until coding is complete */}
{appState !== AppState.CODE_READY && <TipLink />}
{IS_RUNNING_ON_CLOUD &&
!settings.openAiApiKey &&
!IS_FREE_TRIAL_ENABLED &&
subscriberTier === "free" && <OnboardingNote />}
{appState === AppState.INITIAL && (
<GenerateFromText doCreateFromText={doCreateFromText} />
)}
{IS_RUNNING_ON_CLOUD && !settings.openAiApiKey && <OnboardingNote />}
{/* Rest of the sidebar when we're not in the initial state */}
{(appState === AppState.CODING ||
@ -440,8 +382,6 @@ function App({ navbarComponent }: Props) {
</div>
<main className="py-2 lg:pl-96">
{!!navbarComponent && navbarComponent}
{appState === AppState.INITIAL && (
<StartPane
doCreate={doCreate}

View File

@ -7,8 +7,6 @@ import { URLS } from "../urls";
import { Badge } from "./ui/badge";
import ScreenRecorder from "./recording/ScreenRecorder";
import { ScreenRecorderState } from "../types";
import { IS_RUNNING_ON_CLOUD } from "../config";
import { addEvent } from "../lib/analytics";
const baseStyle = {
flex: 1,
@ -84,17 +82,6 @@ function ImageUpload({ setReferenceImages }: Props) {
"video/webm": [".webm"],
},
onDrop: (acceptedFiles) => {
if (IS_RUNNING_ON_CLOUD) {
const isVideo = acceptedFiles.some((file) =>
file.type.startsWith("video/")
);
if (isVideo) {
toast.error("Videos are not yet supported on the hosted version.");
addEvent("VideoUpload");
return;
}
}
// Set up the preview thumbnail images
setFiles(
acceptedFiles.map((file: File) =>
@ -186,13 +173,10 @@ function ImageUpload({ setReferenceImages }: Props) {
</p>
</div>
)}
{/* Disable on prod for now */}
{!IS_RUNNING_ON_CLOUD && (
<>
{screenRecorderState === ScreenRecorderState.INITIAL && (
<div className="text-center text-sm text-slate-800 mt-4">
<Badge>New!</Badge> Upload a screen recording (.mp4, .mov) or
record your screen to clone a whole app (experimental).{" "}
<Badge>New!</Badge> Upload a screen recording (.mp4, .mov) or record
your screen to clone a whole app (experimental).{" "}
<a
className="underline"
href={URLS["intro-to-video"]}
@ -207,8 +191,6 @@ function ImageUpload({ setReferenceImages }: Props) {
setScreenRecorderState={setScreenRecorderState}
generateCode={setReferenceImages}
/>
</>
)}
</section>
);
}

View File

@ -7,7 +7,9 @@ import {
AlertDialogHeader,
AlertDialogTitle,
} from "./ui/alert-dialog";
import { addEvent } from "../lib/analytics";
import { Input } from "./ui/input";
import toast from "react-hot-toast";
import { PICO_BACKEND_FORM_SECRET } from "../config";
const LOGOS = ["microsoft", "amazon", "mit", "stanford", "bytedance", "baidu"];
@ -15,19 +17,40 @@ const TermsOfServiceDialog: React.FC<{
open: boolean;
onOpenChange: (open: boolean) => void;
}> = ({ open, onOpenChange }) => {
const [email, setEmail] = React.useState("");
const onSubscribe = async () => {
await fetch("https://backend.buildpicoapps.com/form", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ email, secret: PICO_BACKEND_FORM_SECRET }),
});
};
return (
<AlertDialog open={open} onOpenChange={onOpenChange}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle className="mb-2 text-xl">
One last step
Enter your email to get started
</AlertDialogTitle>
</AlertDialogHeader>
<div className="mb-2">
<Input
placeholder="Email"
value={email}
onChange={(e) => {
setEmail(e.target.value);
}}
/>
</div>
<div className="flex flex-col space-y-3 text-sm">
<p>
You consent to receiving occasional product updates via email, and
you accept the{" "}
By providing your email, you consent to receiving occasional product
updates, and you accept the{" "}
<a
href="https://a.picoapps.xyz/camera-write"
target="_blank"
@ -53,8 +76,13 @@ const TermsOfServiceDialog: React.FC<{
<AlertDialogFooter>
<AlertDialogAction
onClick={() => {
addEvent("EmailSubmit");
onClick={(e) => {
if (!email.trim() || !email.trim().includes("@")) {
e.preventDefault();
toast.error("Please enter your email");
} else {
onSubscribe();
}
}}
>
Agree & Continue

View File

@ -3,8 +3,6 @@ import { HTTP_BACKEND_URL } from "../config";
import { Button } from "./ui/button";
import { Input } from "./ui/input";
import { toast } from "react-hot-toast";
import { useStore } from "../store/store";
import { useAuth } from "@clerk/clerk-react";
interface Props {
screenshotOneApiKey: string | null;
@ -15,31 +13,28 @@ export function UrlInputSection({ doCreate, screenshotOneApiKey }: Props) {
const [isLoading, setIsLoading] = useState(false);
const [referenceUrl, setReferenceUrl] = useState("");
// Hosted version only
const subscriberTier = useStore((state) => state.subscriberTier);
const { getToken } = useAuth();
async function takeScreenshot() {
if (!referenceUrl) {
return toast.error("Please enter a URL");
if (!screenshotOneApiKey) {
toast.error(
"Please add a ScreenshotOne API key in the Settings dialog. This is optional - you can also drag/drop and upload images directly.",
{ duration: 8000 }
);
return;
}
if (!screenshotOneApiKey && subscriberTier === "free") {
return toast.error(
"Please upgrade to a paid plan to use the screenshot feature."
);
if (!referenceUrl) {
toast.error("Please enter a URL");
return;
}
if (referenceUrl) {
try {
setIsLoading(true);
const authToken = await getToken();
const response = await fetch(`${HTTP_BACKEND_URL}/api/screenshot`, {
method: "POST",
body: JSON.stringify({
url: referenceUrl,
apiKey: screenshotOneApiKey,
authToken,
}),
headers: {
"Content-Type": "application/json",

View File

@ -1,11 +0,0 @@
import Spinner from "../core/Spinner";
function FullPageSpinner() {
return (
<div className="w-full h-screen flex items-center justify-center">
<Spinner />
</div>
);
}
export default FullPageSpinner;

View File

@ -1,57 +0,0 @@
import { useState, useRef, useEffect } from "react";
import { Button } from "../ui/button";
import { Textarea } from "../ui/textarea";
import toast from "react-hot-toast";
interface GenerateFromTextProps {
doCreateFromText: (text: string) => void;
}
function GenerateFromText({ doCreateFromText }: GenerateFromTextProps) {
const [isOpen, setIsOpen] = useState(false);
const [text, setText] = useState("");
const textareaRef = useRef<HTMLTextAreaElement>(null);
useEffect(() => {
if (isOpen && textareaRef.current) {
textareaRef.current.focus();
}
}, [isOpen]);
const handleGenerate = () => {
if (text.trim() === "") {
// Assuming there's a toast function available in the context
toast.error("Please enter a prompt to generate from");
return;
}
doCreateFromText(text);
};
return (
<div className="mt-4">
{!isOpen ? (
<div className="flex justify-center">
<Button variant="secondary" onClick={() => setIsOpen(true)}>
Generate from text prompt [BETA]
</Button>
</div>
) : (
<>
<Textarea
ref={textareaRef}
rows={2}
placeholder="A Saas admin dashboard"
className="w-full mb-4"
value={text}
onChange={(e) => setText(e.target.value)}
/>
<div className="flex justify-end">
<Button onClick={handleGenerate}>Generate</Button>
</div>
</>
)}
</div>
);
}
export default GenerateFromText;

View File

@ -46,7 +46,7 @@ export default function HistoryDisplay({ shouldDisableReverts }: Props) {
)}
>
<div
className="flex justify-between truncate flex-1 p-2 plausible-event-name=HistoryClick"
className="flex justify-between truncate flex-1 p-2"
onClick={() =>
shouldDisableReverts
? toast.error(

View File

@ -1,101 +0,0 @@
import { useUser } from "@clerk/clerk-react";
import posthog from "posthog-js";
import App from "../../App";
import { useEffect, useRef } from "react";
import FullPageSpinner from "../core/FullPageSpinner";
import { useAuthenticatedFetch } from "./useAuthenticatedFetch";
import { useStore } from "../../store/store";
import AvatarDropdown from "./AvatarDropdown";
import { UserResponse } from "./types";
import { POSTHOG_HOST, POSTHOG_KEY, SAAS_BACKEND_URL } from "../../config";
import LandingPage from "./LandingPage";
import Intercom from "@intercom/messenger-js-sdk";
function AppContainer() {
const { isSignedIn, isLoaded } = useUser();
const setSubscriberTier = useStore((state) => state.setSubscriberTier);
// For fetching user
const authenticatedFetch = useAuthenticatedFetch();
const isInitRequestInProgress = useRef(false);
// Get information from our backend about the user (subscription status)
useEffect(() => {
const init = async () => {
// Make sure there's only one request in progress
// so that we don't create multiple users
if (isInitRequestInProgress.current) return;
isInitRequestInProgress.current = true;
const user: UserResponse = await authenticatedFetch(
SAAS_BACKEND_URL + "/users/create",
"POST"
);
// If the user is not signed in, authenticatedFetch will return undefined
if (!user) {
isInitRequestInProgress.current = false;
return;
}
if (!user.subscriber_tier) {
setSubscriberTier("free");
} else {
// Initialize PostHog only for paid users
// and unmask all inputs except for passwords
posthog.init(POSTHOG_KEY, {
api_host: POSTHOG_HOST,
session_recording: {
maskAllInputs: false,
maskInputOptions: {
password: true,
},
},
});
// Identify the user to PostHog
posthog.identify(user.email, {
email: user.email,
first_name: user.first_name,
last_name: user.last_name,
});
setSubscriberTier(user.subscriber_tier);
}
// Initialize Intercom
Intercom({
app_id: "c5eiaj9m",
user_id: user.email,
name: user.first_name,
email: user.email,
"Subscriber Tier": user.subscriber_tier || "free",
});
isInitRequestInProgress.current = false;
};
init();
}, []);
// If Clerk is still loading, show a spinner
if (!isLoaded) return <FullPageSpinner />;
// If the user is not signed in, show the landing page
if (isLoaded && !isSignedIn) return <LandingPage />;
// If the user is signed in, show the app
return (
<>
<App
navbarComponent={
<div className="flex justify-end items-center gap-x-2 px-10 mt-0 mb-4">
<AvatarDropdown />
</div>
}
/>
</>
);
}
export default AppContainer;

View File

@ -1,154 +0,0 @@
import { useClerk, useUser } from "@clerk/clerk-react";
import { Avatar, AvatarImage, AvatarFallback } from "../ui/avatar";
import {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuLabel,
} from "../ui/dropdown-menu";
import { useStore } from "../../store/store";
import { capitalize } from "./utils";
import StripeCustomerPortalLink from "./StripeCustomerPortalLink";
import { Progress } from "../ui/progress";
import { useAuthenticatedFetch } from "./useAuthenticatedFetch";
import { SAAS_BACKEND_URL } from "../../config";
import { CreditsUsage } from "./types";
import { useState } from "react";
import toast from "react-hot-toast";
import { showNewMessage } from "@intercom/messenger-js-sdk";
import { URLS } from "../../urls";
export default function AvatarDropdown() {
const [isOpen, setIsOpen] = useState(false);
const [isLoadingUsage, setIsLoadingUsage] = useState(false);
const [usedCredits, setUsedCredits] = useState(0);
const [totalCredits, setTotalCredits] = useState(0);
const subscriberTier = useStore((state) => state.subscriberTier);
const setPricingDialogOpen = useStore((state) => state.setPricingDialogOpen);
const isFreeUser = subscriberTier === "free" || !subscriberTier;
const { user, isLoaded, isSignedIn } = useUser();
const { signOut } = useClerk();
const authenticatedFetch = useAuthenticatedFetch();
const openPricingDialog = () => setPricingDialogOpen(true);
async function open(isOpen: boolean) {
setIsOpen(isOpen);
// Do not fetch usage if the user is a free user
// or that information hasn't loaded yet
// or the dropdown is closed
if (isFreeUser || !subscriberTier || !isOpen) return;
setIsLoadingUsage(true);
try {
const res: CreditsUsage = await authenticatedFetch(
SAAS_BACKEND_URL + "/credits/usage",
"POST"
);
setUsedCredits(res.used_monthly_credits);
setTotalCredits(res.total_monthly_credits);
} catch (e) {
toast.error(
"Failed to fetch credit usage. Please contact support to get this issue fixed."
);
} finally {
setIsLoadingUsage(false);
}
}
// If Clerk is still loading or user is logged out, don't show anything
if (!isLoaded || !isSignedIn) return null;
return (
<>
<DropdownMenu open={isOpen} onOpenChange={open}>
<DropdownMenuTrigger asChild>
<div className="flex items-center space-x-2 cursor-pointer">
<span className="text-sm">Your account</span>
<Avatar className="w-8 h-8">
<AvatarImage src={user?.imageUrl} alt="Profile image" />
<AvatarFallback>{user?.firstName}</AvatarFallback>
</Avatar>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56" align="end">
{/* Free users */}
{isFreeUser && (
<DropdownMenuItem asChild={true}>
<a onClick={openPricingDialog}>Get pro</a>
</DropdownMenuItem>
)}
{/* Paying user */}
{!isFreeUser && (
<>
<DropdownMenuLabel onClick={openPricingDialog}>
{capitalize(subscriberTier) + " Subscriber"}
</DropdownMenuLabel>
{/* Loading credit usage */}
{isLoadingUsage && (
<DropdownMenuItem className="text-xs text-gray-700">
Loading credit usage...
</DropdownMenuItem>
)}
{/* Credits usage */}
{!isLoadingUsage && (
<>
<DropdownMenuItem onClick={openPricingDialog}>
<Progress value={(usedCredits / totalCredits) * 100} />
</DropdownMenuItem>
<DropdownMenuItem
className="text-xs text-gray-700"
onClick={openPricingDialog}
>
{usedCredits} out of {totalCredits} credits used for{" "}
{new Date().toLocaleString("default", { month: "long" })}.
{subscriberTier !== "pro" && (
<> Upgrade to Pro to get more credits.</>
)}
</DropdownMenuItem>
</>
)}
<DropdownMenuSeparator />
<DropdownMenuItem asChild={true}>
<a href={URLS.tips} target="_blank">
Tips for better results
</a>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem asChild={true}>
<a onClick={() => showNewMessage("")}>Contact support</a>
</DropdownMenuItem>
<DropdownMenuItem asChild={true}>
<a
href="https://screenshot-to-code.canny.io/feature-requests"
target="_blank"
>
Feature requests
</a>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem asChild={true}>
<StripeCustomerPortalLink label="Manage billing" />
</DropdownMenuItem>
<DropdownMenuItem asChild={true}>
<StripeCustomerPortalLink label="Cancel subscription" />
</DropdownMenuItem>
</>
)}
{/* All users */}
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => signOut()}>Log out</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</>
);
}

View File

@ -1,20 +0,0 @@
import React, { useEffect } from "react";
import { useNavigate } from "react-router-dom";
const CheckoutSuccessPage: React.FC = () => {
const navigate = useNavigate();
useEffect(() => {
// Redirect to home page after a short delay
const redirectTimer = setTimeout(() => {
navigate("/");
}, 200);
// Clean up the timer if the component unmounts
return () => clearTimeout(redirectTimer);
}, [navigate]);
return <div></div>;
};
export default CheckoutSuccessPage;

View File

@ -1,47 +0,0 @@
function FAQs() {
const faqs = [
{
question: "How do credits work?",
answer:
"Each creation, whether from a screenshot or text, consumes 1 credit. Every additional edit also consumes 1 credit. If you run out of credits, you can easily upgrade your plan to obtain more.",
},
{
question: "When do credits reset?",
answer:
"Your credits reset at the beginning of each month and do not roll over. Every month, on the 1st, you will receive a fresh batch of credits.",
},
{
question: "Can I cancel my plan?",
answer:
"Yes, you can cancel your plan at any time. Your plan will remain active until the end of the billing cycle.",
},
{
question: "Can I upgrade or downgrade my plan?",
answer:
"Yes, you can change your plan at any time. The changes will take effect immediately.",
},
{
question: "What payment methods do you accept?",
answer:
"We accept all major credit cards, Alipay, Amazon Pay and Cash App Pay. Certain payment methods may not be available in your country.",
},
];
return (
<div className="max-w-5xl mx-auto mb-16">
<h2 className="text-3xl font-bold mb-8 text-center">
Frequently Asked Questions
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
{faqs.map((faq, index) => (
<div key={index} className="border-b border-gray-200 pb-6">
<h3 className="text-lg font-semibold mb-2">{faq.question}</h3>
<p className="text-gray-600">{faq.answer}</p>
</div>
))}
</div>
</div>
);
}
export default FAQs;

View File

@ -1,14 +0,0 @@
import React from "react";
import Footer from "./LandingPage/Footer";
import FAQs from "./FAQs";
const FaqsPage: React.FC = () => {
return (
<div className="container mx-auto px-4 py-8">
<FAQs />
<Footer />
</div>
);
};
export default FaqsPage;

View File

@ -1,174 +0,0 @@
import { FaGithub } from "react-icons/fa";
import Footer from "./LandingPage/Footer";
import { Button } from "../ui/button";
import { SignUp } from "@clerk/clerk-react";
import { useState } from "react";
import { Dialog, DialogContent } from "../ui/dialog";
import { Tweet } from "react-tweet";
// import YouTube, { YouTubeProps } from "react-youtube";
const LOGOS = ["microsoft", "amazon", "mit", "stanford", "bytedance", "baidu"];
function LandingPage() {
const [isAuthPopupOpen, setIsAuthPopupOpen] = useState(false);
const signIn = () => {
setIsAuthPopupOpen(true);
};
// const youtubeOpts: YouTubeProps["opts"] = {
// height: "262.5", // Increased by 50%
// width: "480", // Increased by 50%
// playerVars: {
// autoplay: 1,
// },
// };
return (
<div className="w-full xl:w-[1000px] mx-auto mt-4">
{/* Auth dialog */}
<Dialog
open={isAuthPopupOpen}
onOpenChange={(value) => setIsAuthPopupOpen(value)}
>
<DialogContent className="flex justify-center">
<SignUp
fallbackRedirectUrl="/"
appearance={{
elements: {
// formButtonPrimary: "bg-slate-500 hover:bg-slate-400 text-sm",
cardBox: {
boxShadow: "none",
borderRadius: "0",
border: "none",
backgroundColor: "transparent",
},
card: {
borderRadius: "0",
border: "none",
backgroundColor: "transparent",
},
footer: {
display: "flex",
flexDirection: "column",
textAlign: "center",
background: "transparent",
},
footerAction: {
marginBottom: "5px",
},
},
layout: { privacyPageUrl: "https://a.picoapps.xyz/camera-write" },
}}
/>
</DialogContent>
</Dialog>
{/* Navbar */}
<nav className="border-b border-gray-200 px-4 py-2">
<div className="flex justify-between items-center">
<div className="text-lg font-semibold">Screenshot to Code</div>
<div className="flex items-center space-x-4">
<Button variant="secondary" onClick={signIn}>
Sign in
</Button>
<Button onClick={signIn}>Get started</Button>
</div>
</div>
</nav>
{/* Hero */}
<header className="px-4 py-16">
<div className="mx-auto">
<h2 className="text-5xl font-bold leading-tight mb-6">
Build User Interfaces 10x Faster
</h2>
<p className="text-gray-600 text-xl mb-6">
Convert any screenshot or design to clean code (with support for
most frameworks)
</p>
<div className="flex gap-4 flex-col sm:flex-row">
<Button size="lg" className="text-lg py-6 px-8" onClick={signIn}>
Get started
</Button>
<Button
variant="secondary"
onClick={() =>
window.open(
"https://github.com/abi/screenshot-to-code",
"_blank"
)
}
className="flex items-center space-x-2 text-gray-600 hover:text-gray-900 py-6 px-8"
>
<FaGithub size={24} />
<span>GitHub</span>
<span className="text-sm bg-gray-200 rounded-full px-2 py-1">
53,939 stars
</span>
</Button>
</div>
</div>
</header>
{/* Logo wall */}
<div className="mx-auto mt-12 px-4 sm:px-0">
<p className="text-gray-600 text-xl mb-10 text-center">
#1 tool used by developers and designers from leading companies. Fully
open source with 53,000+ stars on GitHub.
</p>
<div
className="mx-auto grid max-w-lg items-center gap-x-2
gap-y-10 sm:max-w-xl grid-cols-3 lg:mx-0 lg:max-w-none mt-10"
>
{LOGOS.map((companyName) => (
<img
key={companyName}
className="col-span-1 max-h-8 w-full object-contain
grayscale opacity-50 hover:opacity-100"
src={`https://picoapps.xyz/logos/${companyName}.png`}
alt={companyName}
width={120}
height={48}
/>
))}
</div>
</div>
{/* Video section */}
{/* <div className="px-4 mt-20 mb-10 text-center">
<video
src="/demos/youtube.mp4"
className="max-w-lg mx-auto rounded-md w-full sm:w-auto"
autoPlay
loop
muted
/>
<div className="mt-6">
Watch Screenshot to Code convert a screenshot of YouTube to
HTML/Tailwind
</div>
</div> */}
{/* Here's what users have to say */}
<div className="mt-16">
<h2 className="text-gray-600 text-2xl mb-4 text-center">
Here's what users have to say
</h2>
<div className="px-3 grid grid-cols-1 sm:grid-cols-2 gap-2 items-start justify-items-center">
{/* <YouTube videoId="b2xi5qiiTOI" opts={youtubeOpts} /> */}
<Tweet id="1733865178905661940" />
{/* <Tweet id="1727586760584991054" /> Other Rowan Cheung tweet */}
<Tweet id="1727105236811366669" />
<Tweet id="1732032876739224028" />
<Tweet id="1728496255473459339" />
</div>
</div>
{/* Footer */}
<Footer />
</div>
);
}
export default LandingPage;

View File

@ -1,35 +0,0 @@
const Footer = () => {
return (
<footer className="flex justify-between border-t border-gray-200 pt-4 mb-6 px-4 sm:px-0">
<div className="flex flex-col">
<span className="text-xl mb-2">Screenshot to Code</span>
<span className="text-xs">
© {new Date().getFullYear()} WhimsyWorks, Inc. All rights reserved.
</span>
{/* <div
className="bg-gray-800 text-white text-sm px-2 py-2
rounded-full flex items-center space-x-2"
>
<span>Built with</span>
<i className="fas fa-bolt text-yellow-400"></i>
<span>Screenshot to Code</span>
</div> */}
</div>
<div className="flex flex-col text-sm text-gray-600 mr-4">
<span className="uppercase">Company</span>
<div>WhimsyWorks Inc.</div>
<div>Made in NYC 🗽</div>
<a href="https://github.com/abi/screenshot-to-code" target="_blank">
Github
</a>
<a href="mailto:support@picoapps.xyz" target="_blank">
Contact
</a>
<a href="https://a.picoapps.xyz/camera-write" target="_blank">
Terms of Service
</a>
</div>
</footer>
);
};
export default Footer;

View File

@ -1,19 +0,0 @@
import React from "react";
import Footer from "./LandingPage/Footer";
import PricingPlans from "./payments/PricingPlans";
import FAQs from "./FAQs";
const PricingPage: React.FC = () => {
return (
<div className="container mx-auto px-4 py-8">
<h1 className="text-3xl font-bold mb-6">Screenshot to Code Pricing</h1>
<PricingPlans shouldShowFAQLink={false} />
{/* Spacer */}
<div className="text-center mt-8"></div>
<FAQs />
<Footer />
</div>
);
};
export default PricingPage;

View File

@ -1,48 +0,0 @@
import toast from "react-hot-toast";
import { useAuthenticatedFetch } from "./useAuthenticatedFetch";
import { addEvent } from "../../lib/analytics";
import { SAAS_BACKEND_URL } from "../../config";
import { PortalSessionResponse } from "./types";
import { forwardRef, useState } from "react";
import Spinner from "../core/Spinner";
interface Props {
label: string;
}
const StripeCustomerPortalLink = forwardRef<HTMLAnchorElement, Props>(
({ label, ...props }, ref) => {
const [isLoading, setIsLoading] = useState(false);
const authenticatedFetch = useAuthenticatedFetch();
const redirectToBillingPortal = async () => {
try {
setIsLoading(true);
const res: PortalSessionResponse = await authenticatedFetch(
SAAS_BACKEND_URL + "/payments/create_portal_session",
"POST"
);
window.location.href = res.url;
} catch (e) {
toast.error(
"Error directing you to the billing portal. Please email support and we'll get it fixed right away."
);
addEvent("StripeBillingPortalError");
} finally {
setIsLoading(false);
}
};
return (
<a {...props} ref={ref} onClick={redirectToBillingPortal}>
<div className="flex gap-x-2">
{label} {isLoading && <Spinner />}
</div>
</a>
);
}
);
StripeCustomerPortalLink.displayName = "StripeCustomerPortalLink";
export default StripeCustomerPortalLink;

View File

@ -1,70 +0,0 @@
import React from "react";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "../../ui/dialog";
import { useStore } from "../../../store/store";
import PricingPlans from "./PricingPlans";
const LOGOS = ["microsoft", "amazon", "mit", "stanford", "bytedance", "baidu"];
const PricingDialog: React.FC = () => {
const subscriberTier = useStore((state) => state.subscriberTier);
const [showDialog, setShowDialog] = useStore((state) => [
state.isPricingDialogOpen,
state.setPricingDialogOpen,
]);
return (
<Dialog open={showDialog} onOpenChange={(isOpen) => setShowDialog(isOpen)}>
{subscriberTier === "free" && (
<DialogTrigger
className="fixed z-50 bottom-28 right-5 rounded-md shadow-lg bg-black
text-white px-4 text-xs py-3 cursor-pointer"
>
get 100 code generations for $15
</DialogTrigger>
)}
<DialogContent className="max-w-4xl">
<DialogHeader>
<DialogTitle className="text-3xl text-center">
Ship Code Faster
</DialogTitle>
</DialogHeader>
<PricingPlans />
<DialogFooter></DialogFooter>
{/* Logos */}
<div className="max-w-lg mx-auto">
<div
className="mx-auto grid max-w-lg items-center gap-x-2
gap-y-10 sm:max-w-xl grid-cols-6 lg:mx-0 lg:max-w-none mt-4"
>
{LOGOS.map((companyName) => (
<img
key={companyName}
className="col-span-1 max-h-12 w-full object-contain grayscale opacity-50 hover:opacity-100"
src={`https://picoapps.xyz/logos/${companyName}.png`}
alt={companyName}
width={120}
height={48}
/>
))}
</div>
<div className="text-gray-600 leading-tight text-sm mt-4 text-center">
Designers and engineers from these organizations use Screenshot to
Code to build interfaces faster.
</div>
</div>
</DialogContent>
</Dialog>
);
};
export default PricingDialog;

View File

@ -1,145 +0,0 @@
import { FaCheckCircle } from "react-icons/fa";
import Spinner from "../../core/Spinner";
import React from "react";
import { Button } from "../../ui/button";
import useStripeCheckout from "./useStripeCheckout";
interface PricingPlansProps {
shouldShowFAQLink?: boolean;
}
function PricingPlans({ shouldShowFAQLink = true }: PricingPlansProps) {
const { checkout, isLoadingCheckout } = useStripeCheckout();
const [paymentInterval, setPaymentInterval] = React.useState<
"monthly" | "yearly"
>("monthly");
return (
<>
<div className="flex justify-center gap-x-2 mt-2">
<Button
variant={paymentInterval === "monthly" ? "default" : "secondary"}
onClick={() => setPaymentInterval("monthly")}
>
Monthly
</Button>
<Button
variant={paymentInterval === "yearly" ? "default" : "secondary"}
onClick={() => setPaymentInterval("yearly")}
>
Yearly (2 months free)
</Button>
</div>
<div className="flex justify-center items-center">
<div className="grid grid-cols-2 gap-8 p-2">
<div className="bg-white rounded-lg shadow p-6">
<h2 className="font-semibold">Hobby</h2>
<p className="text-gray-500">Great to start</p>
<div className="my-4">
<span className="text-4xl font-bold">
{paymentInterval === "monthly" ? "$15" : "$150"}
</span>
<span className="text-gray-500">
{paymentInterval === "monthly" ? "/ month" : "/ year"}
</span>
</div>
<button
className="bg-black text-white rounded py-2 px-4 w-full text-sm
flex justify-center items-center gap-x-2"
onClick={() =>
checkout(
paymentInterval === "monthly"
? "hobby_monthly"
: "hobby_yearly"
)
}
>
Subscribe {isLoadingCheckout && <Spinner />}
</button>
<ul className="mt-4 space-y-2">
<li className="flex items-center">
<FaCheckCircle className="text-black mr-2" />
100 credits / mo
</li>
<li className="flex items-center">
<FaCheckCircle className="text-black mr-2" />
All supported AI models
</li>
<li className="flex items-center">
<FaCheckCircle className="text-black mr-2" />
Full code access
</li>
<li className="flex items-center">
<FaCheckCircle className="text-black mr-2" />
Chat support
</li>
</ul>
</div>
<div className="bg-white rounded-lg shadow p-6">
<h2 className="font-semibold">Pro</h2>
<p className="text-gray-500">Higher limits</p>
<div className="my-4">
<span className="text-4xl font-bold">
{paymentInterval === "monthly" ? "$40" : "$400"}
</span>
<span className="text-gray-500">
{paymentInterval === "monthly" ? "/ month" : "/ year"}
</span>
</div>
<button
className="bg-black text-white rounded py-2 px-4 w-full text-sm
flex justify-center items-center gap-x-2"
onClick={() =>
checkout(
paymentInterval === "monthly" ? "pro_monthly" : "pro_yearly"
)
}
>
Subscribe {isLoadingCheckout && <Spinner />}
</button>
<ul className="mt-4 space-y-2">
<li className="flex items-center">
<FaCheckCircle className="text-black mr-2" />
300 credits / mo
</li>
<li className="flex items-center">
<FaCheckCircle className="text-black mr-2" />
All supported AI models
</li>
<li className="flex items-center">
<FaCheckCircle className="text-black mr-2" />
Full code access
</li>
<li className="flex items-center">
<FaCheckCircle className="text-black mr-2" />
Chat support
</li>
</ul>
</div>
</div>
</div>
<p className="text-center text-xs text-gray-600 mt-1">
1 credit = 1 code generation. Cancel subscription at any time. <br />{" "}
{shouldShowFAQLink && (
<>
<a
href="/pricing"
target="_blank"
className="text-blue-900 underline"
>
For more information, visit our FAQs
</a>{" "}
or contact support.
</>
)}
</p>
</>
);
}
export default PricingPlans;

View File

@ -1,71 +0,0 @@
import { useEffect, useState } from "react";
import toast from "react-hot-toast";
import { SAAS_BACKEND_URL, STRIPE_PUBLISHABLE_KEY } from "../../../config";
import { addEvent } from "../../../lib/analytics";
import { Stripe, loadStripe } from "@stripe/stripe-js";
import { useAuthenticatedFetch } from "../useAuthenticatedFetch";
interface CreateCheckoutSessionResponse {
sessionId: string;
}
export default function useStripeCheckout() {
const authenticatedFetch = useAuthenticatedFetch();
const [stripe, setStripe] = useState<Stripe | null>(null);
const [isLoadingCheckout, setIsLoadingCheckout] = useState(false);
const checkout = async (priceLookupKey: string) => {
const rewardfulReferralId = "xxx"; // TODO: Use later with Rewardful
if (!stripe) {
addEvent("StripeNotLoaded");
return;
}
try {
setIsLoadingCheckout(true);
// Create a Checkout Session
const res: CreateCheckoutSessionResponse = await authenticatedFetch(
`${SAAS_BACKEND_URL}/payments/create_checkout_session` +
`?price_lookup_key=${priceLookupKey}` +
`&rewardful_referral_id=${rewardfulReferralId}`,
"POST"
);
// Track going to checkout page as a conversion
gtag("event", "conversion", {
send_to: "AW-16649848443/AKZpCJbP2cYZEPuMooM-",
});
// Redirect to Stripe Checkout
const { error } = await stripe.redirectToCheckout({
sessionId: res.sessionId,
});
if (error) {
throw new Error(error.message);
}
} catch (e) {
toast.error("Error directing you to checkout. Please contact support.");
addEvent("StripeCheckoutError");
} finally {
setIsLoadingCheckout(false);
}
};
// Load Stripe when the component mounts
useEffect(() => {
async function load() {
try {
setStripe(await loadStripe(STRIPE_PUBLISHABLE_KEY));
} catch (e) {
console.error(e);
addEvent("StripeFailedToLoad");
}
}
load();
}, []);
return { checkout, isLoadingCheckout };
}

View File

@ -1,17 +0,0 @@
// Keep in sync with saas backend
export interface UserResponse {
email: string;
first_name: string;
last_name: string;
subscriber_tier: string;
stripe_customer_id: string;
}
export interface PortalSessionResponse {
url: string;
}
export interface CreditsUsage {
total_monthly_credits: number;
used_monthly_credits: number;
}

View File

@ -1,44 +0,0 @@
import { useAuth } from "@clerk/clerk-react";
type FetchMethod = "GET" | "POST" | "PUT" | "DELETE";
// Assumes that the backend is using JWTs for authentication
// and assumes JSON responses
// *If response code is not 200 OK or if there's any other error, throws an error
export const useAuthenticatedFetch = () => {
const { getToken } = useAuth();
const authenticatedFetch = async (
url: string,
method: FetchMethod = "GET",
body: object | null | undefined = null
) => {
const accessToken = await getToken();
if (!accessToken) return;
const headers: HeadersInit = {
"Content-Type": "application/json",
Authorization: `Bearer ${accessToken}`,
};
const options: RequestInit = {
method,
headers,
};
if (body) {
options.body = JSON.stringify(body);
}
const response = await fetch(url, options);
if (!response.ok) {
throw new Error(`HTTP error ${response.status}: ${response.statusText}`);
}
const json = await response.json();
return json;
};
return authenticatedFetch;
};

View File

@ -1,6 +0,0 @@
export function capitalize(str: string): string {
if (str.length === 0) {
return str;
}
return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
}

View File

@ -1,18 +1,25 @@
import { useStore } from "../../store/store";
export function OnboardingNote() {
const setPricingDialogOpen = useStore((state) => state.setPricingDialogOpen);
return (
<div className="flex flex-col space-y-4 bg-green-700 p-2 rounded text-stone-200 text-sm">
<span>
To use Screenshot to Code,{" "}
<a
className="inline underline hover:opacity-70 cursor-pointer"
onClick={() => setPricingDialogOpen(true)}
className="inline underline hover:opacity-70"
href="https://buy.stripe.com/8wM6sre70gBW1nqaEE"
target="_blank"
>
Subscribe to get started
</a>
buy some credits (100 generations for $36)
</a>{" "}
or use your own OpenAI API key with GPT4 vision access.{" "}
<a
href="https://github.com/abi/screenshot-to-code/blob/main/Troubleshooting.md"
className="inline underline hover:opacity-70"
target="_blank"
>
Follow these instructions to get yourself a key.
</a>{" "}
and paste it in the Settings dialog (gear icon above). Your key is only
stored in your browser. Never stored on our servers.
</span>
</div>
);

View File

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

View File

@ -58,14 +58,14 @@ function CodeTab({ code, setCode, settings }: Props) {
<div className="flex justify-start items-center px-4 mb-2">
<span
title="Copy Code"
className="bg-black text-white flex items-center justify-center hover:text-black hover:bg-gray-100 cursor-pointer rounded-lg text-sm p-2.5 plausible-event-name=CopyCode"
className="bg-black text-white flex items-center justify-center hover:text-black hover:bg-gray-100 cursor-pointer rounded-lg text-sm p-2.5"
onClick={copyCode}
>
Copy Code <FaCopy className="ml-2" />
</span>
<Button
onClick={doOpenInCodepenio}
className="bg-gray-100 text-black ml-2 py-2 px-4 border border-black rounded-md hover:bg-gray-400 focus:outline-none plausible-event-name=Codepen"
className="bg-gray-100 text-black ml-2 py-2 px-4 border border-black rounded-md hover:bg-gray-400 focus:outline-none"
>
Open in{" "}
<img

View File

@ -2,20 +2,16 @@ import React from "react";
import { useAppStore } from "../../store/app-store";
import { AppState, Settings } from "../../types";
import OutputSettingsSection from "./OutputSettingsSection";
import ModelSettingsSection from "./ModelSettingsSection";
import { Stack } from "../../lib/stacks";
import { CodeGenerationModel } from "../../lib/models";
interface GenerationSettingsProps {
settings: Settings;
setSettings: React.Dispatch<React.SetStateAction<Settings>>;
selectedCodeGenerationModel: CodeGenerationModel;
}
export const GenerationSettings: React.FC<GenerationSettingsProps> = ({
settings,
setSettings,
selectedCodeGenerationModel,
}) => {
const { appState } = useAppStore();
@ -26,13 +22,6 @@ export const GenerationSettings: React.FC<GenerationSettingsProps> = ({
}));
}
function setCodeGenerationModel(codeGenerationModel: CodeGenerationModel) {
setSettings((prev: Settings) => ({
...prev,
codeGenerationModel,
}));
}
const shouldDisableUpdates =
appState === AppState.CODING || appState === AppState.CODE_READY;
@ -43,12 +32,6 @@ export const GenerationSettings: React.FC<GenerationSettingsProps> = ({
setStack={setStack}
shouldDisableUpdates={shouldDisableUpdates}
/>
<ModelSettingsSection
codeGenerationModel={selectedCodeGenerationModel}
setCodeGenerationModel={setCodeGenerationModel}
shouldDisableUpdates={shouldDisableUpdates}
/>
</div>
);
};

View File

@ -1,69 +0,0 @@
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
} from "../ui/select";
import {
CODE_GENERATION_MODEL_DESCRIPTIONS,
CodeGenerationModel,
} from "../../lib/models";
import { IS_RUNNING_ON_CLOUD } from "../../config";
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 flex-col">
<div className="flex items-center">
<span className="font-semibold">
{CODE_GENERATION_MODEL_DESCRIPTIONS[model].name}
</span>
{!IS_RUNNING_ON_CLOUD &&
CODE_GENERATION_MODEL_DESCRIPTIONS[model].inBeta && (
<Badge className="ml-2" variant="secondary">
Beta
</Badge>
)}
</div>
</div>
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
</div>
</div>
);
}
export default ModelSettingsSection;

View File

@ -8,7 +8,6 @@ import {
} from "../ui/select";
import { Badge } from "../ui/badge";
import { Stack, STACK_DESCRIPTIONS } from "../../lib/stacks";
import { addEvent } from "../../lib/analytics";
function generateDisplayComponent(stack: Stack) {
const stackComponents = STACK_DESCRIPTIONS[stack].components;
@ -44,10 +43,7 @@ function OutputSettingsSection({
<span>{label}</span>
<Select
value={stack}
onValueChange={(value: string) => {
addEvent("OutputSettings", { stack: value });
setStack(value as Stack);
}}
onValueChange={(value: string) => setStack(value as Stack)}
disabled={shouldDisableUpdates}
>
<SelectTrigger className="col-span-2" id="output-settings-js">

View File

@ -1,48 +0,0 @@
import * as React from "react"
import * as AvatarPrimitive from "@radix-ui/react-avatar"
import { cn } from "@/lib/utils"
const Avatar = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Root
ref={ref}
className={cn(
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
className
)}
{...props}
/>
))
Avatar.displayName = AvatarPrimitive.Root.displayName
const AvatarImage = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Image>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Image
ref={ref}
className={cn("aspect-square h-full w-full", className)}
{...props}
/>
))
AvatarImage.displayName = AvatarPrimitive.Image.displayName
const AvatarFallback = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Fallback>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Fallback
ref={ref}
className={cn(
"flex h-full w-full items-center justify-center rounded-full bg-muted",
className
)}
{...props}
/>
))
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
export { Avatar, AvatarImage, AvatarFallback }

View File

@ -1,16 +1,16 @@
import * as React from "react";
import * as DialogPrimitive from "@radix-ui/react-dialog";
// import { Cross2Icon } from "@radix-ui/react-icons";
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { Cross2Icon } from "@radix-ui/react-icons"
import { cn } from "@/lib/utils";
import { cn } from "@/lib/utils"
const Dialog = DialogPrimitive.Root;
const Dialog = DialogPrimitive.Root
const DialogTrigger = DialogPrimitive.Trigger;
const DialogTrigger = DialogPrimitive.Trigger
const DialogPortal = DialogPrimitive.Portal;
const DialogPortal = DialogPrimitive.Portal
const DialogClose = DialogPrimitive.Close;
const DialogClose = DialogPrimitive.Close
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
@ -24,8 +24,8 @@ const DialogOverlay = React.forwardRef<
)}
{...props}
/>
));
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
@ -42,14 +42,14 @@ const DialogContent = React.forwardRef<
{...props}
>
{children}
{/* <DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<Cross2Icon className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close> */}
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
));
DialogContent.displayName = DialogPrimitive.Content.displayName;
))
DialogContent.displayName = DialogPrimitive.Content.displayName
const DialogHeader = ({
className,
@ -62,8 +62,8 @@ const DialogHeader = ({
)}
{...props}
/>
);
DialogHeader.displayName = "DialogHeader";
)
DialogHeader.displayName = "DialogHeader"
const DialogFooter = ({
className,
@ -76,8 +76,8 @@ const DialogFooter = ({
)}
{...props}
/>
);
DialogFooter.displayName = "DialogFooter";
)
DialogFooter.displayName = "DialogFooter"
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
@ -91,8 +91,8 @@ const DialogTitle = React.forwardRef<
)}
{...props}
/>
));
DialogTitle.displayName = DialogPrimitive.Title.displayName;
))
DialogTitle.displayName = DialogPrimitive.Title.displayName
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
@ -103,8 +103,8 @@ const DialogDescription = React.forwardRef<
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
));
DialogDescription.displayName = DialogPrimitive.Description.displayName;
))
DialogDescription.displayName = DialogPrimitive.Description.displayName
export {
Dialog,
@ -117,4 +117,4 @@ export {
DialogFooter,
DialogTitle,
DialogDescription,
};
}

View File

@ -1,203 +0,0 @@
import * as React from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import {
CheckIcon,
ChevronRightIcon,
DotFilledIcon,
} from "@radix-ui/react-icons"
import { cn } from "@/lib/utils"
const DropdownMenu = DropdownMenuPrimitive.Root
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
const DropdownMenuGroup = DropdownMenuPrimitive.Group
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
const DropdownMenuSub = DropdownMenuPrimitive.Sub
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
}
>(({ className, inset, children, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
inset && "pl-8",
className
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto h-4 w-4" />
</DropdownMenuPrimitive.SubTrigger>
))
DropdownMenuSubTrigger.displayName =
DropdownMenuPrimitive.SubTrigger.displayName
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
))
DropdownMenuSubContent.displayName =
DropdownMenuPrimitive.SubContent.displayName
const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
))
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
inset && "pl-8",
className
)}
{...props}
/>
))
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CheckIcon className="h-4 w-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
))
DropdownMenuCheckboxItem.displayName =
DropdownMenuPrimitive.CheckboxItem.displayName
const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<DropdownMenuPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<DotFilledIcon className="h-4 w-4 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
))
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label
ref={ref}
className={cn(
"px-2 py-1.5 text-sm font-semibold",
inset && "pl-8",
className
)}
{...props}
/>
))
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
const DropdownMenuShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
{...props}
/>
)
}
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
export {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup,
}

View File

@ -1,19 +0,0 @@
function FeedbackCallNote() {
return (
<div className="bg-blue-100 text-blue-800 p-4 rounded-lg">
<p className="text-sm">
Share your feedback with us on a{" "}
<a
href="https://dub.sh/DK4JOEY"
className="text-blue-800 underline"
target="_blank"
rel="noopener noreferrer"
>
15 min call (in English) and get $50 via Paypal or Amazon gift card.
</a>
</p>
</div>
);
}
export default FeedbackCallNote;

View File

@ -8,23 +8,5 @@ export const WS_BACKEND_URL =
export const HTTP_BACKEND_URL =
import.meta.env.VITE_HTTP_BACKEND_URL || "http://127.0.0.1:7001";
// Hosted version only
export const PICO_BACKEND_FORM_SECRET =
import.meta.env.VITE_PICO_BACKEND_FORM_SECRET || null;
export const CLERK_PUBLISHABLE_KEY =
import.meta.env.VITE_CLERK_PUBLISHABLE_KEY || null;
export const STRIPE_PUBLISHABLE_KEY =
import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY || null;
export const SAAS_BACKEND_URL = import.meta.env.VITE_SAAS_BACKEND_URL || null;
// Feature flags
export const SHOULD_SHOW_FEEDBACK_CALL_NOTE = false;
export const IS_FREE_TRIAL_ENABLED = false;
// PostHog
export const POSTHOG_KEY = import.meta.env.VITE_POSTHOG_KEY || null;
export const POSTHOG_HOST = import.meta.env.VITE_POSTHOG_HOST || null;

View File

@ -1,7 +0,0 @@
export function addEvent(eventName: string, props = {}) {
try {
window.plausible(eventName, { props });
} catch (e) {
// silently fail in non-production environments
}
}

View File

@ -10,26 +10,11 @@ export enum CodeGenerationModel {
// Will generate a static error if a model in the enum above is not in the descriptions
export const CODE_GENERATION_MODEL_DESCRIPTIONS: {
[key in CodeGenerationModel]: {
name: string;
inBeta: boolean;
};
[key in CodeGenerationModel]: { name: string; inBeta: boolean };
} = {
"gpt-4o-2024-05-13": { name: "GPT-4o", inBeta: false },
"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,
},
"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 },
};

View File

@ -1,37 +1,19 @@
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App.tsx";
import "./index.css";
import { Toaster } from "react-hot-toast";
import AppContainer from "./components/hosted/AppContainer.tsx";
import { ClerkProvider } from "@clerk/clerk-react";
import EvalsPage from "./components/evals/EvalsPage.tsx";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import { CLERK_PUBLISHABLE_KEY } from "./config.ts";
import "./index.css";
import PricingPage from "./components/hosted/PricingPage.tsx";
import CheckoutSuccessPage from "./components/hosted/CheckoutSuccessPage.tsx";
import FaqsPage from "./components/hosted/FaqsPage.tsx";
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<ClerkProvider
publishableKey={CLERK_PUBLISHABLE_KEY}
localization={{
footerPageLink__privacy:
"By signing up, you accept our terms of service and consent to receiving occasional product updates via email.",
}}
>
<Router>
<Routes>
<Route path="/" element={<AppContainer />} />
<Route path="/" element={<App />} />
<Route path="/evals" element={<EvalsPage />} />
<Route path="/pricing" element={<PricingPage />} />
<Route path="/faqs" element={<FaqsPage />} />
<Route path="/checkout-success" element={<CheckoutSuccessPage />} />
</Routes>
</Router>
<Toaster
toastOptions={{ className: "dark:bg-zinc-950 dark:text-white" }}
/>
</ClerkProvider>
<Toaster toastOptions={{ className: "dark:bg-zinc-950 dark:text-white" }} />
</React.StrictMode>
);

View File

@ -1,19 +0,0 @@
// plausible.d.ts
// Define the Plausible function type
type Plausible = (eventName: string, options?: PlausibleOptions) => void;
// Define the Plausible options type
interface PlausibleOptions {
callback?: () => void;
props?: Record<string, any>;
}
// Extend the Window interface to include the `plausible` function
declare global {
interface Window {
plausible: Plausible;
}
}
export {};

View File

@ -4,8 +4,8 @@ import { Commit, CommitHash } from "../components/commits/types";
// Store for app-wide state
interface ProjectStore {
// Inputs
inputMode: "image" | "video" | "text";
setInputMode: (mode: "image" | "video" | "text") => void;
inputMode: "image" | "video";
setInputMode: (mode: "image" | "video") => void;
isImportedFromCode: boolean;
setIsImportedFromCode: (imported: boolean) => void;
referenceImages: string[];

View File

@ -1,16 +0,0 @@
import { create } from "zustand";
interface Store {
isPricingDialogOpen: boolean;
setPricingDialogOpen: (isOpen: boolean) => void;
subscriberTier: string;
setSubscriberTier: (tier: string) => void;
}
export const useStore = create<Store>((set) => ({
isPricingDialogOpen: false,
setPricingDialogOpen: (isOpen: boolean) =>
set(() => ({ isPricingDialogOpen: isOpen })),
subscriberTier: "",
setSubscriberTier: (tier: string) => set(() => ({ subscriberTier: tier })),
}));

View File

@ -33,12 +33,11 @@ export enum ScreenRecorderState {
export interface CodeGenerationParams {
generationType: "create" | "update";
inputMode: "image" | "video" | "text";
inputMode: "image" | "video";
image: string;
resultImage?: string;
history?: string[];
isImportedFromCode?: boolean;
authToken?: string;
}
export type FullGenerationSettings = CodeGenerationParams & Settings;

View File

@ -16,7 +16,7 @@ export default ({ mode }) => {
inject: {
data: {
injectHead: process.env.VITE_IS_DEPLOYED
? '<script defer="" data-domain="screenshottocode.com" src="https://plausible.io/js/script.tagged-events.outbound-links.js"></script><script>window.plausible = window.plausible || function() { (window.plausible.q = window.plausible.q || []).push(arguments) }</script>'
? '<script defer="" data-domain="screenshottocode.com" src="https://plausible.io/js/script.js"></script>'
: "",
},
},

View File

@ -493,33 +493,6 @@
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
"@clerk/clerk-react@5.4.2":
version "5.4.2"
resolved "https://registry.yarnpkg.com/@clerk/clerk-react/-/clerk-react-5.4.2.tgz#866c23b83ef32cd27ff402c9e75ce8ab1149495a"
integrity sha512-F6F9yZ2lZDv365M6rn4Cpct0V2ZxFMoSxxRGag1du9QP73+twL7YoEeIimp+j4/RxH5uSsGv9JL2I7a6jnktHw==
dependencies:
"@clerk/shared" "2.5.2"
"@clerk/types" "4.14.0"
tslib "2.4.1"
"@clerk/shared@2.5.2":
version "2.5.2"
resolved "https://registry.yarnpkg.com/@clerk/shared/-/shared-2.5.2.tgz#03aba401cdd4ce91eede0978bba7f3ffd771f16c"
integrity sha512-3+I5vMhkn3wSqCuoxIIXRma3m8zpLJBH11MO2uxKOAeZJRyeALY0jGPeyptskl+Fl9j9Rtan0OWJIisMN8TiAA==
dependencies:
"@clerk/types" "4.14.0"
glob-to-regexp "0.4.1"
js-cookie "3.0.5"
std-env "^3.7.0"
swr "^2.2.0"
"@clerk/types@4.14.0":
version "4.14.0"
resolved "https://registry.yarnpkg.com/@clerk/types/-/types-4.14.0.tgz#2215d3a8337f984a401c5a819b793de622d223a5"
integrity sha512-d3MUcWtXGTOS7QYCRVrOra7NYAGRiNWlb+Ke2KMSb+Z3lea6WlKeHa18uVnAkIVxBXtHlzdU2kwW0PrHkx8j9Q==
dependencies:
csstype "3.1.1"
"@codemirror/autocomplete@^6.0.0":
version "6.11.0"
resolved "https://registry.yarnpkg.com/@codemirror/autocomplete/-/autocomplete-6.11.0.tgz#406dee8bf5342dfb48920ad75454d3406ddf9963"
@ -921,11 +894,6 @@
resolved "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz"
integrity sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==
"@intercom/messenger-js-sdk@^0.0.11":
version "0.0.11"
resolved "https://registry.yarnpkg.com/@intercom/messenger-js-sdk/-/messenger-js-sdk-0.0.11.tgz#ffdf37891826296d514a496e13a8d07e3d101c7e"
integrity sha512-jBHXO2+cGoBHYQMPaLP8eUm4AREcTWXlfd9shlBLSyEkFuW8+So/ynUDlftvWYz81KvGohRWYauw6vLRH/AlfA==
"@istanbuljs/load-nyc-config@^1.0.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced"
@ -1340,17 +1308,6 @@
"@babel/runtime" "^7.13.10"
"@radix-ui/react-primitive" "1.0.3"
"@radix-ui/react-avatar@^1.0.4":
version "1.0.4"
resolved "https://registry.yarnpkg.com/@radix-ui/react-avatar/-/react-avatar-1.0.4.tgz#de9a5349d9e3de7bbe990334c4d2011acbbb9623"
integrity sha512-kVK2K7ZD3wwj3qhle0ElXhOjbezIgyl2hVvgwfIdexL3rN6zJmy5AqqIf+D31lxVppdzV8CjAfZ6PklkmInZLw==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-context" "1.0.1"
"@radix-ui/react-primitive" "1.0.3"
"@radix-ui/react-use-callback-ref" "1.0.1"
"@radix-ui/react-use-layout-effect" "1.0.1"
"@radix-ui/react-checkbox@^1.0.4":
version "1.0.4"
resolved "https://registry.yarnpkg.com/@radix-ui/react-checkbox/-/react-checkbox-1.0.4.tgz#98f22c38d5010dd6df4c5744cac74087e3275f4b"
@ -1446,20 +1403,6 @@
"@radix-ui/react-use-callback-ref" "1.0.1"
"@radix-ui/react-use-escape-keydown" "1.0.3"
"@radix-ui/react-dropdown-menu@^2.0.6":
version "2.0.6"
resolved "https://registry.yarnpkg.com/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.0.6.tgz#cdf13c956c5e263afe4e5f3587b3071a25755b63"
integrity sha512-i6TuFOoWmLWq+M/eCLGd/bQ2HfAX1RJgvrBQ6AQLmzfvsLdefxbWu8G9zczcPFfcSPehz9GcpF6K9QYreFV8hA==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/primitive" "1.0.1"
"@radix-ui/react-compose-refs" "1.0.1"
"@radix-ui/react-context" "1.0.1"
"@radix-ui/react-id" "1.0.1"
"@radix-ui/react-menu" "2.0.6"
"@radix-ui/react-primitive" "1.0.3"
"@radix-ui/react-use-controllable-state" "1.0.1"
"@radix-ui/react-focus-guards@1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-guards/-/react-focus-guards-1.0.1.tgz#1ea7e32092216b946397866199d892f71f7f98ad"
@ -1514,31 +1457,6 @@
"@babel/runtime" "^7.13.10"
"@radix-ui/react-primitive" "1.0.3"
"@radix-ui/react-menu@2.0.6":
version "2.0.6"
resolved "https://registry.yarnpkg.com/@radix-ui/react-menu/-/react-menu-2.0.6.tgz#2c9e093c1a5d5daa87304b2a2f884e32288ae79e"
integrity sha512-BVkFLS+bUC8HcImkRKPSiVumA1VPOOEC5WBMiT+QAVsPzW1FJzI9KnqgGxVDPBcql5xXrHkD3JOVoXWEXD8SYA==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/primitive" "1.0.1"
"@radix-ui/react-collection" "1.0.3"
"@radix-ui/react-compose-refs" "1.0.1"
"@radix-ui/react-context" "1.0.1"
"@radix-ui/react-direction" "1.0.1"
"@radix-ui/react-dismissable-layer" "1.0.5"
"@radix-ui/react-focus-guards" "1.0.1"
"@radix-ui/react-focus-scope" "1.0.4"
"@radix-ui/react-id" "1.0.1"
"@radix-ui/react-popper" "1.1.3"
"@radix-ui/react-portal" "1.0.4"
"@radix-ui/react-presence" "1.0.1"
"@radix-ui/react-primitive" "1.0.3"
"@radix-ui/react-roving-focus" "1.0.4"
"@radix-ui/react-slot" "1.0.2"
"@radix-ui/react-use-callback-ref" "1.0.1"
aria-hidden "^1.1.1"
react-remove-scroll "2.5.5"
"@radix-ui/react-popover@^1.0.7":
version "1.0.7"
resolved "https://registry.yarnpkg.com/@radix-ui/react-popover/-/react-popover-1.0.7.tgz#23eb7e3327330cb75ec7b4092d685398c1654e3c"
@ -1877,18 +1795,6 @@
dependencies:
"@sinonjs/commons" "^3.0.0"
"@stripe/stripe-js@^2.2.2":
version "2.4.0"
resolved "https://registry.yarnpkg.com/@stripe/stripe-js/-/stripe-js-2.4.0.tgz#7a7e5b187b9e9bb43073edd946ec3e9a778e61bd"
integrity sha512-WFkQx1mbs2b5+7looI9IV1BLa3bIApuN3ehp9FP58xGg7KL9hCHDECgW3BwO9l9L+xBPVAD7Yjn1EhGe6EDTeA==
"@swc/helpers@^0.5.3":
version "0.5.11"
resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.5.11.tgz#5bab8c660a6e23c13b2d23fcd1ee44a2db1b0cb7"
integrity sha512-YNlnKRWF2sVojTpIyzwou9XoTNbzbzONwRhOoniEioF1AtaitTvVZblaQRrAzChWQ1bLYyYSWzM18y4WwgzJ+A==
dependencies:
tslib "^2.4.0"
"@tootallnate/quickjs-emscripten@^0.23.0":
version "0.23.0"
resolved "https://registry.yarnpkg.com/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz#db4ecfd499a9765ab24002c3b696d02e6d32a12c"
@ -1952,11 +1858,6 @@
dependencies:
"@types/node" "*"
"@types/gtag.js@^0.0.20":
version "0.0.20"
resolved "https://registry.yarnpkg.com/@types/gtag.js/-/gtag.js-0.0.20.tgz#e47edabb4ed5ecac90a079275958e6c929d7c08a"
integrity sha512-wwAbk3SA2QeU67unN7zPxjEHmPmlXwZXZvQEpbEUQuMCRGgKyE1m6XDuTUA9b6pCGb/GqJmdfMOY5LuDjJSbbg==
"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1":
version "2.0.6"
resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz#7739c232a1fee9b4d3ce8985f314c0c6d33549d7"
@ -2698,11 +2599,6 @@ clean-css@^5.2.2:
dependencies:
source-map "~0.6.0"
client-only@^0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/client-only/-/client-only-0.0.1.tgz#38bba5d403c41ab150bff64a95c85013cf73bca1"
integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==
cliui@^8.0.1:
version "8.0.1"
resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa"
@ -2876,11 +2772,6 @@ cssesc@^3.0.0:
resolved "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz"
integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==
csstype@3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.1.tgz#841b532c45c758ee546a11d5bd7b7b473c8c30b9"
integrity sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==
csstype@^3.0.2:
version "3.1.2"
resolved "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz"
@ -2898,13 +2789,6 @@ debug@4, debug@4.3.4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, de
dependencies:
ms "2.1.2"
debug@^2.6.6:
version "2.6.9"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
dependencies:
ms "2.0.0"
dedent@^1.0.0:
version "1.5.3"
resolved "https://registry.yarnpkg.com/dedent/-/dedent-1.5.3.tgz#99aee19eb9bae55a67327717b6e848d0bf777e5a"
@ -3353,9 +3237,9 @@ extract-zip@2.0.1:
optionalDependencies:
"@types/yauzl" "^2.9.1"
fast-deep-equal@3.1.3, fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
version "3.1.3"
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz"
integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
fast-fifo@^1.1.0, fast-fifo@^1.2.0:
@ -3405,11 +3289,6 @@ fd-slicer@~1.1.0:
dependencies:
pend "~1.2.0"
fflate@^0.4.8:
version "0.4.8"
resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.4.8.tgz#f90b82aefbd8ac174213abb338bd7ef848f0f5ae"
integrity sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==
file-entry-cache@^6.0.1:
version "6.0.1"
resolved "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz"
@ -3581,11 +3460,6 @@ glob-parent@^6.0.2:
dependencies:
is-glob "^4.0.3"
glob-to-regexp@0.4.1:
version "0.4.1"
resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e"
integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==
glob@7.1.6:
version "7.1.6"
resolved "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz"
@ -4279,11 +4153,6 @@ jiti@^1.19.1:
resolved "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz"
integrity sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==
js-cookie@3.0.5:
version "3.0.5"
resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-3.0.5.tgz#0b7e2fd0c01552c58ba86e0841f94dc2557dcdbc"
integrity sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==
"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
version "4.0.0"
resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz"
@ -4388,11 +4257,6 @@ lines-and-columns@^1.1.6:
resolved "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz"
integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==
load-script@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/load-script/-/load-script-1.0.0.tgz#0491939e0bee5643ee494a7e3da3d2bac70c6ca4"
integrity sha512-kPEjMFtZvwL9TaZo0uZ2ml+Ye9HUMmPwbYRJ324qF9tqMejwykJ5ggTyvzmrbBeapCAbk98BSbTeovHEEP1uCA==
local-pkg@^0.5.0:
version "0.5.0"
resolved "https://registry.yarnpkg.com/local-pkg/-/local-pkg-0.5.0.tgz#093d25a346bae59a99f80e75f6e9d36d7e8c925c"
@ -4558,11 +4422,6 @@ mlly@^1.2.0, mlly@^1.4.2:
pkg-types "^1.0.3"
ufo "^1.3.0"
ms@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==
ms@2.1.2:
version "2.1.2"
resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz"
@ -4945,19 +4804,6 @@ postcss@^8.4.32:
picocolors "^1.0.0"
source-map-js "^1.0.2"
posthog-js@^1.128.1:
version "1.136.2"
resolved "https://registry.yarnpkg.com/posthog-js/-/posthog-js-1.136.2.tgz#a90bc908665f7bbc9b366e16a0a38e40aacef7dc"
integrity sha512-9oTUB/JDayzV+hB4f7u+ZNUbfnkGHLxyZw+FOE59pCgmbWHcJxhpGbu2Xlyv027/iHIjQbn1mtm2wJmBI2BuqA==
dependencies:
fflate "^0.4.8"
preact "^10.19.3"
preact@^10.19.3:
version "10.22.0"
resolved "https://registry.yarnpkg.com/preact/-/preact-10.22.0.tgz#a50f38006ae438d255e2631cbdaf7488e6dd4e16"
integrity sha512-RRurnSjJPj4rp5K6XoP45Ui33ncb7e4H7WiOHVpjbkvqvA3U+N8Z6Qbo0AE6leGYBV66n8EhEaFixvIu3SkxFw==
prelude-ls@^1.2.1:
version "1.2.1"
resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz"
@ -4985,7 +4831,7 @@ prompts@^2.0.1:
kleur "^3.0.3"
sisteransi "^1.0.5"
prop-types@15.8.1, prop-types@^15.8.1:
prop-types@^15.8.1:
version "15.8.1"
resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz"
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
@ -5170,24 +5016,6 @@ react-style-singleton@^2.2.1:
invariant "^2.2.4"
tslib "^2.0.0"
react-tweet@^3.2.0:
version "3.2.1"
resolved "https://registry.yarnpkg.com/react-tweet/-/react-tweet-3.2.1.tgz#000d9bf2b2ce919fdec0e14241f05631e8917143"
integrity sha512-dktP3RMuwRB4pnSDocKpSsW5Hq1IXRW6fONkHhxT5EBIXsKZzdQuI70qtub1XN2dtZdkJWWxfBm/Q+kN+vRYFA==
dependencies:
"@swc/helpers" "^0.5.3"
clsx "^2.0.0"
swr "^2.2.4"
react-youtube@^10.1.0:
version "10.1.0"
resolved "https://registry.yarnpkg.com/react-youtube/-/react-youtube-10.1.0.tgz#7e5670c764f12eb408166e8eb438d788dc64e8b5"
integrity sha512-ZfGtcVpk0SSZtWCSTYOQKhfx5/1cfyEW1JN/mugGNfAxT3rmVJeMbGpA9+e78yG21ls5nc/5uZJETE3cm3knBg==
dependencies:
fast-deep-equal "3.1.3"
prop-types "15.8.1"
youtube-player "5.5.2"
react@^18.2.0:
version "18.2.0"
resolved "https://registry.npmjs.org/react/-/react-18.2.0.tgz"
@ -5358,11 +5186,6 @@ signal-exit@^4.1.0:
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04"
integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==
sister@^3.0.0:
version "3.0.2"
resolved "https://registry.yarnpkg.com/sister/-/sister-3.0.2.tgz#bb3e39f07b1f75bbe1945f29a27ff1e5a2f26be4"
integrity sha512-p19rtTs+NksBRKW9qn0UhZ8/TUI9BPw9lmtHny+Y3TinWlOa9jWh9xB0AtPSdmOy49NJJJSSe0Ey4C7h0TrcYA==
sisteransi@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed"
@ -5448,11 +5271,6 @@ std-env@^3.5.0:
resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.6.0.tgz#94807562bddc68fa90f2e02c5fd5b6865bb4e98e"
integrity sha512-aFZ19IgVmhdB2uX599ve2kE6BIE3YMnQ6Gp6BURhW/oIzpXGKr878TQfAQZn1+i0Flcc/UKUy1gOlcfaUBCryg==
std-env@^3.7.0:
version "3.7.0"
resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.7.0.tgz#c9f7386ced6ecf13360b6c6c55b8aaa4ef7481d2"
integrity sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==
streamx@^2.13.0, streamx@^2.15.0:
version "2.16.1"
resolved "https://registry.yarnpkg.com/streamx/-/streamx-2.16.1.tgz#2b311bd34832f08aa6bb4d6a80297c9caef89614"
@ -5558,14 +5376,6 @@ supports-preserve-symlinks-flag@^1.0.0:
resolved "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz"
integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
swr@^2.2.0, swr@^2.2.4:
version "2.2.5"
resolved "https://registry.yarnpkg.com/swr/-/swr-2.2.5.tgz#063eea0e9939f947227d5ca760cc53696f46446b"
integrity sha512-QtxqyclFeAsxEUeZIYmsaQ0UjimSq1RZ9Un7I68/0ClKK/U3LoyQunwkQfJZr2fc22DfIXLNDc2wFyTEikCUpg==
dependencies:
client-only "^0.0.1"
use-sync-external-store "^1.2.0"
tailwind-merge@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/tailwind-merge/-/tailwind-merge-2.0.0.tgz#a0f3a8c874ebae5feec5595614d08245a5f88a39"
@ -5747,11 +5557,6 @@ ts-jest@^29.1.2:
semver "^7.5.3"
yargs-parser "^21.0.1"
tslib@2.4.1:
version "2.4.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.1.tgz#0d0bfbaac2880b91e22df0768e55be9753a5b17e"
integrity sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==
tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.4.0:
version "2.6.2"
resolved "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz"
@ -5847,11 +5652,6 @@ use-sync-external-store@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==
use-sync-external-store@^1.2.0:
version "1.2.2"
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz#c3b6390f3a30eba13200d2302dcdf1e7b57b2ef9"
integrity sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==
util-deprecate@^1.0.2:
version "1.0.2"
resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz"
@ -6143,15 +5943,6 @@ yocto-queue@^1.0.0:
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-1.0.0.tgz#7f816433fb2cbc511ec8bf7d263c3b58a1a3c251"
integrity sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==
youtube-player@5.5.2:
version "5.5.2"
resolved "https://registry.yarnpkg.com/youtube-player/-/youtube-player-5.5.2.tgz#052b86b1eabe21ff331095ffffeae285fa7f7cb5"
integrity sha512-ZGtsemSpXnDky2AUYWgxjaopgB+shFHgXVpiJFeNB5nWEugpW1KWYDaHKuLqh2b67r24GtP6HoSW5swvf0fFIQ==
dependencies:
debug "^2.6.6"
load-script "^1.0.0"
sister "^3.0.0"
zod@3.22.4:
version "3.22.4"
resolved "https://registry.yarnpkg.com/zod/-/zod-3.22.4.tgz#f31c3a9386f61b1f228af56faa9255e845cf3fff"