Compare commits

..

2 Commits
hosted ... main

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
62 changed files with 975 additions and 3307 deletions

View File

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

View File

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

View File

@ -22,12 +22,3 @@ DEBUG_DIR = os.environ.get("DEBUG_DIR", "")
# Set to True when running in production (on the hosted version) # Set to True when running in production (on the hosted version)
# Used as a feature flag to enable or disable certain features # Used as a feature flag to enable or disable certain features
IS_PROD = os.environ.get("IS_PROD", False) 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", "")
BACKEND_SAAS_API_SECRET = os.environ.get("BACKEND_SAAS_API_SECRET", "")

View File

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

View File

@ -3,7 +3,6 @@ import re
from typing import Dict, List, Literal, Union from typing import Dict, List, Literal, Union
from openai import AsyncOpenAI from openai import AsyncOpenAI
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
import sentry_sdk
from image_generation.replicate import call_replicate from image_generation.replicate import call_replicate
@ -12,7 +11,7 @@ async def process_tasks(
prompts: List[str], prompts: List[str],
api_key: str, api_key: str,
base_url: str | None, base_url: str | None,
model: Literal["dalle3", "sdxl-lightning"], model: Literal["dalle3", "flux"],
): ):
import time import time
@ -30,10 +29,6 @@ async def process_tasks(
for result in results: for result in results:
if isinstance(result, BaseException): if isinstance(result, BaseException):
print(f"An exception occurred: {result}") print(f"An exception occurred: {result}")
try:
raise result
except Exception:
sentry_sdk.capture_exception()
processed_results.append(None) processed_results.append(None)
else: else:
processed_results.append(result) processed_results.append(result)
@ -103,7 +98,7 @@ async def generate_images(
api_key: str, api_key: str,
base_url: Union[str, None], base_url: Union[str, None],
image_cache: Dict[str, str], image_cache: Dict[str, str],
model: Literal["dalle3", "sdxl-lightning"] = "dalle3", model: Literal["dalle3", "flux"] = "dalle3",
) -> str: ) -> str:
# Find all images # Find all images
soup = BeautifulSoup(code, "html.parser") soup = BeautifulSoup(code, "html.parser")

View File

