Compare commits
255 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fc70ecac6b | ||
|
|
06ce9f8c32 | ||
|
|
1bf3340502 | ||
|
|
76defbb1c2 | ||
|
|
1b37c4e063 | ||
|
|
7b08507b61 | ||
|
|
659b82b214 | ||
|
|
b41f3c65c7 | ||
|
|
2d1c1019ac | ||
|
|
d7666fe5ae | ||
|
|
76188a7465 | ||
|
|
6379a1f1c6 | ||
|
|
3a51e83ee9 | ||
|
|
a03e823ecb | ||
|
|
73827716f9 | ||
|
|
efc575e033 | ||
|
|
56526636fb | ||
|
|
da806ff6f2 | ||
|
|
2888893072 | ||
|
|
33793051bc | ||
|
|
f9aa14b566 | ||
|
|
960e905a73 | ||
|
|
f1501c0aaf | ||
|
|
6600035267 | ||
|
|
86f6781682 | ||
|
|
c107f4eda5 | ||
|
|
901d3e87ed | ||
|
|
f0fef4f5e3 | ||
|
|
92f2933f0a | ||
|
|
0a953fde63 | ||
|
|
ba317d3e2d | ||
|
|
3d056fcb10 | ||
|
|
2d877c0d84 | ||
|
|
a40e5da1be | ||
|
|
e3e6e6a884 | ||
|
|
8d1f76abec | ||
|
|
13d3ff6e38 | ||
|
|
3d44c4406d | ||
|
|
64a4ab8c90 | ||
|
|
4b616342c6 | ||
|
|
b7985b9a4e | ||
|
|
97bd1c924a | ||
|
|
ad9c3eafcc | ||
|
|
75317cb542 | ||
|
|
f17a6c1242 | ||
|
|
9230162a8c | ||
|
|
2274268b2f | ||
|
|
75e93a66b8 | ||
|
|
54557de762 | ||
|
|
f9c35839de | ||
|
|
1b22c1ab14 | ||
|
|
138cc1f1a5 | ||
|
|
b733400e91 | ||
|
|
f7d0dbb7ce | ||
|
|
c092418e47 | ||
|
|
f9d6279b53 | ||
|
|
5c7aa62414 | ||
|
|
1a26e325be | ||
|
|
50d1288693 | ||
|
|
4bf472cc23 | ||
|
|
456374dc9d | ||
|
|
8a6557ce0c | ||
|
|
ba277121db | ||
|
|
b80f0ec7bf | ||
|
|
55ffd26a52 | ||
|
|
457b95d272 | ||
|
|
b7c81158ca | ||
|
|
1d1cbfc6f6 | ||
|
|
7f7d0c3504 | ||
|
|
8c11366bd0 | ||
|
|
7c132fd1bd | ||
|
|
2ec4bf59d3 | ||
|
|
d13ae72c06 | ||
|
|
d8b1c0ba4d | ||
|
|
0171ff9f3b | ||
|
|
549210dcd4 | ||
|
|
c1fa5c4e97 | ||
|
|
92ea62e9bf | ||
|
|
1f9e93a2c7 | ||
|
|
5e00b406af | ||
|
|
cda34ae642 | ||
|
|
b44f583a90 | ||
|
|
a928631a49 | ||
|
|
efdad0ad50 | ||
|
|
cea45af385 | ||
|
|
fed7fe50d6 | ||
|
|
8951864d63 | ||
|
|
d2369cb0a0 | ||
|
|
ae08466405 | ||
|
|
df5f954ee2 | ||
|
|
5109695873 | ||
|
|
d56242af2c | ||
|
|
7d4d62aa41 | ||
|
|
29978828a4 | ||
|
|
2f260f5442 | ||
|
|
801458eb50 | ||
|
|
214dbb60e6 | ||
|
|
d4e3405d0a | ||
|
|
d96931eeae | ||
|
|
7293c979df | ||
|
|
b191d72d12 | ||
|
|
3e9cf7fdcb | ||
|
|
f84b9134e5 | ||
|
|
df800c7ab3 | ||
|
|
14932448fb | ||
|
|
b4d8618838 | ||
|
|
9a910c98ea | ||
|
|
a4ede44a0e | ||
|
|
6e29558a4d | ||
|
|
4ac083eb46 | ||
|
|
0b9e44653d | ||
|
|
6e3de3b1d7 | ||
|
|
8c0d820140 | ||
|
|
558e8634eb | ||
|
|
ea7d238606 | ||
|
|
9df95d9916 | ||
|
|
9a13fcc3d0 | ||
|
|
711c193b32 | ||
|
|
a227704d41 | ||
|
|
aed7e3dacf | ||
|
|
07fc02a15d | ||
|
|
c6b04aefbe | ||
|
|
9730f6ae7f | ||
|
|
522b7b8e23 | ||
|
|
c51ff4d7ad | ||
|
|
60171bcc0b | ||
|
|
1aec82f53d | ||
|
|
a197bd9223 | ||
|
|
fa944890ad | ||
|
|
575847f845 | ||
|
|
273a77f7c4 | ||
|
|
87d5a1da57 | ||
|
|
ab8baca1ab | ||
|
|
ac86e42126 | ||
|
|
e1f39cac78 | ||
|
|
7f8e0cbbd1 | ||
|
|
6ea61b472d | ||
|
|
196cf5865b | ||
|
|
1d8f6641a2 | ||
|
|
3a1634bac2 | ||
|
|
f171ec1e6e | ||
|
|
ea02e2daea | ||
|
|
8209c7bd26 | ||
|
|
b6222ddbc9 | ||
|
|
785a135460 | ||
|
|
7edbe16325 | ||
|
|
f67cfb6d8a | ||
|
|
f6079542f7 | ||
|
|
71dfde3892 | ||
|
|
b66dcc5df5 | ||
|
|
c303c32996 | ||
|
|
b4fb612856 | ||
|
|
2950c3cebe | ||
|
|
9f7c0b4b35 | ||
|
|
ab15aff021 | ||
|
|
e9140c331b | ||
|
|
de834d83e5 | ||
|
|
ac858e252b | ||
|
|
b04dba001d | ||
|
|
234e806a6d | ||
|
|
d834940c24 | ||
|
|
25fccc0ef5 | ||
|
|
5256d428e0 | ||
|
|
a46ff8692c | ||
|
|
cddc99dc19 | ||
|
|
f3ca39b40a | ||
|
|
97591336c3 | ||
|
|
b35738524b | ||
|
|
a2c0ac1171 | ||
|
|
dbbb29f0d1 | ||
|
|
94ac6760b7 | ||
|
|
77a8377e99 | ||
|
|
c70e4958b9 | ||
|
|
56c90d9c83 | ||
|
|
71ceeb533f | ||
|
|
a165df2735 | ||
|
|
c7482be855 | ||
|
|
2bc22b1653 | ||
|
|
8ff2037579 | ||
|
|
1f5bec4521 | ||
|
|
5d7fe8b363 | ||
|
|
a974c91c76 | ||
|
|
231d334679 | ||
|
|
9bc5817aa4 | ||
|
|
204d449dd4 | ||
|
|
dde125b8c0 | ||
|
|
c3b7ff7246 | ||
|
|
1a5f05d574 | ||
|
|
730e58da72 | ||
|
|
b579f326dd | ||
|
|
d5364fb5aa | ||
|
|
dc3de0b470 | ||
|
|
54017dbcd9 | ||
|
|
dcb0116c06 | ||
|
|
0f16c1d8a2 | ||
|
|
bc9330fd57 | ||
|
|
44d3776bd8 | ||
|
|
732ffc33e1 | ||
|
|
ff3ca97241 | ||
|
|
d9cb13b1c2 | ||
|
|
08cd5384be | ||
|
|
d3ec75873c | ||
|
|
0d74d43eb6 | ||
|
|
1cdfd7d1ac | ||
|
|
59e8974ebc | ||
|
|
64789d1c29 | ||
|
|
adbc459347 | ||
|
|
af8fb1b9bb | ||
|
|
9ccc47920a | ||
|
|
70a67c0ea7 | ||
|
|
7155a0616e | ||
|
|
0ea6f091e8 | ||
|
|
b8a94e7efb | ||
|
|
7975e0a6f7 | ||
|
|
5a114866f2 | ||
|
|
2dbf5a3c3f | ||
|
|
1fb390e48c | ||
|
|
c5e0a536ce | ||
|
|
0e51e59554 | ||
|
|
d72912f11c | ||
|
|
4c4a19a40a | ||
|
|
67e1d2b3d3 | ||
|
|
afb1b3b036 | ||
|
|
0f7425eb3b | ||
|
|
d21ccc5627 | ||
|
|
737062d091 | ||
|
|
7d67abb3a3 | ||
|
|
b8b5e933bd | ||
|
|
5568672416 | ||
|
|
1081c100aa | ||
|
|
e031662b13 | ||
|
|
4b77361494 | ||
|
|
0d9f93ea1f | ||
|
|
b7d808b227 | ||
|
|
0979fd5f2b | ||
|
|
bb5be35928 | ||
|
|
bc036f04e7 | ||
|
|
a6be4379e5 | ||
|
|
0143223290 | ||
|
|
9b87034846 | ||
|
|
228e59a46e | ||
|
|
46fdd36f11 | ||
|
|
058aee85f2 | ||
|
|
8d3643a2c3 | ||
|
|
0222cd06a0 | ||
|
|
b3139f9f54 | ||
|
|
dfdef74b21 | ||
|
|
8dd0c658d5 | ||
|
|
439fb89645 | ||
|
|
7a7be7460f | ||
|
|
7eb88b2cec | ||
|
|
e84ac95603 | ||
|
|
7190bb461f | ||
|
|
37b4db944c | ||
|
|
2dddc479b2 |
@ -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/
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
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
|
||||||
@ -11,4 +13,8 @@ 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
|
||||||
|
|||||||
@ -22,3 +22,12 @@ 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", "")
|
||||||
|
|||||||
@ -4,4 +4,5 @@ from typing import Literal
|
|||||||
InputMode = Literal[
|
InputMode = Literal[
|
||||||
"image",
|
"image",
|
||||||
"video",
|
"video",
|
||||||
|
"text",
|
||||||
]
|
]
|
||||||
|
|||||||
@ -3,6 +3,7 @@ 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
|
||||||
|
|
||||||
@ -29,6 +30,10 @@ 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)
|
||||||
|
|||||||
@ -4,6 +4,7 @@ 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
|
||||||
@ -12,6 +13,7 @@ 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"
|
||||||
@ -62,6 +64,25 @@ 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
|
||||||
@ -138,6 +159,14 @@ 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()
|
||||||
|
|
||||||
|
|||||||
@ -4,10 +4,27 @@ 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
|
||||||
|
|||||||
1590
backend/poetry.lock
generated
1590
backend/poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@ -5,6 +5,7 @@ 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
|
||||||
|
|
||||||
@ -42,12 +43,17 @@ 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 params.get("resultImage"):
|
if input_mode == "image":
|
||||||
prompt_messages = assemble_prompt(
|
if params.get("resultImage"):
|
||||||
params["image"], stack, params["resultImage"]
|
prompt_messages = assemble_prompt(
|
||||||
)
|
params["image"], stack, params["resultImage"]
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
prompt_messages = assemble_prompt(params["image"], stack)
|
||||||
|
elif input_mode == "text":
|
||||||
|
prompt_messages = assemble_text_prompt(params["image"], stack)
|
||||||
else:
|
else:
|
||||||
prompt_messages = assemble_prompt(params["image"], stack)
|
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
|
||||||
@ -132,3 +138,22 @@ 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,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|||||||
37
backend/prompts/test_text_prompts.py
Normal file
37
backend/prompts/test_text_prompts.py
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
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()
|
||||||
126
backend/prompts/text_prompts.py
Normal file
126
backend/prompts/text_prompts.py
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
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,
|
||||||
|
)
|
||||||
@ -17,6 +17,7 @@ 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"
|
||||||
|
|||||||
@ -2,13 +2,14 @@ import asyncio
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
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,
|
||||||
)
|
)
|
||||||
@ -20,9 +21,12 @@ 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 Any, Callable, Coroutine, Dict, List, Literal, cast, get_args
|
from typing import Dict, 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
|
||||||
@ -87,6 +91,7 @@ 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
|
||||||
@ -94,6 +99,7 @@ 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(
|
||||||
@ -123,14 +129,61 @@ 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}")
|
||||||
|
|
||||||
openai_api_key = get_from_settings_dialog_or_env(
|
# Read the auth token from the request (on the hosted version)
|
||||||
params, "openAiApiKey", OPENAI_API_KEY
|
auth_token = params.get("authToken")
|
||||||
)
|
if not auth_token:
|
||||||
|
await throw_error("You need to be logged in to use screenshot to code")
|
||||||
|
raise Exception("No auth token")
|
||||||
|
|
||||||
# If neither is provided, we throw an error later only if Claude is used.
|
openai_api_key = None
|
||||||
anthropic_api_key = get_from_settings_dialog_or_env(
|
anthropic_api_key = None
|
||||||
params, "anthropicApiKey", ANTHROPIC_API_KEY
|
|
||||||
)
|
# 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":
|
||||||
|
raise Exception("No payment method found")
|
||||||
|
|
||||||
# Base URL for OpenAI API
|
# Base URL for OpenAI API
|
||||||
openai_base_url: str | None = None
|
openai_base_url: str | None = None
|
||||||
@ -143,9 +196,12 @@ 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 = bool(params.get("isImageGenerationEnabled", True))
|
should_generate_images = (
|
||||||
|
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,
|
||||||
@ -153,6 +209,7 @@ 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,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -206,6 +263,7 @@ 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
|
||||||
@ -213,6 +271,11 @@ 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)
|
||||||
@ -246,9 +309,13 @@ 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"
|
||||||
@ -265,26 +332,35 @@ 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 = ["anthropic", "openai"]
|
variant_models = [
|
||||||
|
Llm.CLAUDE_3_5_SONNET_2024_06_20,
|
||||||
|
Llm.GPT_4O_2024_05_13,
|
||||||
|
]
|
||||||
elif openai_api_key:
|
elif openai_api_key:
|
||||||
variant_models = ["openai", "openai"]
|
variant_models = [
|
||||||
|
Llm.GPT_4O_2024_05_13,
|
||||||
|
Llm.GPT_4O_2024_05_13,
|
||||||
|
]
|
||||||
elif anthropic_api_key:
|
elif anthropic_api_key:
|
||||||
variant_models = ["anthropic", "anthropic"]
|
variant_models = [
|
||||||
|
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 == "openai":
|
if model == Llm.GPT_4O_2024_05_13:
|
||||||
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.")
|
||||||
@ -298,7 +374,7 @@ async def stream_code(websocket: WebSocket):
|
|||||||
model=Llm.GPT_4O_2024_05_13,
|
model=Llm.GPT_4O_2024_05_13,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
elif model == "anthropic":
|
elif model == Llm.CLAUDE_3_5_SONNET_2024_06_20:
|
||||||
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.")
|
||||||
@ -328,6 +404,12 @@ 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)
|
||||||
|
|
||||||
@ -372,10 +454,27 @@ 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, completions[0])
|
# write_logs(prompt_messages, completion) # type: ignore
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
|||||||
58
backend/routes/logging_utils.py
Normal file
58
backend/routes/logging_utils.py
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
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
|
||||||
25
backend/routes/saas_utils.py
Normal file
25
backend/routes/saas_utils.py
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
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
|
||||||
@ -2,6 +2,9 @@ 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()
|
||||||
|
|
||||||
@ -12,10 +15,31 @@ 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, device: str = "desktop"
|
target_url: str, api_key: str | None, auth_token: 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,
|
||||||
@ -44,7 +68,8 @@ async def capture_screenshot(
|
|||||||
|
|
||||||
class ScreenshotRequest(BaseModel):
|
class ScreenshotRequest(BaseModel):
|
||||||
url: str
|
url: str
|
||||||
apiKey: str
|
apiKey: str | None
|
||||||
|
authToken: str
|
||||||
|
|
||||||
|
|
||||||
class ScreenshotResponse(BaseModel):
|
class ScreenshotResponse(BaseModel):
|
||||||
@ -56,9 +81,10 @@ 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)
|
image_bytes = await capture_screenshot(url, api_key=api_key, auth_token=auth_token)
|
||||||
|
|
||||||
# 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")
|
||||||
|
|||||||
@ -16,6 +16,21 @@
|
|||||||
<!-- 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 -->
|
||||||
|
|||||||
@ -13,12 +13,16 @@
|
|||||||
"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",
|
||||||
@ -30,12 +34,15 @@
|
|||||||
"@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",
|
||||||
@ -43,6 +50,8 @@
|
|||||||
"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",
|
||||||
|
|||||||
BIN
frontend/public/demos/youtube.mp4
Normal file
BIN
frontend/public/demos/youtube.mp4
Normal file
Binary file not shown.
@ -1,31 +1,46 @@
|
|||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef, useState } 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";
|
||||||
|
|
||||||
function App() {
|
interface Props {
|
||||||
|
navbarComponent?: JSX.Element;
|
||||||
|
}
|
||||||
|
|
||||||
|
function App({ navbarComponent }: Props) {
|
||||||
|
const [initialPrompt, setInitialPrompt] = useState<string>("");
|
||||||
|
|
||||||
|
// Relevant for hosted version only
|
||||||
|
// TODO: Move to AppContainer
|
||||||
|
const { getToken } = useAuth();
|
||||||
|
const subscriberTier = useStore((state) => state.subscriberTier);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
// Inputs
|
// Inputs
|
||||||
inputMode,
|
inputMode,
|
||||||
@ -140,12 +155,21 @@ function App() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addEvent("Regenerate");
|
||||||
|
|
||||||
// Re-run the create
|
// Re-run the create
|
||||||
doCreate(referenceImages, inputMode);
|
if (inputMode === "image" || inputMode === "video") {
|
||||||
|
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);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -170,7 +194,7 @@ function App() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
function doGenerateCode(params: CodeGenerationParams) {
|
async function doGenerateCode(params: CodeGenerationParams) {
|
||||||
// Reset the execution console
|
// Reset the execution console
|
||||||
resetExecutionConsoles();
|
resetExecutionConsoles();
|
||||||
|
|
||||||
@ -178,7 +202,12 @@ function App() {
|
|||||||
setAppState(AppState.CODING);
|
setAppState(AppState.CODING);
|
||||||
|
|
||||||
// Merge settings with params
|
// Merge settings with params
|
||||||
const updatedParams = { ...params, ...settings };
|
const authToken = await getToken();
|
||||||
|
const updatedParams = {
|
||||||
|
...params,
|
||||||
|
...settings,
|
||||||
|
authToken: authToken || undefined,
|
||||||
|
};
|
||||||
|
|
||||||
const baseCommitObject = {
|
const baseCommitObject = {
|
||||||
variants: [{ code: "" }, { code: "" }],
|
variants: [{ code: "" }, { code: "" }],
|
||||||
@ -227,13 +256,17 @@ function App() {
|
|||||||
},
|
},
|
||||||
// On complete
|
// On complete
|
||||||
() => {
|
() => {
|
||||||
|
addEvent("CreateSuccessful");
|
||||||
setAppState(AppState.CODE_READY);
|
setAppState(AppState.CODE_READY);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initial version creation
|
// Initial version creation
|
||||||
function doCreate(referenceImages: string[], inputMode: "image" | "video") {
|
async function doCreate(
|
||||||
|
referenceImages: string[],
|
||||||
|
inputMode: "image" | "video"
|
||||||
|
) {
|
||||||
// Reset any existing state
|
// Reset any existing state
|
||||||
reset();
|
reset();
|
||||||
|
|
||||||
@ -243,6 +276,7 @@ function App() {
|
|||||||
|
|
||||||
// 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],
|
||||||
@ -251,6 +285,19 @@ function App() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
@ -272,6 +319,7 @@ function App() {
|
|||||||
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."
|
||||||
);
|
);
|
||||||
@ -293,10 +341,11 @@ function App() {
|
|||||||
? await takeScreenshot()
|
? await takeScreenshot()
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
|
addEvent("Edit");
|
||||||
doGenerateCode({
|
doGenerateCode({
|
||||||
generationType: "update",
|
generationType: "update",
|
||||||
inputMode,
|
inputMode,
|
||||||
image: referenceImages[0],
|
image: inputMode === "text" ? initialPrompt : referenceImages[0],
|
||||||
resultImage,
|
resultImage,
|
||||||
history: updatedHistory,
|
history: updatedHistory,
|
||||||
isImportedFromCode,
|
isImportedFromCode,
|
||||||
@ -345,7 +394,7 @@ function App() {
|
|||||||
{IS_RUNNING_ON_CLOUD && <PicoBadge />}
|
{IS_RUNNING_ON_CLOUD && <PicoBadge />}
|
||||||
{IS_RUNNING_ON_CLOUD && (
|
{IS_RUNNING_ON_CLOUD && (
|
||||||
<TermsOfServiceDialog
|
<TermsOfServiceDialog
|
||||||
open={!settings.isTermOfServiceAccepted}
|
open={false}
|
||||||
onOpenChange={handleTermDialogOpenChange}
|
onOpenChange={handleTermDialogOpenChange}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@ -366,7 +415,14 @@ function App() {
|
|||||||
{/* 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 && !settings.openAiApiKey && <OnboardingNote />}
|
{IS_RUNNING_ON_CLOUD &&
|
||||||
|
!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 ||
|
||||||
@ -382,6 +438,8 @@ function App() {
|
|||||||
</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}
|
||||||
|
|||||||
@ -7,6 +7,8 @@ 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,
|
||||||
@ -82,6 +84,17 @@ 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) =>
|
||||||
@ -173,24 +186,29 @@ function ImageUpload({ setReferenceImages }: Props) {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{screenRecorderState === ScreenRecorderState.INITIAL && (
|
{/* Disable on prod for now */}
|
||||||
<div className="text-center text-sm text-slate-800 mt-4">
|
{!IS_RUNNING_ON_CLOUD && (
|
||||||
<Badge>New!</Badge> Upload a screen recording (.mp4, .mov) or record
|
<>
|
||||||
your screen to clone a whole app (experimental).{" "}
|
{screenRecorderState === ScreenRecorderState.INITIAL && (
|
||||||
<a
|
<div className="text-center text-sm text-slate-800 mt-4">
|
||||||
className="underline"
|
<Badge>New!</Badge> Upload a screen recording (.mp4, .mov) or
|
||||||
href={URLS["intro-to-video"]}
|
record your screen to clone a whole app (experimental).{" "}
|
||||||
target="_blank"
|
<a
|
||||||
>
|
className="underline"
|
||||||
Learn more.
|
href={URLS["intro-to-video"]}
|
||||||
</a>
|
target="_blank"
|
||||||
</div>
|
>
|
||||||
|
Learn more.
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<ScreenRecorder
|
||||||
|
screenRecorderState={screenRecorderState}
|
||||||
|
setScreenRecorderState={setScreenRecorderState}
|
||||||
|
generateCode={setReferenceImages}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
<ScreenRecorder
|
|
||||||
screenRecorderState={screenRecorderState}
|
|
||||||
setScreenRecorderState={setScreenRecorderState}
|
|
||||||
generateCode={setReferenceImages}
|
|
||||||
/>
|
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,9 +7,7 @@ import {
|
|||||||
AlertDialogHeader,
|
AlertDialogHeader,
|
||||||
AlertDialogTitle,
|
AlertDialogTitle,
|
||||||
} from "./ui/alert-dialog";
|
} from "./ui/alert-dialog";
|
||||||
import { Input } from "./ui/input";
|
import { addEvent } from "../lib/analytics";
|
||||||
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"];
|
||||||
|
|
||||||
@ -17,40 +15,19 @@ 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">
|
||||||
Enter your email to get started
|
One last step
|
||||||
</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>
|
||||||
By providing your email, you consent to receiving occasional product
|
You consent to receiving occasional product updates via email, and
|
||||||
updates, and you accept the{" "}
|
you accept the{" "}
|
||||||
<a
|
<a
|
||||||
href="https://a.picoapps.xyz/camera-write"
|
href="https://a.picoapps.xyz/camera-write"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@ -76,13 +53,8 @@ const TermsOfServiceDialog: React.FC<{
|
|||||||
|
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
<AlertDialogAction
|
<AlertDialogAction
|
||||||
onClick={(e) => {
|
onClick={() => {
|
||||||
if (!email.trim() || !email.trim().includes("@")) {
|
addEvent("EmailSubmit");
|
||||||
e.preventDefault();
|
|
||||||
toast.error("Please enter your email");
|
|
||||||
} else {
|
|
||||||
onSubscribe();
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Agree & Continue
|
Agree & Continue
|
||||||
|
|||||||
@ -3,6 +3,8 @@ 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;
|
||||||
@ -13,28 +15,31 @@ 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 (!screenshotOneApiKey) {
|
if (!referenceUrl) {
|
||||||
toast.error(
|
return toast.error("Please enter a URL");
|
||||||
"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 (!referenceUrl) {
|
if (!screenshotOneApiKey && subscriberTier === "free") {
|
||||||
toast.error("Please enter a URL");
|
return toast.error(
|
||||||
return;
|
"Please upgrade to a paid plan to use the screenshot feature."
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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",
|
||||||
|
|||||||
11
frontend/src/components/core/FullPageSpinner.tsx
Normal file
11
frontend/src/components/core/FullPageSpinner.tsx
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import Spinner from "../core/Spinner";
|
||||||
|
|
||||||
|
function FullPageSpinner() {
|
||||||
|
return (
|
||||||
|
<div className="w-full h-screen flex items-center justify-center">
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FullPageSpinner;
|
||||||
@ -0,0 +1,57 @@
|
|||||||
|
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;
|
||||||
@ -46,7 +46,7 @@ export default function HistoryDisplay({ shouldDisableReverts }: Props) {
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="flex justify-between truncate flex-1 p-2"
|
className="flex justify-between truncate flex-1 p-2 plausible-event-name=HistoryClick"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
shouldDisableReverts
|
shouldDisableReverts
|
||||||
? toast.error(
|
? toast.error(
|
||||||
|
|||||||
101
frontend/src/components/hosted/AppContainer.tsx
Normal file
101
frontend/src/components/hosted/AppContainer.tsx
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
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;
|
||||||
154
frontend/src/components/hosted/AvatarDropdown.tsx
Normal file
154
frontend/src/components/hosted/AvatarDropdown.tsx
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
20
frontend/src/components/hosted/CheckoutSuccessPage.tsx
Normal file
20
frontend/src/components/hosted/CheckoutSuccessPage.tsx
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
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;
|
||||||
47
frontend/src/components/hosted/FAQs.tsx
Normal file
47
frontend/src/components/hosted/FAQs.tsx
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
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;
|
||||||
14
frontend/src/components/hosted/FaqsPage.tsx
Normal file
14
frontend/src/components/hosted/FaqsPage.tsx
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
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;
|
||||||
174
frontend/src/components/hosted/LandingPage.tsx
Normal file
174
frontend/src/components/hosted/LandingPage.tsx
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
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;
|
||||||
35
frontend/src/components/hosted/LandingPage/Footer.tsx
Normal file
35
frontend/src/components/hosted/LandingPage/Footer.tsx
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
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;
|
||||||
19
frontend/src/components/hosted/PricingPage.tsx
Normal file
19
frontend/src/components/hosted/PricingPage.tsx
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
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;
|
||||||
48
frontend/src/components/hosted/StripeCustomerPortalLink.tsx
Normal file
48
frontend/src/components/hosted/StripeCustomerPortalLink.tsx
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
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;
|
||||||
70
frontend/src/components/hosted/payments/PricingDialog.tsx
Normal file
70
frontend/src/components/hosted/payments/PricingDialog.tsx
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
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;
|
||||||
145
frontend/src/components/hosted/payments/PricingPlans.tsx
Normal file
145
frontend/src/components/hosted/payments/PricingPlans.tsx
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
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;
|
||||||
71
frontend/src/components/hosted/payments/useStripeCheckout.ts
Normal file
71
frontend/src/components/hosted/payments/useStripeCheckout.ts
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
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 };
|
||||||
|
}
|
||||||
17
frontend/src/components/hosted/types.ts
Normal file
17
frontend/src/components/hosted/types.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
// 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;
|
||||||
|
}
|
||||||
44
frontend/src/components/hosted/useAuthenticatedFetch.ts
Normal file
44
frontend/src/components/hosted/useAuthenticatedFetch.ts
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
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;
|
||||||
|
};
|
||||||
6
frontend/src/components/hosted/utils.ts
Normal file
6
frontend/src/components/hosted/utils.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export function capitalize(str: string): string {
|
||||||
|
if (str.length === 0) {
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
|
||||||
|
}
|
||||||
@ -1,25 +1,18 @@
|
|||||||
|
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"
|
className="inline underline hover:opacity-70 cursor-pointer"
|
||||||
href="https://buy.stripe.com/8wM6sre70gBW1nqaEE"
|
onClick={() => setPricingDialogOpen(true)}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
buy some credits (100 generations for $36)
|
Subscribe to get started
|
||||||
</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>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,7 +1,12 @@
|
|||||||
|
import PricingDialog from "../hosted/payments/PricingDialog";
|
||||||
|
|
||||||
export function PicoBadge() {
|
export function PicoBadge() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<a
|
<div>
|
||||||
|
<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"
|
||||||
>
|
>
|
||||||
@ -9,17 +14,9 @@ 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"
|
||||||
>
|
>
|
||||||
feature requests?
|
feedback
|
||||||
</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>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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"
|
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"
|
||||||
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"
|
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"
|
||||||
>
|
>
|
||||||
Open in{" "}
|
Open in{" "}
|
||||||
<img
|
<img
|
||||||
|
|||||||
@ -8,6 +8,7 @@ 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;
|
||||||
@ -43,7 +44,10 @@ function OutputSettingsSection({
|
|||||||
<span>{label}</span>
|
<span>{label}</span>
|
||||||
<Select
|
<Select
|
||||||
value={stack}
|
value={stack}
|
||||||
onValueChange={(value: string) => setStack(value as Stack)}
|
onValueChange={(value: string) => {
|
||||||
|
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">
|
||||||
|
|||||||
@ -46,46 +46,51 @@ function SettingsDialog({ settings, setSettings }: Props) {
|
|||||||
<DialogTitle className="mb-4">Settings</DialogTitle>
|
<DialogTitle className="mb-4">Settings</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<div className="flex items-center space-x-2">
|
{!IS_RUNNING_ON_CLOUD && (
|
||||||
<Label htmlFor="image-generation">
|
<div className="flex items-center space-x-2">
|
||||||
<div>DALL-E Placeholder Image Generation</div>
|
<Label htmlFor="image-generation">
|
||||||
<div className="font-light mt-2 text-xs">
|
<div>DALL-E Placeholder Image Generation</div>
|
||||||
More fun with it but if you want to save money, turn it off.
|
<div className="font-light mt-2 text-xs">
|
||||||
</div>
|
More fun with it but if you want to save money, turn it off.
|
||||||
</Label>
|
|
||||||
<Switch
|
|
||||||
id="image-generation"
|
|
||||||
checked={settings.isImageGenerationEnabled}
|
|
||||||
onCheckedChange={() =>
|
|
||||||
setSettings((s) => ({
|
|
||||||
...s,
|
|
||||||
isImageGenerationEnabled: !s.isImageGenerationEnabled,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col space-y-6">
|
|
||||||
<div>
|
|
||||||
<Label htmlFor="openai-api-key">
|
|
||||||
<div>OpenAI API key</div>
|
|
||||||
<div className="font-light mt-1 mb-2 text-xs leading-relaxed">
|
|
||||||
Only stored in your browser. Never stored on servers. Overrides
|
|
||||||
your .env config.
|
|
||||||
</div>
|
</div>
|
||||||
</Label>
|
</Label>
|
||||||
|
<Switch
|
||||||
<Input
|
id="image-generation"
|
||||||
id="openai-api-key"
|
checked={settings.isImageGenerationEnabled}
|
||||||
placeholder="OpenAI API key"
|
onCheckedChange={() =>
|
||||||
value={settings.openAiApiKey || ""}
|
|
||||||
onChange={(e) =>
|
|
||||||
setSettings((s) => ({
|
setSettings((s) => ({
|
||||||
...s,
|
...s,
|
||||||
openAiApiKey: e.target.value,
|
isImageGenerationEnabled: !s.isImageGenerationEnabled,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex flex-col space-y-6">
|
||||||
|
{!IS_RUNNING_ON_CLOUD && (
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="openai-api-key">
|
||||||
|
<div>OpenAI API key</div>
|
||||||
|
<div className="font-light mt-1 mb-2 text-xs leading-relaxed">
|
||||||
|
Only stored in your browser. Never stored on servers.
|
||||||
|
Overrides your .env config.
|
||||||
|
</div>
|
||||||
|
</Label>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
id="openai-api-key"
|
||||||
|
placeholder="OpenAI API key"
|
||||||
|
value={settings.openAiApiKey || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
setSettings((s) => ({
|
||||||
|
...s,
|
||||||
|
openAiApiKey: e.target.value,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{!IS_RUNNING_ON_CLOUD && (
|
{!IS_RUNNING_ON_CLOUD && (
|
||||||
<div>
|
<div>
|
||||||
@ -110,61 +115,65 @@ function SettingsDialog({ settings, setSettings }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div>
|
{!IS_RUNNING_ON_CLOUD && (
|
||||||
<Label htmlFor="anthropic-api-key">
|
<div>
|
||||||
<div>Anthropic API key</div>
|
<Label htmlFor="anthropic-api-key">
|
||||||
<div className="font-light mt-1 text-xs leading-relaxed">
|
<div>Anthropic API key</div>
|
||||||
Only stored in your browser. Never stored on servers. Overrides
|
<div className="font-light mt-1 text-xs leading-relaxed">
|
||||||
your .env config.
|
Only stored in your browser. Never stored on servers.
|
||||||
</div>
|
Overrides your .env config.
|
||||||
</Label>
|
</div>
|
||||||
|
</Label>
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
id="anthropic-api-key"
|
id="anthropic-api-key"
|
||||||
placeholder="Anthropic API key"
|
placeholder="Anthropic API key"
|
||||||
value={settings.anthropicApiKey || ""}
|
value={settings.anthropicApiKey || ""}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setSettings((s) => ({
|
setSettings((s) => ({
|
||||||
...s,
|
...s,
|
||||||
anthropicApiKey: e.target.value,
|
anthropicApiKey: e.target.value,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<Accordion type="single" collapsible className="w-full">
|
{!IS_RUNNING_ON_CLOUD && (
|
||||||
<AccordionItem value="item-1">
|
<Accordion type="single" collapsible className="w-full">
|
||||||
<AccordionTrigger>Screenshot by URL Config</AccordionTrigger>
|
<AccordionItem value="item-1">
|
||||||
<AccordionContent>
|
<AccordionTrigger>Screenshot by URL Config</AccordionTrigger>
|
||||||
<Label htmlFor="screenshot-one-api-key">
|
<AccordionContent>
|
||||||
<div className="leading-normal font-normal text-xs">
|
<Label htmlFor="screenshot-one-api-key">
|
||||||
If you want to use URLs directly instead of taking the
|
<div className="leading-normal font-normal text-xs">
|
||||||
screenshot yourself, add a ScreenshotOne API key.{" "}
|
If you want to use URLs directly instead of taking the
|
||||||
<a
|
screenshot yourself, add a ScreenshotOne API key.{" "}
|
||||||
href="https://screenshotone.com?via=screenshot-to-code"
|
<a
|
||||||
className="underline"
|
href="https://screenshotone.com?via=screenshot-to-code"
|
||||||
target="_blank"
|
className="underline"
|
||||||
>
|
target="_blank"
|
||||||
Get 100 screenshots/mo for free.
|
>
|
||||||
</a>
|
Get 100 screenshots/mo for free.
|
||||||
</div>
|
</a>
|
||||||
</Label>
|
</div>
|
||||||
|
</Label>
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
id="screenshot-one-api-key"
|
id="screenshot-one-api-key"
|
||||||
className="mt-2"
|
className="mt-2"
|
||||||
placeholder="ScreenshotOne API key"
|
placeholder="ScreenshotOne API key"
|
||||||
value={settings.screenshotOneApiKey || ""}
|
value={settings.screenshotOneApiKey || ""}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setSettings((s) => ({
|
setSettings((s) => ({
|
||||||
...s,
|
...s,
|
||||||
screenshotOneApiKey: e.target.value,
|
screenshotOneApiKey: e.target.value,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</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">
|
||||||
|
|||||||
48
frontend/src/components/ui/avatar.tsx
Normal file
48
frontend/src/components/ui/avatar.tsx
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
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 }
|
||||||
@ -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,
|
||||||
}
|
};
|
||||||
|
|||||||
203
frontend/src/components/ui/dropdown-menu.tsx
Normal file
203
frontend/src/components/ui/dropdown-menu.tsx
Normal file
@ -0,0 +1,203 @@
|
|||||||
|
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,
|
||||||
|
}
|
||||||
19
frontend/src/components/user-feedback/FeedbackCallNote.tsx
Normal file
19
frontend/src/components/user-feedback/FeedbackCallNote.tsx
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
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;
|
||||||
@ -24,7 +24,8 @@ 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"
|
||||||
|
|||||||
@ -8,5 +8,23 @@ 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;
|
||||||
|
|||||||
7
frontend/src/lib/analytics.ts
Normal file
7
frontend/src/lib/analytics.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export function addEvent(eventName: string, props = {}) {
|
||||||
|
try {
|
||||||
|
window.plausible(eventName, { props });
|
||||||
|
} catch (e) {
|
||||||
|
// silently fail in non-production environments
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -10,11 +10,26 @@ 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]: { name: string; inBeta: boolean };
|
[key in CodeGenerationModel]: {
|
||||||
|
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": { name: "Claude 3.5 Sonnet", inBeta: false },
|
"claude-3-5-sonnet-20240620": {
|
||||||
"gpt-4-turbo-2024-04-09": { name: "GPT-4 Turbo (deprecated)", inBeta: false },
|
name: "Claude 3.5 Sonnet",
|
||||||
gpt_4_vision: { name: "GPT-4 Vision (deprecated)", inBeta: false },
|
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,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,19 +1,37 @@
|
|||||||
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>
|
||||||
<Router>
|
<ClerkProvider
|
||||||
<Routes>
|
publishableKey={CLERK_PUBLISHABLE_KEY}
|
||||||
<Route path="/" element={<App />} />
|
localization={{
|
||||||
<Route path="/evals" element={<EvalsPage />} />
|
footerPageLink__privacy:
|
||||||
</Routes>
|
"By signing up, you accept our terms of service and consent to receiving occasional product updates via email.",
|
||||||
</Router>
|
}}
|
||||||
<Toaster toastOptions={{ className: "dark:bg-zinc-950 dark:text-white" }} />
|
>
|
||||||
|
<Router>
|
||||||
|
<Routes>
|
||||||
|
<Route path="/" element={<AppContainer />} />
|
||||||
|
<Route path="/evals" element={<EvalsPage />} />
|
||||||
|
<Route path="/pricing" element={<PricingPage />} />
|
||||||
|
<Route path="/faqs" element={<FaqsPage />} />
|
||||||
|
<Route path="/checkout-success" element={<CheckoutSuccessPage />} />
|
||||||
|
</Routes>
|
||||||
|
</Router>
|
||||||
|
<Toaster
|
||||||
|
toastOptions={{ className: "dark:bg-zinc-950 dark:text-white" }}
|
||||||
|
/>
|
||||||
|
</ClerkProvider>
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
);
|
);
|
||||||
|
|||||||
19
frontend/src/plausible.d.ts
vendored
Normal file
19
frontend/src/plausible.d.ts
vendored
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
// 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 {};
|
||||||
@ -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";
|
inputMode: "image" | "video" | "text";
|
||||||
setInputMode: (mode: "image" | "video") => void;
|
setInputMode: (mode: "image" | "video" | "text") => void;
|
||||||
isImportedFromCode: boolean;
|
isImportedFromCode: boolean;
|
||||||
setIsImportedFromCode: (imported: boolean) => void;
|
setIsImportedFromCode: (imported: boolean) => void;
|
||||||
referenceImages: string[];
|
referenceImages: string[];
|
||||||
|
|||||||
16
frontend/src/store/store.ts
Normal file
16
frontend/src/store/store.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
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 })),
|
||||||
|
}));
|
||||||
@ -33,11 +33,12 @@ export enum ScreenRecorderState {
|
|||||||
|
|
||||||
export interface CodeGenerationParams {
|
export interface CodeGenerationParams {
|
||||||
generationType: "create" | "update";
|
generationType: "create" | "update";
|
||||||
inputMode: "image" | "video";
|
inputMode: "image" | "video" | "text";
|
||||||
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;
|
||||||
|
|||||||
@ -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.js"></script>'
|
? '<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>'
|
||||||
: "",
|
: "",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@ -493,6 +493,33 @@
|
|||||||
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"
|
||||||
@ -894,6 +921,11 @@
|
|||||||
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"
|
||||||
@ -1308,6 +1340,17 @@
|
|||||||
"@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"
|
||||||
@ -1403,6 +1446,20 @@
|
|||||||
"@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"
|
||||||
@ -1457,6 +1514,31 @@
|
|||||||
"@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"
|
||||||
@ -1795,6 +1877,18 @@
|
|||||||
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"
|
||||||
@ -1858,6 +1952,11 @@
|
|||||||
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"
|
||||||
@ -2599,6 +2698,11 @@ 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"
|
||||||
@ -2772,6 +2876,11 @@ 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"
|
||||||
@ -2789,6 +2898,13 @@ 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"
|
||||||
@ -3237,9 +3353,9 @@ extract-zip@2.0.1:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
"@types/yauzl" "^2.9.1"
|
"@types/yauzl" "^2.9.1"
|
||||||
|
|
||||||
fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
|
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.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz"
|
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
|
||||||
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:
|
||||||
@ -3289,6 +3405,11 @@ 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"
|
||||||
@ -3460,6 +3581,11 @@ 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"
|
||||||
@ -4153,6 +4279,11 @@ 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"
|
||||||
@ -4257,6 +4388,11 @@ 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"
|
||||||
@ -4422,6 +4558,11 @@ 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"
|
||||||
@ -4804,6 +4945,19 @@ 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"
|
||||||
@ -4831,7 +4985,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==
|
||||||
@ -5016,6 +5170,24 @@ 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"
|
||||||
@ -5186,6 +5358,11 @@ 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"
|
||||||
@ -5271,6 +5448,11 @@ 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"
|
||||||
@ -5376,6 +5558,14 @@ 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"
|
||||||
@ -5557,6 +5747,11 @@ 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"
|
||||||
@ -5652,6 +5847,11 @@ 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"
|
||||||
@ -5943,6 +6143,15 @@ 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"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user