@ -4,7 +4,6 @@ from typing import Any, Awaitable, Callable, List, cast
from anthropic import AsyncAnthropic from anthropic import AsyncAnthropic
from openai import AsyncOpenAI from openai import AsyncOpenAI
from openai.types.chat import ChatCompletionMessageParam, ChatCompletionChunk from openai.types.chat import ChatCompletionMessageParam, ChatCompletionChunk
import sentry_sdk
from config import IS_DEBUG_ENABLED from config import IS_DEBUG_ENABLED
from debug.DebugFileWriter import DebugFileWriter from debug.DebugFileWriter import DebugFileWriter
from image_processing.utils import process_image 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 # 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): class Llm(Enum):
GPT_4_VISION = "gpt-4-vision-preview" GPT_4_VISION = "gpt-4-vision-preview"
GPT_4_TURBO_2024_04_09 = "gpt-4-turbo-2024-04-09" GPT_4_TURBO_2024_04_09 = "gpt-4-turbo-2024-04-09"
@ -64,25 +62,6 @@ async def stream_openai_response(
full_response = "" full_response = ""
async for chunk in stream: # type: ignore async for chunk in stream: # type: ignore
assert isinstance(chunk, ChatCompletionChunk) assert isinstance(chunk, ChatCompletionChunk)
# 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 ( if (
chunk.choices chunk.choices
and len(chunk.choices) > 0 and len(chunk.choices) > 0
@ -159,14 +138,6 @@ async def stream_claude_response(
# Return final message # Return final message
response = await stream.get_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 # Close the Anthropic client
await client.close() await client.close()

View File

@ -4,27 +4,10 @@ from dotenv import load_dotenv
load_dotenv() load_dotenv()
import os
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from routes import screenshot, generate_code, home, evals 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) app = FastAPI(openapi_url=None, docs_url=None, redoc_url=None)
# Configure CORS settings # Configure CORS settings

1580
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 image_generation.core import create_alt_url_mapping
from prompts.imported_code_prompts import IMPORTED_CODE_SYSTEM_PROMPTS from prompts.imported_code_prompts import IMPORTED_CODE_SYSTEM_PROMPTS
from prompts.screenshot_system_prompts import SYSTEM_PROMPTS from prompts.screenshot_system_prompts import SYSTEM_PROMPTS
from prompts.text_prompts import SYSTEM_PROMPTS as TEXT_SYSTEM_PROMPTS
from prompts.types import Stack from prompts.types import Stack
from video.utils import assemble_claude_prompt_video from video.utils import assemble_claude_prompt_video
@ -43,17 +42,12 @@ async def create_prompt(
prompt_messages.append(message) prompt_messages.append(message)
else: else:
# Assemble the prompt for non-imported code # Assemble the prompt for non-imported code
if input_mode == "image":
if params.get("resultImage"): if params.get("resultImage"):
prompt_messages = assemble_prompt( prompt_messages = assemble_prompt(
params["image"], stack, params["resultImage"] params["image"], stack, params["resultImage"]
) )
else: else:
prompt_messages = assemble_prompt(params["image"], stack) prompt_messages = assemble_prompt(params["image"], stack)
elif input_mode == "text":
prompt_messages = assemble_text_prompt(params["image"], stack)
else:
raise Exception("Invalid input mode")
if params["generationType"] == "update": if params["generationType"] == "update":
# Transform the history tree into message format # Transform the history tree into message format
@ -138,22 +132,3 @@ def assemble_prompt(
"content": user_content, "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" pre-commit = "^3.6.2"
anthropic = "^0.18.0" anthropic = "^0.18.0"
moviepy = "^1.0.3" moviepy = "^1.0.3"
sentry-sdk = {extras = ["fastapi"], version = "^1.38.0"}
pillow = "^10.3.0" pillow = "^10.3.0"
types-pillow = "^10.2.0.20240520" types-pillow = "^10.2.0.20240520"
aiohttp = "^3.9.5" aiohttp = "^3.9.5"

View File

@ -1,15 +1,15 @@
import asyncio import asyncio
from dataclasses import dataclass from dataclasses import dataclass
import traceback
from fastapi import APIRouter, WebSocket from fastapi import APIRouter, WebSocket
import openai import openai
import sentry_sdk
from codegen.utils import extract_html_content from codegen.utils import extract_html_content
from config import ( from config import (
ANTHROPIC_API_KEY,
IS_PROD, IS_PROD,
NUM_VARIANTS, NUM_VARIANTS,
OPENAI_API_KEY,
OPENAI_BASE_URL, OPENAI_BASE_URL,
PLATFORM_ANTHROPIC_API_KEY,
PLATFORM_OPENAI_API_KEY,
REPLICATE_API_KEY, REPLICATE_API_KEY,
SHOULD_MOCK_AI_RESPONSE, SHOULD_MOCK_AI_RESPONSE,
) )
@ -21,12 +21,9 @@ from llm import (
stream_claude_response_native, stream_claude_response_native,
stream_openai_response, stream_openai_response,
) )
from fs_logging.core import write_logs
from mock_llm import mock_completion from mock_llm import mock_completion
from typing import Dict, cast, get_args from typing import Any, Callable, Coroutine, Dict, List, Literal, 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, Literal, cast, get_args
from image_generation.core import generate_images from image_generation.core import generate_images
from prompts import create_prompt from prompts import create_prompt
from prompts.claude_prompts import VIDEO_PROMPT from prompts.claude_prompts import VIDEO_PROMPT
@ -67,7 +64,7 @@ async def perform_image_generation(
return completion return completion
if replicate_api_key: if replicate_api_key:
image_generation_model = "sdxl-lightning" image_generation_model = "flux"
api_key = replicate_api_key api_key = replicate_api_key
else: else:
if not openai_api_key: if not openai_api_key:
@ -91,7 +88,6 @@ async def perform_image_generation(
@dataclass @dataclass
class ExtractedParams: class ExtractedParams:
user_id: str
stack: Stack stack: Stack
input_mode: InputMode input_mode: InputMode
code_generation_model: Llm code_generation_model: Llm
@ -99,7 +95,6 @@ class ExtractedParams:
openai_api_key: str | None openai_api_key: str | None
anthropic_api_key: str | None anthropic_api_key: str | None
openai_base_url: str | None openai_base_url: str | None
payment_method: PaymentMethod
async def extract_params( async def extract_params(
@ -129,61 +124,14 @@ async def extract_params(
await throw_error(f"Invalid model: {code_generation_model_str}") await throw_error(f"Invalid model: {code_generation_model_str}")
raise ValueError(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) openai_api_key = get_from_settings_dialog_or_env(
auth_token = params.get("authToken") params, "openAiApiKey", OPENAI_API_KEY
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 = 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."
)
else:
await throw_error("Unknown error occurred. Contact support.")
raise Exception("Unknown error occurred when checking subscription credits")
user_id = res.user_id
print("Payment method: ", payment_method)
if payment_method is PaymentMethod.UNKNOWN:
openai_api_key = get_from_settings_dialog_or_env(params, "openAiApiKey", None)
if not openai_api_key:
await throw_error(
"Please subscribe to a paid plan to generate code. If you are a subscriber and seeing this error, please contact support."
)
else:
sentry_sdk.capture_exception(Exception("OpenAI key is no longer supported"))
await throw_error(
"Using your own OpenAI key is no longer supported due to the costs of running this website. Please subscribe to a paid plan to generate code. If you are a subscriber and seeing this error, please contact support."
) )
if res.status != "not_subscriber": # If neither is provided, we throw an error later only if Claude is used.
raise Exception("No payment method found") anthropic_api_key = get_from_settings_dialog_or_env(
params, "anthropicApiKey", ANTHROPIC_API_KEY
)
# Base URL for OpenAI API # Base URL for OpenAI API
openai_base_url: str | None = None openai_base_url: str | None = None
@ -196,12 +144,9 @@ async def extract_params(
print("Using official OpenAI URL") print("Using official OpenAI URL")
# Get the image generation flag from the request. Fall back to True if not provided. # Get the image generation flag from the request. Fall back to True if not provided.
should_generate_images = ( should_generate_images = bool(params.get("isImageGenerationEnabled", True))
bool(params.get("isImageGenerationEnabled", True)) if not IS_PROD else True
)
return ExtractedParams( return ExtractedParams(
user_id=user_id,
stack=validated_stack, stack=validated_stack,
input_mode=validated_input_mode, input_mode=validated_input_mode,
code_generation_model=code_generation_model, code_generation_model=code_generation_model,
@ -209,7 +154,6 @@ async def extract_params(
openai_api_key=openai_api_key, openai_api_key=openai_api_key,
anthropic_api_key=anthropic_api_key, anthropic_api_key=anthropic_api_key,
openai_base_url=openai_base_url, openai_base_url=openai_base_url,
payment_method=payment_method,
) )
@ -263,7 +207,6 @@ async def stream_code(websocket: WebSocket):
print("Received params") print("Received params")
extracted_params = await extract_params(params, throw_error) extracted_params = await extract_params(params, throw_error)
user_id = extracted_params.user_id
stack = extracted_params.stack stack = extracted_params.stack
input_mode = extracted_params.input_mode input_mode = extracted_params.input_mode
code_generation_model = extracted_params.code_generation_model code_generation_model = extracted_params.code_generation_model
@ -271,11 +214,6 @@ async def stream_code(websocket: WebSocket):
openai_base_url = extracted_params.openai_base_url openai_base_url = extracted_params.openai_base_url
anthropic_api_key = extracted_params.anthropic_api_key anthropic_api_key = extracted_params.anthropic_api_key
should_generate_images = extracted_params.should_generate_images should_generate_images = extracted_params.should_generate_images
payment_method = extracted_params.payment_method
# If the payment method is unknown, we shouldn't proceed
if payment_method is PaymentMethod.UNKNOWN:
return
# Auto-upgrade usage of older models # Auto-upgrade usage of older models
code_generation_model = auto_upgrade_model(code_generation_model) code_generation_model = auto_upgrade_model(code_generation_model)
@ -309,13 +247,9 @@ async def stream_code(websocket: WebSocket):
if SHOULD_MOCK_AI_RESPONSE: if SHOULD_MOCK_AI_RESPONSE:
completions = [await mock_completion(process_chunk, input_mode=input_mode)] completions = [await mock_completion(process_chunk, input_mode=input_mode)]
variant_models = [Llm.GPT_4O_2024_05_13]
else: else:
try: try:
if input_mode == "video": if input_mode == "video":
if IS_PROD:
raise Exception("Video mode is not supported in prod")
if not anthropic_api_key: if not anthropic_api_key:
await throw_error( await throw_error(
"Video only works with Anthropic models. No Anthropic API key found. Please add the environment variable ANTHROPIC_API_KEY to backend/.env or in the settings dialog" "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"
@ -332,35 +266,26 @@ async def stream_code(websocket: WebSocket):
include_thinking=True, include_thinking=True,
) )
] ]
variant_models = [Llm.CLAUDE_3_OPUS]
else: else:
# Depending on the presence and absence of various keys, # Depending on the presence and absence of various keys,
# we decide which models to run # we decide which models to run
variant_models = [] variant_models = []
if openai_api_key and anthropic_api_key: if openai_api_key and anthropic_api_key:
variant_models = [ variant_models = ["anthropic", "openai"]
Llm.CLAUDE_3_5_SONNET_2024_06_20,
Llm.GPT_4O_2024_05_13,
]
elif openai_api_key: elif openai_api_key:
variant_models = [ variant_models = ["openai", "openai"]
Llm.GPT_4O_2024_05_13,
Llm.GPT_4O_2024_05_13,
]
elif anthropic_api_key: elif anthropic_api_key:
variant_models = [ variant_models = ["anthropic", "anthropic"]
Llm.CLAUDE_3_5_SONNET_2024_06_20,
Llm.CLAUDE_3_5_SONNET_2024_06_20,
]
else: else:
await throw_error( await throw_error(
"No OpenAI or Anthropic API key found. Please add the environment variable OPENAI_API_KEY or ANTHROPIC_API_KEY to backend/.env or in the settings dialog. If you add it to .env, make sure to restart the backend server." "No OpenAI or Anthropic API key found. Please add the environment variable OPENAI_API_KEY or ANTHROPIC_API_KEY to backend/.env or in the settings dialog. If you add it to .env, make sure to restart the backend server."
) )
raise Exception("No OpenAI or Anthropic key") 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): for index, model in enumerate(variant_models):
if model == Llm.GPT_4O_2024_05_13: if model == "openai":
if openai_api_key is None: if openai_api_key is None:
await throw_error("OpenAI API key is missing.") await throw_error("OpenAI API key is missing.")
raise Exception("OpenAI API key is missing.") raise Exception("OpenAI API key is missing.")
@ -374,7 +299,7 @@ async def stream_code(websocket: WebSocket):
model=Llm.GPT_4O_2024_05_13, model=Llm.GPT_4O_2024_05_13,
) )
) )
elif model == Llm.CLAUDE_3_5_SONNET_2024_06_20: elif model == "anthropic":
if anthropic_api_key is None: if anthropic_api_key is None:
await throw_error("Anthropic API key is missing.") await throw_error("Anthropic API key is missing.")
raise Exception("Anthropic API key is missing.") raise Exception("Anthropic API key is missing.")
@ -397,6 +322,12 @@ async def stream_code(websocket: WebSocket):
) )
if all_generations_failed: if all_generations_failed:
await throw_error("Error generating code. Please contact support.") 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") raise Exception("All generations failed")
# If some completions failed, replace them with empty strings # If some completions failed, replace them with empty strings
@ -404,12 +335,6 @@ async def stream_code(websocket: WebSocket):
if isinstance(completion, Exception): if isinstance(completion, Exception):
completions[index] = "" completions[index] = ""
print("Generation failed for variant", index) print("Generation failed for variant", index)
try:
raise Exception(
"One of the generations failed"
) from completion
except:
sentry_sdk.capture_exception()
print("Models used for generation: ", variant_models) print("Models used for generation: ", variant_models)
@ -454,27 +379,10 @@ async def stream_code(websocket: WebSocket):
completions = [extract_html_content(completion) for completion in completions] completions = [extract_html_content(completion) for completion in completions]
# Write the messages dict into a log so that we can debug later # Write the messages dict into a log so that we can debug later
# write_logs(prompt_messages, completion) # type: ignore write_logs(prompt_messages, completions[0])
if IS_PROD:
# Catch any errors from sending to SaaS backend and continue
try:
await send_to_saas_backend(
user_id,
prompt_messages,
completions,
payment_method=payment_method,
llm_versions=variant_models,
stack=stack,
is_imported_from_code=bool(params.get("isImportedFromCode", False)),
includes_result_image=bool(params.get("resultImage", False)),
input_mode=input_mode,
)
except Exception as e:
print("Error sending to SaaS backend", e)
sentry_sdk.capture_exception(e)
## Image Generation ## Image Generation
for index, _ in enumerate(completions): for index, _ in enumerate(completions):
await send_message("status", "Generating images...", index) await send_message("status", "Generating images...", index)

View File

@ -1,58 +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_API_SECRET, 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(
user_id: str,
prompt_messages: List[ChatCompletionMessageParam],
completions: list[str],
llm_versions: list[Llm],
payment_method: PaymentMethod,
stack: Stack,
is_imported_from_code: bool,
includes_result_image: bool,
input_mode: InputMode,
):
if IS_PROD:
async with httpx.AsyncClient() as client:
url = BACKEND_SAAS_URL + "/generations/store"
data = json.dumps(
{
"user_id": user_id,
"prompt": json.dumps(prompt_messages),
"completions": completions,
"payment_method": payment_method.value,
"llm_versions": [llm_version.value for llm_version in llm_versions],
"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 {BACKEND_SAAS_API_SECRET}", # Add the auth token to the headers
}
response = await client.post(url, content=data, headers=headers, timeout=10)
response.raise_for_status()
response_data = response.json()
return response_data

View File

@ -1,25 +0,0 @@
import httpx
from pydantic import BaseModel
from config import BACKEND_SAAS_URL
class SubscriptionCreditsResponse(BaseModel):
user_id: str
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 fastapi import APIRouter
from pydantic import BaseModel from pydantic import BaseModel
import httpx import httpx
from config import PLATFORM_SCREENSHOTONE_API_KEY
from routes.saas_utils import does_user_have_subscription_credits
router = APIRouter() router = APIRouter()
@ -15,31 +12,10 @@ def bytes_to_data_url(image_bytes: bytes, mime_type: str) -> str:
async def capture_screenshot( 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: ) -> bytes:
api_base_url = "https://api.screenshotone.com/take" 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 = { params = {
"access_key": api_key, "access_key": api_key,
"url": target_url, "url": target_url,
@ -68,8 +44,7 @@ async def capture_screenshot(
class ScreenshotRequest(BaseModel): class ScreenshotRequest(BaseModel):
url: str url: str
apiKey: str | None apiKey: str
authToken: str
class ScreenshotResponse(BaseModel): class ScreenshotResponse(BaseModel):
@ -81,10 +56,9 @@ async def app_screenshot(request: ScreenshotRequest):
# Extract the URL from the request body # Extract the URL from the request body
url = request.url url = request.url
api_key = request.apiKey api_key = request.apiKey
auth_token = request.authToken
# TODO: Add error handling # 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 # Convert the image bytes to a data url
data_url = bytes_to_data_url(image_bytes, "image/png") 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( async def generate_and_save_images(
prompts: List[str], prompts: List[str],
model: Literal["dalle3", "sdxl-lightning"], model: Literal["dalle3", "flux"],
api_key: Optional[str], api_key: Optional[str],
) -> None: ) -> None:
# Ensure the output directory exists # Ensure the output directory exists
@ -64,7 +64,7 @@ async def generate_and_save_images(
image_data: bytes = await response.read() image_data: bytes = await response.read()
# Save the image with a filename based on the input eval # 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 = ( filename: str = (
f"{prefix}{prompts[i][:50].replace(' ', '_').replace(':', '')}.png" f"{prefix}{prompts[i][:50].replace(' ', '_').replace(':', '')}.png"
) )
@ -78,7 +78,7 @@ async def generate_and_save_images(
async def main() -> None: async def main() -> None:
# await generate_and_save_images(EVALS, "dalle3", OPENAI_API_KEY) # 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__": if __name__ == "__main__":

View File

@ -16,21 +16,6 @@
<!-- Injected code for hosted version --> <!-- Injected code for hosted version -->
<%- injectHead %> <%- 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> <title>Screenshot to Code</title>
<!-- Open Graph Meta Tags --> <!-- Open Graph Meta Tags -->

View File

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

Binary file not shown.

View File

@ -1,46 +1,31 @@
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef } from "react";
import { generateCode } from "./generateCode"; import { generateCode } from "./generateCode";
import { IS_FREE_TRIAL_ENABLED, IS_RUNNING_ON_CLOUD } from "./config";
import SettingsDialog from "./components/settings/SettingsDialog"; import SettingsDialog from "./components/settings/SettingsDialog";
import { AppState, CodeGenerationParams, EditorTheme, Settings } from "./types"; import { AppState, CodeGenerationParams, EditorTheme, Settings } from "./types";
import { IS_RUNNING_ON_CLOUD } from "./config";
import { PicoBadge } from "./components/messages/PicoBadge"; import { PicoBadge } from "./components/messages/PicoBadge";
import { OnboardingNote } from "./components/messages/OnboardingNote"; import { OnboardingNote } from "./components/messages/OnboardingNote";
import { usePersistedState } from "./hooks/usePersistedState"; import { usePersistedState } from "./hooks/usePersistedState";
import TermsOfServiceDialog from "./components/TermsOfServiceDialog"; import TermsOfServiceDialog from "./components/TermsOfServiceDialog";
import { USER_CLOSE_WEB_SOCKET_CODE } from "./constants"; import { USER_CLOSE_WEB_SOCKET_CODE } from "./constants";
import { addEvent } from "./lib/analytics";
import { extractHistory } from "./components/history/utils"; import { extractHistory } from "./components/history/utils";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import { useAuth } from "@clerk/clerk-react";
import { useStore } from "./store/store";
import { Stack } from "./lib/stacks"; import { Stack } from "./lib/stacks";
import { CodeGenerationModel } from "./lib/models"; import { CodeGenerationModel } from "./lib/models";
import useBrowserTabIndicator from "./hooks/useBrowserTabIndicator"; import useBrowserTabIndicator from "./hooks/useBrowserTabIndicator";
import TipLink from "./components/messages/TipLink"; import TipLink from "./components/messages/TipLink";
import { useAppStore } from "./store/app-store"; import { useAppStore } from "./store/app-store";
import GenerateFromText from "./components/generate-from-text/GenerateFromText";
import { useProjectStore } from "./store/project-store"; import { useProjectStore } from "./store/project-store";
import Sidebar from "./components/sidebar/Sidebar";
import PreviewPane from "./components/preview/PreviewPane"; import PreviewPane from "./components/preview/PreviewPane";
import DeprecationMessage from "./components/messages/DeprecationMessage"; import DeprecationMessage from "./components/messages/DeprecationMessage";
import { GenerationSettings } from "./components/settings/GenerationSettings"; import { GenerationSettings } from "./components/settings/GenerationSettings";
import StartPane from "./components/start-pane/StartPane"; import StartPane from "./components/start-pane/StartPane";
import { takeScreenshot } from "./lib/takeScreenshot"; import { takeScreenshot } from "./lib/takeScreenshot";
import Sidebar from "./components/sidebar/Sidebar";
import { Commit } from "./components/commits/types"; import { Commit } from "./components/commits/types";
import { createCommit } from "./components/commits/utils"; import { createCommit } from "./components/commits/utils";
interface Props { function App() {
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);
const { const {
// Inputs // Inputs
inputMode, inputMode,
@ -155,21 +140,12 @@ function App({ navbarComponent }: Props) {
return; return;
} }
addEvent("Regenerate");
// Re-run the create // Re-run the create
if (inputMode === "image" || inputMode === "video") {
doCreate(referenceImages, inputMode); doCreate(referenceImages, inputMode);
} else {
// TODO: Fix this
doCreateFromText(initialPrompt);
}
}; };
// Used when the user cancels the code generation // Used when the user cancels the code generation
const cancelCodeGeneration = () => { const cancelCodeGeneration = () => {
addEvent("Cancel");
wsRef.current?.close?.(USER_CLOSE_WEB_SOCKET_CODE); 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 // Reset the execution console
resetExecutionConsoles(); resetExecutionConsoles();
@ -202,12 +178,7 @@ function App({ navbarComponent }: Props) {
setAppState(AppState.CODING); setAppState(AppState.CODING);
// Merge settings with params // Merge settings with params
const authToken = await getToken(); const updatedParams = { ...params, ...settings };
const updatedParams = {
...params,
...settings,
authToken: authToken || undefined,
};
const baseCommitObject = { const baseCommitObject = {
variants: [{ code: "" }, { code: "" }], variants: [{ code: "" }, { code: "" }],
@ -256,17 +227,13 @@ function App({ navbarComponent }: Props) {
}, },
// On complete // On complete
() => { () => {
addEvent("CreateSuccessful");
setAppState(AppState.CODE_READY); setAppState(AppState.CODE_READY);
} }
); );
} }
// Initial version creation // Initial version creation
async function doCreate( function doCreate(referenceImages: string[], inputMode: "image" | "video") {
referenceImages: string[],
inputMode: "image" | "video"
) {
// Reset any existing state // Reset any existing state
reset(); reset();
@ -276,7 +243,6 @@ function App({ navbarComponent }: Props) {
// Kick off the code generation // Kick off the code generation
if (referenceImages.length > 0) { if (referenceImages.length > 0) {
addEvent("Create");
doGenerateCode({ doGenerateCode({
generationType: "create", generationType: "create",
image: referenceImages[0], image: referenceImages[0],
@ -285,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 // Subsequent updates
async function doUpdate( async function doUpdate(
updateInstruction: string, updateInstruction: string,
@ -319,7 +272,6 @@ function App({ navbarComponent }: Props) {
try { try {
historyTree = extractHistory(head, commits); historyTree = extractHistory(head, commits);
} catch { } catch {
addEvent("HistoryTreeFailed");
toast.error( toast.error(
"Version history is invalid. This shouldn't happen. Please contact support or open a Github issue." "Version history is invalid. This shouldn't happen. Please contact support or open a Github issue."
); );
@ -341,11 +293,10 @@ function App({ navbarComponent }: Props) {
? await takeScreenshot() ? await takeScreenshot()
: undefined; : undefined;
addEvent("Edit");
doGenerateCode({ doGenerateCode({
generationType: "update", generationType: "update",
inputMode, inputMode,
image: inputMode === "text" ? initialPrompt : referenceImages[0], image: referenceImages[0],
resultImage, resultImage,
history: updatedHistory, history: updatedHistory,
isImportedFromCode, isImportedFromCode,
@ -394,7 +345,7 @@ function App({ navbarComponent }: Props) {
{IS_RUNNING_ON_CLOUD && <PicoBadge />} {IS_RUNNING_ON_CLOUD && <PicoBadge />}
{IS_RUNNING_ON_CLOUD && ( {IS_RUNNING_ON_CLOUD && (
<TermsOfServiceDialog <TermsOfServiceDialog
open={false} open={!settings.isTermOfServiceAccepted}
onOpenChange={handleTermDialogOpenChange} onOpenChange={handleTermDialogOpenChange}
/> />
)} )}
@ -415,14 +366,7 @@ function App({ navbarComponent }: Props) {
{/* Show tip link until coding is complete */} {/* Show tip link until coding is complete */}
{appState !== AppState.CODE_READY && <TipLink />} {appState !== AppState.CODE_READY && <TipLink />}
{IS_RUNNING_ON_CLOUD && {IS_RUNNING_ON_CLOUD && !settings.openAiApiKey && <OnboardingNote />}
!settings.openAiApiKey &&
!IS_FREE_TRIAL_ENABLED &&
subscriberTier === "free" && <OnboardingNote />}
{appState === AppState.INITIAL && (
<GenerateFromText doCreateFromText={doCreateFromText} />
)}
{/* Rest of the sidebar when we're not in the initial state */} {/* Rest of the sidebar when we're not in the initial state */}
{(appState === AppState.CODING || {(appState === AppState.CODING ||
@ -438,8 +382,6 @@ function App({ navbarComponent }: Props) {
</div> </div>
<main className="py-2 lg:pl-96"> <main className="py-2 lg:pl-96">
{!!navbarComponent && navbarComponent}
{appState === AppState.INITIAL && ( {appState === AppState.INITIAL && (
<StartPane <StartPane
doCreate={doCreate} doCreate={doCreate}

View File

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

View File

@ -7,7 +7,9 @@ import {
AlertDialogHeader, AlertDialogHeader,
AlertDialogTitle, AlertDialogTitle,
} from "./ui/alert-dialog"; } 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"]; const LOGOS = ["microsoft", "amazon", "mit", "stanford", "bytedance", "baidu"];
@ -15,19 +17,40 @@ const TermsOfServiceDialog: React.FC<{
open: boolean; open: boolean;
onOpenChange: (open: boolean) => void; onOpenChange: (open: boolean) => void;
}> = ({ open, onOpenChange }) => { }> = ({ 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 ( return (
<AlertDialog open={open} onOpenChange={onOpenChange}> <AlertDialog open={open} onOpenChange={onOpenChange}>
<AlertDialogContent> <AlertDialogContent>
<AlertDialogHeader> <AlertDialogHeader>
<AlertDialogTitle className="mb-2 text-xl"> <AlertDialogTitle className="mb-2 text-xl">
One last step Enter your email to get started
</AlertDialogTitle> </AlertDialogTitle>
</AlertDialogHeader> </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"> <div className="flex flex-col space-y-3 text-sm">
<p> <p>
You consent to receiving occasional product updates via email, and By providing your email, you consent to receiving occasional product
you accept the{" "} updates, and you accept the{" "}
<a <a
href="https://a.picoapps.xyz/camera-write" href="https://a.picoapps.xyz/camera-write"
target="_blank" target="_blank"
@ -53,8 +76,13 @@ const TermsOfServiceDialog: React.FC<{
<AlertDialogFooter> <AlertDialogFooter>
<AlertDialogAction <AlertDialogAction
onClick={() => { onClick={(e) => {
addEvent("EmailSubmit"); if (!email.trim() || !email.trim().includes("@")) {
e.preventDefault();
toast.error("Please enter your email");
} else {
onSubscribe();
}
}} }}
> >
Agree & Continue Agree & Continue

View File

@ -3,8 +3,6 @@ import { HTTP_BACKEND_URL } from "../config";
import { Button } from "./ui/button"; import { Button } from "./ui/button";
import { Input } from "./ui/input"; import { Input } from "./ui/input";
import { toast } from "react-hot-toast"; import { toast } from "react-hot-toast";
import { useStore } from "../store/store";
import { useAuth } from "@clerk/clerk-react";
interface Props { interface Props {
screenshotOneApiKey: string | null; screenshotOneApiKey: string | null;
@ -15,31 +13,28 @@ export function UrlInputSection({ doCreate, screenshotOneApiKey }: Props) {
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [referenceUrl, setReferenceUrl] = useState(""); const [referenceUrl, setReferenceUrl] = useState("");
// Hosted version only
const subscriberTier = useStore((state) => state.subscriberTier);
const { getToken } = useAuth();
async function takeScreenshot() { async function takeScreenshot() {
if (!referenceUrl) { if (!screenshotOneApiKey) {
return toast.error("Please enter a URL"); 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") { if (!referenceUrl) {
return toast.error( toast.error("Please enter a URL");
"Please upgrade to a paid plan to use the screenshot feature." return;
);
} }
if (referenceUrl) { if (referenceUrl) {
try { try {
setIsLoading(true); setIsLoading(true);
const authToken = await getToken();
const response = await fetch(`${HTTP_BACKEND_URL}/api/screenshot`, { const response = await fetch(`${HTTP_BACKEND_URL}/api/screenshot`, {
method: "POST", method: "POST",
body: JSON.stringify({ body: JSON.stringify({
url: referenceUrl, url: referenceUrl,
apiKey: screenshotOneApiKey, apiKey: screenshotOneApiKey,
authToken,
}), }),
headers: { headers: {
"Content-Type": "application/json", "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 <div
className="flex justify-between truncate flex-1 p-2 plausible-event-name=HistoryClick" className="flex justify-between truncate flex-1 p-2"
onClick={() => onClick={() =>
shouldDisableReverts shouldDisableReverts
? toast.error( ? 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() { export function OnboardingNote() {
const setPricingDialogOpen = useStore((state) => state.setPricingDialogOpen);
return ( return (
<div className="flex flex-col space-y-4 bg-green-700 p-2 rounded text-stone-200 text-sm"> <div className="flex flex-col space-y-4 bg-green-700 p-2 rounded text-stone-200 text-sm">
<span> <span>
To use Screenshot to Code,{" "}
<a <a
className="inline underline hover:opacity-70 cursor-pointer" className="inline underline hover:opacity-70"
onClick={() => setPricingDialogOpen(true)} href="https://buy.stripe.com/8wM6sre70gBW1nqaEE"
target="_blank" target="_blank"
> >
Subscribe to get started buy some credits (100 generations for $36)
</a> </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> </span>
</div> </div>
); );

View File

@ -1,12 +1,7 @@
import PricingDialog from "../hosted/payments/PricingDialog";
export function PicoBadge() { export function PicoBadge() {
return ( return (
<> <>
<div> <a
<PricingDialog />
</div>
{/* <a
href="https://screenshot-to-code.canny.io/feature-requests" href="https://screenshot-to-code.canny.io/feature-requests"
target="_blank" target="_blank"
> >
@ -14,9 +9,17 @@ export function PicoBadge() {
className="fixed z-50 bottom-16 right-5 rounded-md shadow bg-black className="fixed z-50 bottom-16 right-5 rounded-md shadow bg-black
text-white px-4 text-xs py-3 cursor-pointer" text-white px-4 text-xs py-3 cursor-pointer"
> >
feedback feature requests?
</div> </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"> <div className="flex justify-start items-center px-4 mb-2">
<span <span
title="Copy Code" 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} onClick={copyCode}
> >
Copy Code <FaCopy className="ml-2" /> Copy Code <FaCopy className="ml-2" />
</span> </span>
<Button <Button
onClick={doOpenInCodepenio} 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{" "} Open in{" "}
<img <img

View File

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

View File

@ -46,7 +46,6 @@ function SettingsDialog({ settings, setSettings }: Props) {
<DialogTitle className="mb-4">Settings</DialogTitle> <DialogTitle className="mb-4">Settings</DialogTitle>
</DialogHeader> </DialogHeader>
{!IS_RUNNING_ON_CLOUD && (
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<Label htmlFor="image-generation"> <Label htmlFor="image-generation">
<div>DALL-E Placeholder Image Generation</div> <div>DALL-E Placeholder Image Generation</div>
@ -65,16 +64,13 @@ function SettingsDialog({ settings, setSettings }: Props) {
} }
/> />
</div> </div>
)}
<div className="flex flex-col space-y-6"> <div className="flex flex-col space-y-6">
{!IS_RUNNING_ON_CLOUD && (
<div> <div>
<Label htmlFor="openai-api-key"> <Label htmlFor="openai-api-key">
<div>OpenAI API key</div> <div>OpenAI API key</div>
<div className="font-light mt-1 mb-2 text-xs leading-relaxed"> <div className="font-light mt-1 mb-2 text-xs leading-relaxed">
Only stored in your browser. Never stored on servers. Only stored in your browser. Never stored on servers. Overrides
Overrides your .env config. your .env config.
</div> </div>
</Label> </Label>
@ -90,7 +86,6 @@ function SettingsDialog({ settings, setSettings }: Props) {
} }
/> />
</div> </div>
)}
{!IS_RUNNING_ON_CLOUD && ( {!IS_RUNNING_ON_CLOUD && (
<div> <div>
@ -115,13 +110,12 @@ function SettingsDialog({ settings, setSettings }: Props) {
</div> </div>
)} )}
{!IS_RUNNING_ON_CLOUD && (
<div> <div>
<Label htmlFor="anthropic-api-key"> <Label htmlFor="anthropic-api-key">
<div>Anthropic API key</div> <div>Anthropic API key</div>
<div className="font-light mt-1 text-xs leading-relaxed"> <div className="font-light mt-1 text-xs leading-relaxed">
Only stored in your browser. Never stored on servers. Only stored in your browser. Never stored on servers. Overrides
Overrides your .env config. your .env config.
</div> </div>
</Label> </Label>
@ -137,9 +131,7 @@ function SettingsDialog({ settings, setSettings }: Props) {
} }
/> />
</div> </div>
)}
{!IS_RUNNING_ON_CLOUD && (
<Accordion type="single" collapsible className="w-full"> <Accordion type="single" collapsible className="w-full">
<AccordionItem value="item-1"> <AccordionItem value="item-1">
<AccordionTrigger>Screenshot by URL Config</AccordionTrigger> <AccordionTrigger>Screenshot by URL Config</AccordionTrigger>
@ -173,7 +165,6 @@ function SettingsDialog({ settings, setSettings }: Props) {
</AccordionContent> </AccordionContent>
</AccordionItem> </AccordionItem>
</Accordion> </Accordion>
)}
<Accordion type="single" collapsible className="w-full"> <Accordion type="single" collapsible className="w-full">
<AccordionItem value="item-1"> <AccordionItem value="item-1">

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 React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"; import * as DialogPrimitive from "@radix-ui/react-dialog"
// import { Cross2Icon } from "@radix-ui/react-icons"; 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< const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>, React.ElementRef<typeof DialogPrimitive.Overlay>,
@ -24,8 +24,8 @@ const DialogOverlay = React.forwardRef<
)} )}
{...props} {...props}
/> />
)); ))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
const DialogContent = React.forwardRef< const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>, React.ElementRef<typeof DialogPrimitive.Content>,
@ -42,14 +42,14 @@ const DialogContent = React.forwardRef<
{...props} {...props}
> >
{children} {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" /> <Cross2Icon className="h-4 w-4" />
<span className="sr-only">Close</span> <span className="sr-only">Close</span>
</DialogPrimitive.Close> */} </DialogPrimitive.Close>
</DialogPrimitive.Content> </DialogPrimitive.Content>
</DialogPortal> </DialogPortal>
)); ))
DialogContent.displayName = DialogPrimitive.Content.displayName; DialogContent.displayName = DialogPrimitive.Content.displayName
const DialogHeader = ({ const DialogHeader = ({
className, className,
@ -62,8 +62,8 @@ const DialogHeader = ({
)} )}
{...props} {...props}
/> />
); )
DialogHeader.displayName = "DialogHeader"; DialogHeader.displayName = "DialogHeader"
const DialogFooter = ({ const DialogFooter = ({
className, className,
@ -76,8 +76,8 @@ const DialogFooter = ({
)} )}
{...props} {...props}
/> />
); )
DialogFooter.displayName = "DialogFooter"; DialogFooter.displayName = "DialogFooter"
const DialogTitle = React.forwardRef< const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>, React.ElementRef<typeof DialogPrimitive.Title>,
@ -91,8 +91,8 @@ const DialogTitle = React.forwardRef<
)} )}
{...props} {...props}
/> />
)); ))
DialogTitle.displayName = DialogPrimitive.Title.displayName; DialogTitle.displayName = DialogPrimitive.Title.displayName
const DialogDescription = React.forwardRef< const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>, React.ElementRef<typeof DialogPrimitive.Description>,
@ -103,8 +103,8 @@ const DialogDescription = React.forwardRef<
className={cn("text-sm text-muted-foreground", className)} className={cn("text-sm text-muted-foreground", className)}
{...props} {...props}
/> />
)); ))
DialogDescription.displayName = DialogPrimitive.Description.displayName; DialogDescription.displayName = DialogPrimitive.Description.displayName
export { export {
Dialog, Dialog,
@ -117,4 +117,4 @@ export {
DialogFooter, DialogFooter,
DialogTitle, DialogTitle,
DialogDescription, 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

@ -24,8 +24,7 @@ function Variants() {
{variants.map((_, index) => ( {variants.map((_, index) => (
<div <div
key={index} key={index}
className={`p-2 border rounded-md cursor-pointer className={`p-2 border rounded-md cursor-pointer ${
plausible-event-name=VariantClick ${
index === selectedVariantIndex index === selectedVariantIndex
? "bg-blue-100 dark:bg-blue-900" ? "bg-blue-100 dark:bg-blue-900"
: "bg-gray-50 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-700" : "bg-gray-50 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-700"

View File

@ -8,23 +8,5 @@ export const WS_BACKEND_URL =
export const HTTP_BACKEND_URL = export const HTTP_BACKEND_URL =
import.meta.env.VITE_HTTP_BACKEND_URL || "http://127.0.0.1:7001"; import.meta.env.VITE_HTTP_BACKEND_URL || "http://127.0.0.1:7001";
// Hosted version only
export const PICO_BACKEND_FORM_SECRET = export const PICO_BACKEND_FORM_SECRET =
import.meta.env.VITE_PICO_BACKEND_FORM_SECRET || null; 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 // Will generate a static error if a model in the enum above is not in the descriptions
export const CODE_GENERATION_MODEL_DESCRIPTIONS: { export const CODE_GENERATION_MODEL_DESCRIPTIONS: {
[key in CodeGenerationModel]: { [key in CodeGenerationModel]: { name: string; inBeta: boolean };
name: string;
inBeta: boolean;
};
} = { } = {
"gpt-4o-2024-05-13": { name: "GPT-4o", inBeta: false }, "gpt-4o-2024-05-13": { name: "GPT-4o", inBeta: false },
"claude-3-5-sonnet-20240620": { "claude-3-5-sonnet-20240620": { name: "Claude 3.5 Sonnet", inBeta: false },
name: "Claude 3.5 Sonnet", "gpt-4-turbo-2024-04-09": { name: "GPT-4 Turbo (deprecated)", inBeta: false },
inBeta: false, gpt_4_vision: { name: "GPT-4 Vision (deprecated)", inBeta: false },
}, claude_3_sonnet: { name: "Claude 3 (deprecated)", 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 React from "react";
import ReactDOM from "react-dom/client"; import ReactDOM from "react-dom/client";
import App from "./App.tsx";
import "./index.css";
import { Toaster } from "react-hot-toast"; 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 EvalsPage from "./components/evals/EvalsPage.tsx";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom"; 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( ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode> <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> <Router>
<Routes> <Routes>
<Route path="/" element={<AppContainer />} /> <Route path="/" element={<App />} />
<Route path="/evals" element={<EvalsPage />} /> <Route path="/evals" element={<EvalsPage />} />
<Route path="/pricing" element={<PricingPage />} />
<Route path="/faqs" element={<FaqsPage />} />
<Route path="/checkout-success" element={<CheckoutSuccessPage />} />
</Routes> </Routes>
</Router> </Router>
<Toaster <Toaster toastOptions={{ className: "dark:bg-zinc-950 dark:text-white" }} />
toastOptions={{ className: "dark:bg-zinc-950 dark:text-white" }}
/>
</ClerkProvider>
</React.StrictMode> </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 // Store for app-wide state
interface ProjectStore { interface ProjectStore {
// Inputs // Inputs
inputMode: "image" | "video" | "text"; inputMode: "image" | "video";
setInputMode: (mode: "image" | "video" | "text") => void; setInputMode: (mode: "image" | "video") => void;
isImportedFromCode: boolean; isImportedFromCode: boolean;
setIsImportedFromCode: (imported: boolean) => void; setIsImportedFromCode: (imported: boolean) => void;
referenceImages: string[]; 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 { export interface CodeGenerationParams {
generationType: "create" | "update"; generationType: "create" | "update";
inputMode: "image" | "video" | "text"; inputMode: "image" | "video";
image: string; image: string;
resultImage?: string; resultImage?: string;
history?: string[]; history?: string[];
isImportedFromCode?: boolean; isImportedFromCode?: boolean;
authToken?: string;
} }
export type FullGenerationSettings = CodeGenerationParams & Settings; export type FullGenerationSettings = CodeGenerationParams & Settings;

View File

@ -16,7 +16,7 @@ export default ({ mode }) => {
inject: { inject: {
data: { data: {
injectHead: process.env.VITE_IS_DEPLOYED 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" resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== 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": "@codemirror/autocomplete@^6.0.0":
version "6.11.0" version "6.11.0"
resolved "https://registry.yarnpkg.com/@codemirror/autocomplete/-/autocomplete-6.11.0.tgz#406dee8bf5342dfb48920ad75454d3406ddf9963" 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" resolved "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz"
integrity sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw== 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": "@istanbuljs/load-nyc-config@^1.0.0":
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced" 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" "@babel/runtime" "^7.13.10"
"@radix-ui/react-primitive" "1.0.3" "@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": "@radix-ui/react-checkbox@^1.0.4":
version "1.0.4" version "1.0.4"
resolved "https://registry.yarnpkg.com/@radix-ui/react-checkbox/-/react-checkbox-1.0.4.tgz#98f22c38d5010dd6df4c5744cac74087e3275f4b" 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-callback-ref" "1.0.1"
"@radix-ui/react-use-escape-keydown" "1.0.3" "@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": "@radix-ui/react-focus-guards@1.0.1":
version "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" 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" "@babel/runtime" "^7.13.10"
"@radix-ui/react-primitive" "1.0.3" "@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": "@radix-ui/react-popover@^1.0.7":
version "1.0.7" version "1.0.7"
resolved "https://registry.yarnpkg.com/@radix-ui/react-popover/-/react-popover-1.0.7.tgz#23eb7e3327330cb75ec7b4092d685398c1654e3c" resolved "https://registry.yarnpkg.com/@radix-ui/react-popover/-/react-popover-1.0.7.tgz#23eb7e3327330cb75ec7b4092d685398c1654e3c"
@ -1877,18 +1795,6 @@
dependencies: dependencies:
"@sinonjs/commons" "^3.0.0" "@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": "@tootallnate/quickjs-emscripten@^0.23.0":
version "0.23.0" version "0.23.0"
resolved "https://registry.yarnpkg.com/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz#db4ecfd499a9765ab24002c3b696d02e6d32a12c" resolved "https://registry.yarnpkg.com/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz#db4ecfd499a9765ab24002c3b696d02e6d32a12c"
@ -1952,11 +1858,6 @@
dependencies: dependencies:
"@types/node" "*" "@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": "@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1":
version "2.0.6" version "2.0.6"
resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz#7739c232a1fee9b4d3ce8985f314c0c6d33549d7" 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: dependencies:
source-map "~0.6.0" 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: cliui@^8.0.1:
version "8.0.1" version "8.0.1"
resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa" 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" resolved "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz"
integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== 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: csstype@^3.0.2:
version "3.1.2" version "3.1.2"
resolved "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz" 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: dependencies:
ms "2.1.2" 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: dedent@^1.0.0:
version "1.5.3" version "1.5.3"
resolved "https://registry.yarnpkg.com/dedent/-/dedent-1.5.3.tgz#99aee19eb9bae55a67327717b6e848d0bf777e5a" resolved "https://registry.yarnpkg.com/dedent/-/dedent-1.5.3.tgz#99aee19eb9bae55a67327717b6e848d0bf777e5a"
@ -3353,9 +3237,9 @@ extract-zip@2.0.1:
optionalDependencies: optionalDependencies:
"@types/yauzl" "^2.9.1" "@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" 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== integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
fast-fifo@^1.1.0, fast-fifo@^1.2.0: fast-fifo@^1.1.0, fast-fifo@^1.2.0:
@ -3405,11 +3289,6 @@ fd-slicer@~1.1.0:
dependencies: dependencies:
pend "~1.2.0" 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: file-entry-cache@^6.0.1:
version "6.0.1" version "6.0.1"
resolved "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz" 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: dependencies:
is-glob "^4.0.3" 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: glob@7.1.6:
version "7.1.6" version "7.1.6"
resolved "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz" 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" resolved "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz"
integrity sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q== 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: "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
version "4.0.0" version "4.0.0"
resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz" 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" resolved "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz"
integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== 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: local-pkg@^0.5.0:
version "0.5.0" version "0.5.0"
resolved "https://registry.yarnpkg.com/local-pkg/-/local-pkg-0.5.0.tgz#093d25a346bae59a99f80e75f6e9d36d7e8c925c" 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" pkg-types "^1.0.3"
ufo "^1.3.0" 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: ms@2.1.2:
version "2.1.2" version "2.1.2"
resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz" resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz"
@ -4945,19 +4804,6 @@ postcss@^8.4.32:
picocolors "^1.0.0" picocolors "^1.0.0"
source-map-js "^1.0.2" 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: prelude-ls@^1.2.1:
version "1.2.1" version "1.2.1"
resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz" 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" kleur "^3.0.3"
sisteransi "^1.0.5" sisteransi "^1.0.5"
prop-types@15.8.1, prop-types@^15.8.1: prop-types@^15.8.1:
version "15.8.1" version "15.8.1"
resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz" resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz"
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
@ -5170,24 +5016,6 @@ react-style-singleton@^2.2.1:
invariant "^2.2.4" invariant "^2.2.4"
tslib "^2.0.0" 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: react@^18.2.0:
version "18.2.0" version "18.2.0"
resolved "https://registry.npmjs.org/react/-/react-18.2.0.tgz" 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" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04"
integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== 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: sisteransi@^1.0.5:
version "1.0.5" version "1.0.5"
resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" 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" resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.6.0.tgz#94807562bddc68fa90f2e02c5fd5b6865bb4e98e"
integrity sha512-aFZ19IgVmhdB2uX599ve2kE6BIE3YMnQ6Gp6BURhW/oIzpXGKr878TQfAQZn1+i0Flcc/UKUy1gOlcfaUBCryg== 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: streamx@^2.13.0, streamx@^2.15.0:
version "2.16.1" version "2.16.1"
resolved "https://registry.yarnpkg.com/streamx/-/streamx-2.16.1.tgz#2b311bd34832f08aa6bb4d6a80297c9caef89614" 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" resolved "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz"
integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== 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: tailwind-merge@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/tailwind-merge/-/tailwind-merge-2.0.0.tgz#a0f3a8c874ebae5feec5595614d08245a5f88a39" 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" semver "^7.5.3"
yargs-parser "^21.0.1" 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: tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.4.0:
version "2.6.2" version "2.6.2"
resolved "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz" 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" 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== 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: util-deprecate@^1.0.2:
version "1.0.2" version "1.0.2"
resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" 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" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-1.0.0.tgz#7f816433fb2cbc511ec8bf7d263c3b58a1a3c251"
integrity sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g== 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: zod@3.22.4:
version "3.22.4" version "3.22.4"
resolved "https://registry.yarnpkg.com/zod/-/zod-3.22.4.tgz#f31c3a9386f61b1f228af56faa9255e845cf3fff" resolved "https://registry.yarnpkg.com/zod/-/zod-3.22.4.tgz#f31c3a9386f61b1f228af56faa9255e845cf3fff"