Compare commits
231 Commits
main
...
hosted-mul
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
@ -7,19 +7,19 @@ repos:
|
||||
- id: end-of-file-fixer
|
||||
- id: check-yaml
|
||||
- id: check-added-large-files
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: poetry-pytest
|
||||
name: Run pytest with Poetry
|
||||
entry: poetry run --directory backend pytest
|
||||
language: system
|
||||
pass_filenames: false
|
||||
always_run: true
|
||||
files: ^backend/
|
||||
# - id: poetry-pyright
|
||||
# name: Run pyright with Poetry
|
||||
# entry: poetry run --directory backend pyright
|
||||
# language: system
|
||||
# pass_filenames: false
|
||||
# always_run: true
|
||||
# files: ^backend/
|
||||
# - repo: local
|
||||
# hooks:
|
||||
# - id: poetry-pytest
|
||||
# name: Run pytest with Poetry
|
||||
# entry: poetry run --directory backend pytest
|
||||
# language: system
|
||||
# pass_filenames: false
|
||||
# always_run: true
|
||||
# files: ^backend/
|
||||
# - id: poetry-pyright
|
||||
# name: Run pyright with Poetry
|
||||
# entry: poetry run --directory backend pyright
|
||||
# language: system
|
||||
# pass_filenames: false
|
||||
# always_run: true
|
||||
# files: ^backend/
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import re
|
||||
|
||||
import sentry_sdk
|
||||
|
||||
|
||||
def extract_html_content(text: str):
|
||||
# Use regex to find content within <html> tags and include the tags themselves
|
||||
@ -11,4 +13,8 @@ def extract_html_content(text: str):
|
||||
print(
|
||||
"[HTML Extraction] No <html> tags found in the generated content: " + text
|
||||
)
|
||||
try:
|
||||
raise Exception("No <html> tags found in the generated content")
|
||||
except:
|
||||
sentry_sdk.capture_exception()
|
||||
return text
|
||||
|
||||
@ -22,3 +22,11 @@ DEBUG_DIR = os.environ.get("DEBUG_DIR", "")
|
||||
# Set to True when running in production (on the hosted version)
|
||||
# Used as a feature flag to enable or disable certain features
|
||||
IS_PROD = os.environ.get("IS_PROD", False)
|
||||
|
||||
# Hosted version only
|
||||
|
||||
PLATFORM_OPENAI_API_KEY = os.environ.get("PLATFORM_OPENAI_API_KEY", "")
|
||||
PLATFORM_ANTHROPIC_API_KEY = os.environ.get("PLATFORM_ANTHROPIC_API_KEY", "")
|
||||
PLATFORM_SCREENSHOTONE_API_KEY = os.environ.get("PLATFORM_SCREENSHOTONE_API_KEY", "")
|
||||
|
||||
BACKEND_SAAS_URL = os.environ.get("BACKEND_SAAS_URL", "")
|
||||
|
||||
@ -4,4 +4,5 @@ from typing import Literal
|
||||
InputMode = Literal[
|
||||
"image",
|
||||
"video",
|
||||
"text",
|
||||
]
|
||||
|
||||
@ -3,6 +3,7 @@ import re
|
||||
from typing import Dict, List, Literal, Union
|
||||
from openai import AsyncOpenAI
|
||||
from bs4 import BeautifulSoup
|
||||
import sentry_sdk
|
||||
|
||||
from image_generation.replicate import call_replicate
|
||||
|
||||
@ -29,6 +30,10 @@ async def process_tasks(
|
||||
for result in results:
|
||||
if isinstance(result, BaseException):
|
||||
print(f"An exception occurred: {result}")
|
||||
try:
|
||||
raise result
|
||||
except Exception:
|
||||
sentry_sdk.capture_exception()
|
||||
processed_results.append(None)
|
||||
else:
|
||||
processed_results.append(result)
|
||||
|
||||
@ -4,6 +4,7 @@ from typing import Any, Awaitable, Callable, List, cast
|
||||
from anthropic import AsyncAnthropic
|
||||
from openai import AsyncOpenAI
|
||||
from openai.types.chat import ChatCompletionMessageParam, ChatCompletionChunk
|
||||
import sentry_sdk
|
||||
from config import IS_DEBUG_ENABLED
|
||||
from debug.DebugFileWriter import DebugFileWriter
|
||||
from image_processing.utils import process_image
|
||||
@ -12,6 +13,7 @@ from utils import pprint_prompt
|
||||
|
||||
|
||||
# Actual model versions that are passed to the LLMs and stored in our logs
|
||||
# Keep in sync with s2c-saas repo & DB column `llm_version`
|
||||
class Llm(Enum):
|
||||
GPT_4_VISION = "gpt-4-vision-preview"
|
||||
GPT_4_TURBO_2024_04_09 = "gpt-4-turbo-2024-04-09"
|
||||
@ -62,6 +64,25 @@ async def stream_openai_response(
|
||||
full_response = ""
|
||||
async for chunk in stream: # type: ignore
|
||||
assert isinstance(chunk, ChatCompletionChunk)
|
||||
|
||||
# Log finish reason for OpenAI but don't halt streaming if it fails
|
||||
try:
|
||||
# Print finish reason if it exists
|
||||
if (
|
||||
chunk.choices
|
||||
and len(chunk.choices) > 0
|
||||
and chunk.choices[0].finish_reason
|
||||
):
|
||||
finish_reason = chunk.choices[0].finish_reason
|
||||
print("[STOP REASON] OpenAI " + finish_reason)
|
||||
if finish_reason == "length":
|
||||
try:
|
||||
raise Exception("OpenAI response too long")
|
||||
except Exception as e:
|
||||
sentry_sdk.capture_exception()
|
||||
except Exception as e:
|
||||
sentry_sdk.capture_exception(e)
|
||||
|
||||
if (
|
||||
chunk.choices
|
||||
and len(chunk.choices) > 0
|
||||
@ -138,6 +159,14 @@ async def stream_claude_response(
|
||||
# Return final message
|
||||
response = await stream.get_final_message()
|
||||
|
||||
# Log stop reason
|
||||
print("[STOP REASON] " + str(response.stop_reason))
|
||||
if response.stop_reason == "max_tokens":
|
||||
try:
|
||||
raise Exception("Claude response too long")
|
||||
except Exception:
|
||||
sentry_sdk.capture_exception()
|
||||
|
||||
# Close the Anthropic client
|
||||
await client.close()
|
||||
|
||||
|
||||
@ -4,10 +4,27 @@ from dotenv import load_dotenv
|
||||
load_dotenv()
|
||||
|
||||
|
||||
import os
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from routes import screenshot, generate_code, home, evals
|
||||
from config import IS_PROD
|
||||
|
||||
# Setup Sentry (only relevant in prod)
|
||||
if IS_PROD:
|
||||
import sentry_sdk
|
||||
|
||||
SENTRY_DSN = os.environ.get("SENTRY_DSN")
|
||||
if not SENTRY_DSN:
|
||||
raise Exception("SENTRY_DSN not found in prod environment")
|
||||
|
||||
sentry_sdk.init(
|
||||
dsn=SENTRY_DSN,
|
||||
traces_sample_rate=0,
|
||||
profiles_sample_rate=0.1,
|
||||
)
|
||||
|
||||
# Setup FastAPI
|
||||
app = FastAPI(openapi_url=None, docs_url=None, redoc_url=None)
|
||||
|
||||
# Configure CORS settings
|
||||
|
||||
782
backend/poetry.lock
generated
782
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 prompts.imported_code_prompts import IMPORTED_CODE_SYSTEM_PROMPTS
|
||||
from prompts.screenshot_system_prompts import SYSTEM_PROMPTS
|
||||
from prompts.text_prompts import SYSTEM_PROMPTS as TEXT_SYSTEM_PROMPTS
|
||||
from prompts.types import Stack
|
||||
from video.utils import assemble_claude_prompt_video
|
||||
|
||||
@ -132,3 +133,22 @@ def assemble_prompt(
|
||||
"content": user_content,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def assemble_text_prompt(
|
||||
text_prompt: str,
|
||||
stack: Stack,
|
||||
) -> list[ChatCompletionMessageParam]:
|
||||
|
||||
system_content = TEXT_SYSTEM_PROMPTS[stack]
|
||||
|
||||
return [
|
||||
{
|
||||
"role": "system",
|
||||
"content": system_content,
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Generate UI for " + text_prompt,
|
||||
},
|
||||
]
|
||||
|
||||
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"
|
||||
anthropic = "^0.18.0"
|
||||
moviepy = "^1.0.3"
|
||||
sentry-sdk = {extras = ["fastapi"], version = "^1.38.0"}
|
||||
pillow = "^10.3.0"
|
||||
types-pillow = "^10.2.0.20240520"
|
||||
aiohttp = "^3.9.5"
|
||||
|
||||
@ -1,14 +1,15 @@
|
||||
import asyncio
|
||||
from dataclasses import dataclass
|
||||
import os
|
||||
from fastapi import APIRouter, WebSocket
|
||||
import openai
|
||||
from codegen.utils import extract_html_content
|
||||
from config import (
|
||||
ANTHROPIC_API_KEY,
|
||||
IS_PROD,
|
||||
NUM_VARIANTS,
|
||||
OPENAI_API_KEY,
|
||||
OPENAI_BASE_URL,
|
||||
PLATFORM_ANTHROPIC_API_KEY,
|
||||
PLATFORM_OPENAI_API_KEY,
|
||||
REPLICATE_API_KEY,
|
||||
SHOULD_MOCK_AI_RESPONSE,
|
||||
)
|
||||
@ -20,8 +21,11 @@ from llm import (
|
||||
stream_claude_response_native,
|
||||
stream_openai_response,
|
||||
)
|
||||
from fs_logging.core import write_logs
|
||||
from mock_llm import mock_completion
|
||||
from typing import Dict, List, cast, get_args
|
||||
from image_generation.core import generate_images
|
||||
from routes.logging_utils import PaymentMethod, send_to_saas_backend
|
||||
from routes.saas_utils import does_user_have_subscription_credits
|
||||
from typing import Any, Callable, Coroutine, Dict, List, Literal, cast, get_args
|
||||
from image_generation.core import generate_images
|
||||
from prompts import create_prompt
|
||||
@ -94,6 +98,7 @@ class ExtractedParams:
|
||||
openai_api_key: str | None
|
||||
anthropic_api_key: str | None
|
||||
openai_base_url: str | None
|
||||
payment_method: PaymentMethod
|
||||
|
||||
|
||||
async def extract_params(
|
||||
@ -123,14 +128,56 @@ async def extract_params(
|
||||
await throw_error(f"Invalid model: {code_generation_model_str}")
|
||||
raise ValueError(f"Invalid model: {code_generation_model_str}")
|
||||
|
||||
openai_api_key = get_from_settings_dialog_or_env(
|
||||
params, "openAiApiKey", OPENAI_API_KEY
|
||||
)
|
||||
# Read the auth token from the request (on the hosted version)
|
||||
auth_token = params.get("authToken")
|
||||
if not auth_token:
|
||||
await throw_error("You need to be logged in to use screenshot to code")
|
||||
raise Exception("No auth token")
|
||||
|
||||
# If neither is provided, we throw an error later only if Claude is used.
|
||||
anthropic_api_key = get_from_settings_dialog_or_env(
|
||||
params, "anthropicApiKey", ANTHROPIC_API_KEY
|
||||
)
|
||||
openai_api_key = None
|
||||
anthropic_api_key = None
|
||||
|
||||
# Track how this generation is being paid for
|
||||
payment_method: PaymentMethod = PaymentMethod.UNKNOWN
|
||||
|
||||
# If the user is a subscriber, use the platform API key
|
||||
# TODO: Rename does_user_have_subscription_credits
|
||||
res = await does_user_have_subscription_credits(auth_token)
|
||||
if res.status != "not_subscriber":
|
||||
if (
|
||||
res.status == "subscriber_has_credits"
|
||||
or res.status == "subscriber_is_trialing"
|
||||
):
|
||||
payment_method = (
|
||||
PaymentMethod.SUBSCRIPTION
|
||||
if res.status == "subscriber_has_credits"
|
||||
else PaymentMethod.TRIAL
|
||||
)
|
||||
openai_api_key = PLATFORM_OPENAI_API_KEY
|
||||
anthropic_api_key = PLATFORM_ANTHROPIC_API_KEY
|
||||
print("Subscription - using platform API key")
|
||||
elif res.status == "subscriber_has_no_credits":
|
||||
await throw_error(
|
||||
"Your subscription has run out of monthly credits. Contact support and we can add more credits to your account for free."
|
||||
)
|
||||
else:
|
||||
await throw_error("Unknown error occurred. Contact support.")
|
||||
raise Exception("Unknown error occurred when checking subscription credits")
|
||||
|
||||
# Use the user's OpenAI API key from the settings dialog if they are not a subscriber
|
||||
if not openai_api_key:
|
||||
openai_api_key = get_from_settings_dialog_or_env(params, "openAiApiKey", None)
|
||||
if openai_api_key:
|
||||
payment_method = PaymentMethod.OPENAI_API_KEY
|
||||
print("Using OpenAI API key from user's settings dialog")
|
||||
|
||||
print("Payment method: ", payment_method)
|
||||
|
||||
if payment_method is PaymentMethod.UNKNOWN:
|
||||
await throw_error(
|
||||
"Please subscribe to a paid plan to generate code. If you are a subscriber and seeing this error, please contact support."
|
||||
)
|
||||
raise Exception("No payment method found")
|
||||
|
||||
# Base URL for OpenAI API
|
||||
openai_base_url: str | None = None
|
||||
@ -153,6 +200,7 @@ async def extract_params(
|
||||
openai_api_key=openai_api_key,
|
||||
anthropic_api_key=anthropic_api_key,
|
||||
openai_base_url=openai_base_url,
|
||||
payment_method=payment_method,
|
||||
)
|
||||
|
||||
|
||||
@ -213,6 +261,7 @@ async def stream_code(websocket: WebSocket):
|
||||
openai_base_url = extracted_params.openai_base_url
|
||||
anthropic_api_key = extracted_params.anthropic_api_key
|
||||
should_generate_images = extracted_params.should_generate_images
|
||||
payment_method = extracted_params.payment_method
|
||||
|
||||
# Auto-upgrade usage of older models
|
||||
code_generation_model = auto_upgrade_model(code_generation_model)
|
||||
@ -249,6 +298,9 @@ async def stream_code(websocket: WebSocket):
|
||||
else:
|
||||
try:
|
||||
if input_mode == "video":
|
||||
if IS_PROD:
|
||||
raise Exception("Video mode is not supported in prod")
|
||||
|
||||
if not anthropic_api_key:
|
||||
await throw_error(
|
||||
"Video only works with Anthropic models. No Anthropic API key found. Please add the environment variable ANTHROPIC_API_KEY to backend/.env or in the settings dialog"
|
||||
@ -266,7 +318,6 @@ async def stream_code(websocket: WebSocket):
|
||||
)
|
||||
]
|
||||
else:
|
||||
|
||||
# Depending on the presence and absence of various keys,
|
||||
# we decide which models to run
|
||||
variant_models = []
|
||||
@ -282,7 +333,7 @@ async def stream_code(websocket: WebSocket):
|
||||
)
|
||||
raise Exception("No OpenAI or Anthropic key")
|
||||
|
||||
tasks: List[Coroutine[Any, Any, str]] = []
|
||||
tasks: list[Coroutine[Any, Any, str]] = []
|
||||
for index, model in enumerate(variant_models):
|
||||
if model == "openai":
|
||||
if openai_api_key is None:
|
||||
@ -356,10 +407,30 @@ async def stream_code(websocket: WebSocket):
|
||||
completions = [extract_html_content(completion) for completion in completions]
|
||||
|
||||
# Write the messages dict into a log so that we can debug later
|
||||
write_logs(prompt_messages, completions[0])
|
||||
# write_logs(prompt_messages, completion) # type: ignore
|
||||
|
||||
if IS_PROD:
|
||||
# Catch any errors from sending to SaaS backend and continue
|
||||
try:
|
||||
# TODO*
|
||||
# assert exact_llm_version is not None, "exact_llm_version is not set"
|
||||
await send_to_saas_backend(
|
||||
prompt_messages,
|
||||
# TODO*: Store both completions
|
||||
completions[0],
|
||||
payment_method=payment_method,
|
||||
# TODO*
|
||||
llm_version=Llm.GPT_4O_2024_05_13,
|
||||
stack=stack,
|
||||
is_imported_from_code=bool(params.get("isImportedFromCode", False)),
|
||||
includes_result_image=bool(params.get("resultImage", False)),
|
||||
input_mode=input_mode,
|
||||
auth_token=params["authToken"],
|
||||
)
|
||||
except Exception as e:
|
||||
print("Error sending to SaaS backend", e)
|
||||
|
||||
## Image Generation
|
||||
|
||||
for index, _ in enumerate(completions):
|
||||
await send_message("status", "Generating images...", index)
|
||||
|
||||
|
||||
56
backend/routes/logging_utils.py
Normal file
56
backend/routes/logging_utils.py
Normal file
@ -0,0 +1,56 @@
|
||||
from enum import Enum
|
||||
import httpx
|
||||
from openai.types.chat import ChatCompletionMessageParam
|
||||
from typing import List
|
||||
import json
|
||||
|
||||
from config import BACKEND_SAAS_URL, IS_PROD
|
||||
from custom_types import InputMode
|
||||
from llm import Llm
|
||||
from prompts.types import Stack
|
||||
|
||||
|
||||
class PaymentMethod(Enum):
|
||||
LEGACY = "legacy"
|
||||
UNKNOWN = "unknown"
|
||||
OPENAI_API_KEY = "openai_api_key"
|
||||
SUBSCRIPTION = "subscription"
|
||||
TRIAL = "trial"
|
||||
|
||||
|
||||
async def send_to_saas_backend(
|
||||
prompt_messages: List[ChatCompletionMessageParam],
|
||||
completion: str,
|
||||
payment_method: PaymentMethod,
|
||||
llm_version: Llm,
|
||||
stack: Stack,
|
||||
is_imported_from_code: bool,
|
||||
includes_result_image: bool,
|
||||
input_mode: InputMode,
|
||||
auth_token: str | None = None,
|
||||
):
|
||||
if IS_PROD:
|
||||
async with httpx.AsyncClient() as client:
|
||||
url = BACKEND_SAAS_URL + "/generations/store"
|
||||
|
||||
data = json.dumps(
|
||||
{
|
||||
"prompt": json.dumps(prompt_messages),
|
||||
"completion": completion,
|
||||
"payment_method": payment_method.value,
|
||||
"llm_version": llm_version.value,
|
||||
"stack": stack,
|
||||
"is_imported_from_code": is_imported_from_code,
|
||||
"includes_result_image": includes_result_image,
|
||||
"input_mode": input_mode,
|
||||
}
|
||||
)
|
||||
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {auth_token}", # Add the auth token to the headers
|
||||
}
|
||||
|
||||
response = await client.post(url, content=data, headers=headers)
|
||||
response_data = response.json()
|
||||
return response_data
|
||||
24
backend/routes/saas_utils.py
Normal file
24
backend/routes/saas_utils.py
Normal file
@ -0,0 +1,24 @@
|
||||
import httpx
|
||||
from pydantic import BaseModel
|
||||
|
||||
from config import BACKEND_SAAS_URL
|
||||
|
||||
|
||||
class SubscriptionCreditsResponse(BaseModel):
|
||||
status: str
|
||||
|
||||
|
||||
async def does_user_have_subscription_credits(
|
||||
auth_token: str,
|
||||
):
|
||||
async with httpx.AsyncClient() as client:
|
||||
url = BACKEND_SAAS_URL + "/credits/has_credits"
|
||||
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {auth_token}",
|
||||
}
|
||||
|
||||
response = await client.post(url, headers=headers, timeout=60)
|
||||
parsed_response = SubscriptionCreditsResponse.parse_obj(response.json())
|
||||
return parsed_response
|
||||
@ -2,6 +2,9 @@ import base64
|
||||
from fastapi import APIRouter
|
||||
from pydantic import BaseModel
|
||||
import httpx
|
||||
from config import PLATFORM_SCREENSHOTONE_API_KEY
|
||||
|
||||
from routes.saas_utils import does_user_have_subscription_credits
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@ -12,10 +15,31 @@ def bytes_to_data_url(image_bytes: bytes, mime_type: str) -> str:
|
||||
|
||||
|
||||
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:
|
||||
api_base_url = "https://api.screenshotone.com/take"
|
||||
|
||||
# Get auth token
|
||||
if not auth_token:
|
||||
raise Exception("No auth token with capture_screenshot")
|
||||
|
||||
# TODO: Clean up this code and send the users correct error messages
|
||||
# If API key is not passed in, only use the platform ScreenshotOne API key if the user is a subscriber
|
||||
if not api_key:
|
||||
res = await does_user_have_subscription_credits(auth_token)
|
||||
if res.status == "not_subscriber":
|
||||
raise Exception(
|
||||
"capture_screenshot - User is not subscriber and has no API key"
|
||||
)
|
||||
elif res.status == "subscriber_has_credits":
|
||||
api_key = PLATFORM_SCREENSHOTONE_API_KEY
|
||||
elif res.status == "subscriber_has_no_credits":
|
||||
raise Exception("capture_screenshot - User has no credits")
|
||||
else:
|
||||
raise Exception(
|
||||
"capture_screenshot - Unknown error occurred when checking subscription credits"
|
||||
)
|
||||
|
||||
params = {
|
||||
"access_key": api_key,
|
||||
"url": target_url,
|
||||
@ -44,7 +68,8 @@ async def capture_screenshot(
|
||||
|
||||
class ScreenshotRequest(BaseModel):
|
||||
url: str
|
||||
apiKey: str
|
||||
apiKey: str | None
|
||||
authToken: str
|
||||
|
||||
|
||||
class ScreenshotResponse(BaseModel):
|
||||
@ -56,9 +81,10 @@ async def app_screenshot(request: ScreenshotRequest):
|
||||
# Extract the URL from the request body
|
||||
url = request.url
|
||||
api_key = request.apiKey
|
||||
auth_token = request.authToken
|
||||
|
||||
# TODO: Add error handling
|
||||
image_bytes = await capture_screenshot(url, api_key=api_key)
|
||||
image_bytes = await capture_screenshot(url, api_key=api_key, auth_token=auth_token)
|
||||
|
||||
# Convert the image bytes to a data url
|
||||
data_url = bytes_to_data_url(image_bytes, "image/png")
|
||||
|
||||
@ -16,6 +16,21 @@
|
||||
<!-- Injected code for hosted version -->
|
||||
<%- injectHead %>
|
||||
|
||||
<!-- Google tag (gtag.js) -->
|
||||
<script
|
||||
async
|
||||
src="https://www.googletagmanager.com/gtag/js?id=AW-16649848443"
|
||||
></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag() {
|
||||
dataLayer.push(arguments);
|
||||
}
|
||||
gtag("js", new Date());
|
||||
|
||||
gtag("config", "AW-16649848443");
|
||||
</script>
|
||||
|
||||
<title>Screenshot to Code</title>
|
||||
|
||||
<!-- Open Graph Meta Tags -->
|
||||
|
||||
@ -13,12 +13,16 @@
|
||||
"test": "jest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@clerk/clerk-react": "5.4.2",
|
||||
"@codemirror/lang-html": "^6.4.6",
|
||||
"@intercom/messenger-js-sdk": "^0.0.11",
|
||||
"@radix-ui/react-accordion": "^1.1.2",
|
||||
"@radix-ui/react-alert-dialog": "^1.0.5",
|
||||
"@radix-ui/react-avatar": "^1.0.4",
|
||||
"@radix-ui/react-checkbox": "^1.0.4",
|
||||
"@radix-ui/react-collapsible": "^1.0.3",
|
||||
"@radix-ui/react-dialog": "^1.0.5",
|
||||
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
||||
"@radix-ui/react-hover-card": "^1.0.7",
|
||||
"@radix-ui/react-icons": "^1.3.0",
|
||||
"@radix-ui/react-label": "^2.0.2",
|
||||
@ -30,12 +34,15 @@
|
||||
"@radix-ui/react-slot": "^1.0.2",
|
||||
"@radix-ui/react-switch": "^1.0.3",
|
||||
"@radix-ui/react-tabs": "^1.0.4",
|
||||
"@stripe/stripe-js": "^2.2.2",
|
||||
"@types/gtag.js": "^0.0.20",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"classnames": "^2.3.2",
|
||||
"clsx": "^2.0.0",
|
||||
"codemirror": "^6.0.1",
|
||||
"copy-to-clipboard": "^3.3.3",
|
||||
"html2canvas": "^1.4.1",
|
||||
"posthog-js": "^1.128.1",
|
||||
"nanoid": "^5.0.7",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
@ -43,6 +50,8 @@
|
||||
"react-hot-toast": "^2.4.1",
|
||||
"react-icons": "^4.12.0",
|
||||
"react-router-dom": "^6.20.1",
|
||||
"react-tweet": "^3.2.0",
|
||||
"react-youtube": "^10.1.0",
|
||||
"tailwind-merge": "^2.0.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"thememirror": "^2.0.1",
|
||||
|
||||
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 { IS_FREE_TRIAL_ENABLED, IS_RUNNING_ON_CLOUD } from "./config";
|
||||
import SettingsDialog from "./components/settings/SettingsDialog";
|
||||
import { AppState, CodeGenerationParams, EditorTheme, Settings } from "./types";
|
||||
import { IS_RUNNING_ON_CLOUD } from "./config";
|
||||
import { PicoBadge } from "./components/messages/PicoBadge";
|
||||
import { OnboardingNote } from "./components/messages/OnboardingNote";
|
||||
import { usePersistedState } from "./hooks/usePersistedState";
|
||||
import TermsOfServiceDialog from "./components/TermsOfServiceDialog";
|
||||
import { USER_CLOSE_WEB_SOCKET_CODE } from "./constants";
|
||||
import { addEvent } from "./lib/analytics";
|
||||
import { extractHistory } from "./components/history/utils";
|
||||
import toast from "react-hot-toast";
|
||||
import { useAuth } from "@clerk/clerk-react";
|
||||
import { useStore } from "./store/store";
|
||||
import { Stack } from "./lib/stacks";
|
||||
import { CodeGenerationModel } from "./lib/models";
|
||||
import useBrowserTabIndicator from "./hooks/useBrowserTabIndicator";
|
||||
import TipLink from "./components/messages/TipLink";
|
||||
import { useAppStore } from "./store/app-store";
|
||||
import GenerateFromText from "./components/generate-from-text/GenerateFromText";
|
||||
import { useProjectStore } from "./store/project-store";
|
||||
import Sidebar from "./components/sidebar/Sidebar";
|
||||
import PreviewPane from "./components/preview/PreviewPane";
|
||||
import DeprecationMessage from "./components/messages/DeprecationMessage";
|
||||
import { GenerationSettings } from "./components/settings/GenerationSettings";
|
||||
import StartPane from "./components/start-pane/StartPane";
|
||||
import { takeScreenshot } from "./lib/takeScreenshot";
|
||||
import Sidebar from "./components/sidebar/Sidebar";
|
||||
import { Commit } from "./components/commits/types";
|
||||
import { createCommit } from "./components/commits/utils";
|
||||
|
||||
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 {
|
||||
// Inputs
|
||||
inputMode,
|
||||
@ -140,12 +155,21 @@ function App() {
|
||||
return;
|
||||
}
|
||||
|
||||
addEvent("Regenerate");
|
||||
|
||||
// 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
|
||||
const cancelCodeGeneration = () => {
|
||||
addEvent("Cancel");
|
||||
|
||||
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
|
||||
resetExecutionConsoles();
|
||||
|
||||
@ -178,7 +202,12 @@ function App() {
|
||||
setAppState(AppState.CODING);
|
||||
|
||||
// Merge settings with params
|
||||
const updatedParams = { ...params, ...settings };
|
||||
const authToken = await getToken();
|
||||
const updatedParams = {
|
||||
...params,
|
||||
...settings,
|
||||
authToken: authToken || undefined,
|
||||
};
|
||||
|
||||
const baseCommitObject = {
|
||||
variants: [{ code: "" }, { code: "" }],
|
||||
@ -233,7 +262,10 @@ function App() {
|
||||
}
|
||||
|
||||
// Initial version creation
|
||||
function doCreate(referenceImages: string[], inputMode: "image" | "video") {
|
||||
async function doCreate(
|
||||
referenceImages: string[],
|
||||
inputMode: "image" | "video"
|
||||
) {
|
||||
// Reset any existing state
|
||||
reset();
|
||||
|
||||
@ -243,6 +275,7 @@ function App() {
|
||||
|
||||
// Kick off the code generation
|
||||
if (referenceImages.length > 0) {
|
||||
addEvent("Create");
|
||||
doGenerateCode({
|
||||
generationType: "create",
|
||||
image: referenceImages[0],
|
||||
@ -251,6 +284,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
|
||||
async function doUpdate(
|
||||
updateInstruction: string,
|
||||
@ -272,6 +318,7 @@ function App() {
|
||||
try {
|
||||
historyTree = extractHistory(head, commits);
|
||||
} catch {
|
||||
addEvent("HistoryTreeFailed");
|
||||
toast.error(
|
||||
"Version history is invalid. This shouldn't happen. Please contact support or open a Github issue."
|
||||
);
|
||||
@ -345,7 +392,7 @@ function App() {
|
||||
{IS_RUNNING_ON_CLOUD && <PicoBadge />}
|
||||
{IS_RUNNING_ON_CLOUD && (
|
||||
<TermsOfServiceDialog
|
||||
open={!settings.isTermOfServiceAccepted}
|
||||
open={false}
|
||||
onOpenChange={handleTermDialogOpenChange}
|
||||
/>
|
||||
)}
|
||||
@ -370,7 +417,14 @@ function App() {
|
||||
{/* Show tip link until coding is complete */}
|
||||
{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 */}
|
||||
{(appState === AppState.CODING ||
|
||||
@ -386,6 +440,8 @@ function App() {
|
||||
</div>
|
||||
|
||||
<main className="py-2 lg:pl-96">
|
||||
{!!navbarComponent && navbarComponent}
|
||||
|
||||
{appState === AppState.INITIAL && (
|
||||
<StartPane
|
||||
doCreate={doCreate}
|
||||
|
||||
@ -7,6 +7,8 @@ import { URLS } from "../urls";
|
||||
import { Badge } from "./ui/badge";
|
||||
import ScreenRecorder from "./recording/ScreenRecorder";
|
||||
import { ScreenRecorderState } from "../types";
|
||||
import { IS_RUNNING_ON_CLOUD } from "../config";
|
||||
import { addEvent } from "../lib/analytics";
|
||||
|
||||
const baseStyle = {
|
||||
flex: 1,
|
||||
@ -82,6 +84,17 @@ function ImageUpload({ setReferenceImages }: Props) {
|
||||
"video/webm": [".webm"],
|
||||
},
|
||||
onDrop: (acceptedFiles) => {
|
||||
if (IS_RUNNING_ON_CLOUD) {
|
||||
const isVideo = acceptedFiles.some((file) =>
|
||||
file.type.startsWith("video/")
|
||||
);
|
||||
if (isVideo) {
|
||||
toast.error("Videos are not yet supported on the hosted version.");
|
||||
addEvent("VideoUpload");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Set up the preview thumbnail images
|
||||
setFiles(
|
||||
acceptedFiles.map((file: File) =>
|
||||
@ -173,24 +186,29 @@ function ImageUpload({ setReferenceImages }: Props) {
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{screenRecorderState === ScreenRecorderState.INITIAL && (
|
||||
<div className="text-center text-sm text-slate-800 mt-4">
|
||||
<Badge>New!</Badge> Upload a screen recording (.mp4, .mov) or record
|
||||
your screen to clone a whole app (experimental).{" "}
|
||||
<a
|
||||
className="underline"
|
||||
href={URLS["intro-to-video"]}
|
||||
target="_blank"
|
||||
>
|
||||
Learn more.
|
||||
</a>
|
||||
</div>
|
||||
{/* Disable on prod for now */}
|
||||
{!IS_RUNNING_ON_CLOUD && (
|
||||
<>
|
||||
{screenRecorderState === ScreenRecorderState.INITIAL && (
|
||||
<div className="text-center text-sm text-slate-800 mt-4">
|
||||
<Badge>New!</Badge> Upload a screen recording (.mp4, .mov) or
|
||||
record your screen to clone a whole app (experimental).{" "}
|
||||
<a
|
||||
className="underline"
|
||||
href={URLS["intro-to-video"]}
|
||||
target="_blank"
|
||||
>
|
||||
Learn more.
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
<ScreenRecorder
|
||||
screenRecorderState={screenRecorderState}
|
||||
setScreenRecorderState={setScreenRecorderState}
|
||||
generateCode={setReferenceImages}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<ScreenRecorder
|
||||
screenRecorderState={screenRecorderState}
|
||||
setScreenRecorderState={setScreenRecorderState}
|
||||
generateCode={setReferenceImages}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@ -7,9 +7,7 @@ import {
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "./ui/alert-dialog";
|
||||
import { Input } from "./ui/input";
|
||||
import toast from "react-hot-toast";
|
||||
import { PICO_BACKEND_FORM_SECRET } from "../config";
|
||||
import { addEvent } from "../lib/analytics";
|
||||
|
||||
const LOGOS = ["microsoft", "amazon", "mit", "stanford", "bytedance", "baidu"];
|
||||
|
||||
@ -17,40 +15,19 @@ const TermsOfServiceDialog: React.FC<{
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}> = ({ open, onOpenChange }) => {
|
||||
const [email, setEmail] = React.useState("");
|
||||
|
||||
const onSubscribe = async () => {
|
||||
await fetch("https://backend.buildpicoapps.com/form", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ email, secret: PICO_BACKEND_FORM_SECRET }),
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<AlertDialog open={open} onOpenChange={onOpenChange}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle className="mb-2 text-xl">
|
||||
Enter your email to get started
|
||||
One last step
|
||||
</AlertDialogTitle>
|
||||
</AlertDialogHeader>
|
||||
|
||||
<div className="mb-2">
|
||||
<Input
|
||||
placeholder="Email"
|
||||
value={email}
|
||||
onChange={(e) => {
|
||||
setEmail(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col space-y-3 text-sm">
|
||||
<p>
|
||||
By providing your email, you consent to receiving occasional product
|
||||
updates, and you accept the{" "}
|
||||
You consent to receiving occasional product updates via email, and
|
||||
you accept the{" "}
|
||||
<a
|
||||
href="https://a.picoapps.xyz/camera-write"
|
||||
target="_blank"
|
||||
@ -76,13 +53,8 @@ const TermsOfServiceDialog: React.FC<{
|
||||
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogAction
|
||||
onClick={(e) => {
|
||||
if (!email.trim() || !email.trim().includes("@")) {
|
||||
e.preventDefault();
|
||||
toast.error("Please enter your email");
|
||||
} else {
|
||||
onSubscribe();
|
||||
}
|
||||
onClick={() => {
|
||||
addEvent("EmailSubmit");
|
||||
}}
|
||||
>
|
||||
Agree & Continue
|
||||
|
||||
@ -3,6 +3,8 @@ import { HTTP_BACKEND_URL } from "../config";
|
||||
import { Button } from "./ui/button";
|
||||
import { Input } from "./ui/input";
|
||||
import { toast } from "react-hot-toast";
|
||||
import { useStore } from "../store/store";
|
||||
import { useAuth } from "@clerk/clerk-react";
|
||||
|
||||
interface Props {
|
||||
screenshotOneApiKey: string | null;
|
||||
@ -13,28 +15,31 @@ export function UrlInputSection({ doCreate, screenshotOneApiKey }: Props) {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [referenceUrl, setReferenceUrl] = useState("");
|
||||
|
||||
// Hosted version only
|
||||
const subscriberTier = useStore((state) => state.subscriberTier);
|
||||
const { getToken } = useAuth();
|
||||
|
||||
async function takeScreenshot() {
|
||||
if (!screenshotOneApiKey) {
|
||||
toast.error(
|
||||
"Please add a ScreenshotOne API key in the Settings dialog. This is optional - you can also drag/drop and upload images directly.",
|
||||
{ duration: 8000 }
|
||||
);
|
||||
return;
|
||||
if (!referenceUrl) {
|
||||
return toast.error("Please enter a URL");
|
||||
}
|
||||
|
||||
if (!referenceUrl) {
|
||||
toast.error("Please enter a URL");
|
||||
return;
|
||||
if (!screenshotOneApiKey && subscriberTier === "free") {
|
||||
return toast.error(
|
||||
"Please upgrade to a paid plan to use the screenshot feature."
|
||||
);
|
||||
}
|
||||
|
||||
if (referenceUrl) {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const authToken = await getToken();
|
||||
const response = await fetch(`${HTTP_BACKEND_URL}/api/screenshot`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
url: referenceUrl,
|
||||
apiKey: screenshotOneApiKey,
|
||||
authToken,
|
||||
}),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
|
||||
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
|
||||
className="flex justify-between truncate flex-1 p-2"
|
||||
className="flex justify-between truncate flex-1 p-2 plausible-event-name=HistoryClick"
|
||||
onClick={() =>
|
||||
shouldDisableReverts
|
||||
? 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() {
|
||||
const setPricingDialogOpen = useStore((state) => state.setPricingDialogOpen);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col space-y-4 bg-green-700 p-2 rounded text-stone-200 text-sm">
|
||||
<span>
|
||||
To use Screenshot to Code,{" "}
|
||||
<a
|
||||
className="inline underline hover:opacity-70"
|
||||
href="https://buy.stripe.com/8wM6sre70gBW1nqaEE"
|
||||
className="inline underline hover:opacity-70 cursor-pointer"
|
||||
onClick={() => setPricingDialogOpen(true)}
|
||||
target="_blank"
|
||||
>
|
||||
buy some credits (100 generations for $36)
|
||||
</a>{" "}
|
||||
or use your own OpenAI API key with GPT4 vision access.{" "}
|
||||
<a
|
||||
href="https://github.com/abi/screenshot-to-code/blob/main/Troubleshooting.md"
|
||||
className="inline underline hover:opacity-70"
|
||||
target="_blank"
|
||||
>
|
||||
Follow these instructions to get yourself a key.
|
||||
</a>{" "}
|
||||
and paste it in the Settings dialog (gear icon above). Your key is only
|
||||
stored in your browser. Never stored on our servers.
|
||||
Subscribe to get started
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -1,7 +1,12 @@
|
||||
import PricingDialog from "../hosted/payments/PricingDialog";
|
||||
|
||||
export function PicoBadge() {
|
||||
return (
|
||||
<>
|
||||
<a
|
||||
<div>
|
||||
<PricingDialog />
|
||||
</div>
|
||||
{/* <a
|
||||
href="https://screenshot-to-code.canny.io/feature-requests"
|
||||
target="_blank"
|
||||
>
|
||||
@ -9,17 +14,9 @@ export function PicoBadge() {
|
||||
className="fixed z-50 bottom-16 right-5 rounded-md shadow bg-black
|
||||
text-white px-4 text-xs py-3 cursor-pointer"
|
||||
>
|
||||
feature requests?
|
||||
feedback
|
||||
</div>
|
||||
</a>
|
||||
<a href="https://picoapps.xyz?ref=screenshot-to-code" target="_blank">
|
||||
<div
|
||||
className="fixed z-50 bottom-5 right-5 rounded-md shadow text-black
|
||||
bg-white px-4 text-xs py-3 cursor-pointer"
|
||||
>
|
||||
an open source project by Pico
|
||||
</div>
|
||||
</a>
|
||||
</a> */}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -58,14 +58,14 @@ function CodeTab({ code, setCode, settings }: Props) {
|
||||
<div className="flex justify-start items-center px-4 mb-2">
|
||||
<span
|
||||
title="Copy Code"
|
||||
className="bg-black text-white flex items-center justify-center hover:text-black hover:bg-gray-100 cursor-pointer rounded-lg text-sm p-2.5"
|
||||
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}
|
||||
>
|
||||
Copy Code <FaCopy className="ml-2" />
|
||||
</span>
|
||||
<Button
|
||||
onClick={doOpenInCodepenio}
|
||||
className="bg-gray-100 text-black ml-2 py-2 px-4 border border-black rounded-md hover:bg-gray-400 focus:outline-none"
|
||||
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{" "}
|
||||
<img
|
||||
|
||||
@ -9,6 +9,7 @@ import {
|
||||
CODE_GENERATION_MODEL_DESCRIPTIONS,
|
||||
CodeGenerationModel,
|
||||
} from "../../lib/models";
|
||||
import { IS_RUNNING_ON_CLOUD } from "../../config";
|
||||
import { Badge } from "../ui/badge";
|
||||
|
||||
interface Props {
|
||||
@ -42,15 +43,18 @@ function ModelSettingsSection({
|
||||
<SelectGroup>
|
||||
{Object.values(CodeGenerationModel).map((model) => (
|
||||
<SelectItem key={model} value={model}>
|
||||
<div className="flex items-center">
|
||||
<span className="font-semibold">
|
||||
{CODE_GENERATION_MODEL_DESCRIPTIONS[model].name}
|
||||
</span>
|
||||
{CODE_GENERATION_MODEL_DESCRIPTIONS[model].inBeta && (
|
||||
<Badge className="ml-2" variant="secondary">
|
||||
Beta
|
||||
</Badge>
|
||||
)}
|
||||
<div className="flex flex-col">
|
||||
<div className="flex items-center">
|
||||
<span className="font-semibold">
|
||||
{CODE_GENERATION_MODEL_DESCRIPTIONS[model].name}
|
||||
</span>
|
||||
{!IS_RUNNING_ON_CLOUD &&
|
||||
CODE_GENERATION_MODEL_DESCRIPTIONS[model].inBeta && (
|
||||
<Badge className="ml-2" variant="secondary">
|
||||
Beta
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
|
||||
@ -8,6 +8,7 @@ import {
|
||||
} from "../ui/select";
|
||||
import { Badge } from "../ui/badge";
|
||||
import { Stack, STACK_DESCRIPTIONS } from "../../lib/stacks";
|
||||
import { addEvent } from "../../lib/analytics";
|
||||
|
||||
function generateDisplayComponent(stack: Stack) {
|
||||
const stackComponents = STACK_DESCRIPTIONS[stack].components;
|
||||
@ -43,7 +44,10 @@ function OutputSettingsSection({
|
||||
<span>{label}</span>
|
||||
<Select
|
||||
value={stack}
|
||||
onValueChange={(value: string) => setStack(value as Stack)}
|
||||
onValueChange={(value: string) => {
|
||||
addEvent("OutputSettings", { stack: value });
|
||||
setStack(value as Stack);
|
||||
}}
|
||||
disabled={shouldDisableUpdates}
|
||||
>
|
||||
<SelectTrigger className="col-span-2" id="output-settings-js">
|
||||
|
||||
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 DialogPrimitive from "@radix-ui/react-dialog"
|
||||
import { Cross2Icon } from "@radix-ui/react-icons"
|
||||
import * as React from "react";
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
||||
// import { Cross2Icon } from "@radix-ui/react-icons";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Dialog = DialogPrimitive.Root
|
||||
const Dialog = DialogPrimitive.Root;
|
||||
|
||||
const DialogTrigger = DialogPrimitive.Trigger
|
||||
const DialogTrigger = DialogPrimitive.Trigger;
|
||||
|
||||
const DialogPortal = DialogPrimitive.Portal
|
||||
const DialogPortal = DialogPrimitive.Portal;
|
||||
|
||||
const DialogClose = DialogPrimitive.Close
|
||||
const DialogClose = DialogPrimitive.Close;
|
||||
|
||||
const DialogOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
||||
@ -24,8 +24,8 @@ const DialogOverlay = React.forwardRef<
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
|
||||
));
|
||||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
|
||||
|
||||
const DialogContent = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||
@ -42,14 +42,14 @@ const DialogContent = React.forwardRef<
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
||||
{/* <DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
||||
<Cross2Icon className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
</DialogPrimitive.Close> */}
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
))
|
||||
DialogContent.displayName = DialogPrimitive.Content.displayName
|
||||
));
|
||||
DialogContent.displayName = DialogPrimitive.Content.displayName;
|
||||
|
||||
const DialogHeader = ({
|
||||
className,
|
||||
@ -62,8 +62,8 @@ const DialogHeader = ({
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
DialogHeader.displayName = "DialogHeader"
|
||||
);
|
||||
DialogHeader.displayName = "DialogHeader";
|
||||
|
||||
const DialogFooter = ({
|
||||
className,
|
||||
@ -76,8 +76,8 @@ const DialogFooter = ({
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
DialogFooter.displayName = "DialogFooter"
|
||||
);
|
||||
DialogFooter.displayName = "DialogFooter";
|
||||
|
||||
const DialogTitle = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Title>,
|
||||
@ -91,8 +91,8 @@ const DialogTitle = React.forwardRef<
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DialogTitle.displayName = DialogPrimitive.Title.displayName
|
||||
));
|
||||
DialogTitle.displayName = DialogPrimitive.Title.displayName;
|
||||
|
||||
const DialogDescription = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Description>,
|
||||
@ -103,8 +103,8 @@ const DialogDescription = React.forwardRef<
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DialogDescription.displayName = DialogPrimitive.Description.displayName
|
||||
));
|
||||
DialogDescription.displayName = DialogPrimitive.Description.displayName;
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
@ -117,4 +117,4 @@ export {
|
||||
DialogFooter,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
}
|
||||
};
|
||||
|
||||
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;
|
||||
@ -8,5 +8,23 @@ export const WS_BACKEND_URL =
|
||||
export const HTTP_BACKEND_URL =
|
||||
import.meta.env.VITE_HTTP_BACKEND_URL || "http://127.0.0.1:7001";
|
||||
|
||||
// Hosted version only
|
||||
|
||||
export const PICO_BACKEND_FORM_SECRET =
|
||||
import.meta.env.VITE_PICO_BACKEND_FORM_SECRET || null;
|
||||
|
||||
export const CLERK_PUBLISHABLE_KEY =
|
||||
import.meta.env.VITE_CLERK_PUBLISHABLE_KEY || null;
|
||||
|
||||
export const STRIPE_PUBLISHABLE_KEY =
|
||||
import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY || null;
|
||||
|
||||
export const SAAS_BACKEND_URL = import.meta.env.VITE_SAAS_BACKEND_URL || null;
|
||||
|
||||
// Feature flags
|
||||
export const SHOULD_SHOW_FEEDBACK_CALL_NOTE = false;
|
||||
export const IS_FREE_TRIAL_ENABLED = false;
|
||||
|
||||
// PostHog
|
||||
export const POSTHOG_KEY = import.meta.env.VITE_POSTHOG_KEY || null;
|
||||
export const POSTHOG_HOST = import.meta.env.VITE_POSTHOG_HOST || null;
|
||||
|
||||
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
|
||||
export const CODE_GENERATION_MODEL_DESCRIPTIONS: {
|
||||
[key in CodeGenerationModel]: { name: string; inBeta: boolean };
|
||||
[key in CodeGenerationModel]: {
|
||||
name: string;
|
||||
inBeta: boolean;
|
||||
};
|
||||
} = {
|
||||
"gpt-4o-2024-05-13": { name: "GPT-4o", inBeta: false },
|
||||
"claude-3-5-sonnet-20240620": { name: "Claude 3.5 Sonnet", inBeta: false },
|
||||
"gpt-4-turbo-2024-04-09": { name: "GPT-4 Turbo (deprecated)", inBeta: false },
|
||||
gpt_4_vision: { name: "GPT-4 Vision (deprecated)", inBeta: false },
|
||||
claude_3_sonnet: { name: "Claude 3 (deprecated)", inBeta: false },
|
||||
"claude-3-5-sonnet-20240620": {
|
||||
name: "Claude 3.5 Sonnet",
|
||||
inBeta: false,
|
||||
},
|
||||
"gpt-4-turbo-2024-04-09": {
|
||||
name: "GPT-4 Turbo (deprecated)",
|
||||
inBeta: false,
|
||||
},
|
||||
gpt_4_vision: {
|
||||
name: "GPT-4 Vision (deprecated)",
|
||||
inBeta: false,
|
||||
},
|
||||
claude_3_sonnet: {
|
||||
name: "Claude 3 (deprecated)",
|
||||
inBeta: false,
|
||||
},
|
||||
};
|
||||
|
||||
@ -1,19 +1,37 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import App from "./App.tsx";
|
||||
import "./index.css";
|
||||
import { Toaster } from "react-hot-toast";
|
||||
import AppContainer from "./components/hosted/AppContainer.tsx";
|
||||
import { ClerkProvider } from "@clerk/clerk-react";
|
||||
import EvalsPage from "./components/evals/EvalsPage.tsx";
|
||||
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
|
||||
import { CLERK_PUBLISHABLE_KEY } from "./config.ts";
|
||||
import "./index.css";
|
||||
import PricingPage from "./components/hosted/PricingPage.tsx";
|
||||
import CheckoutSuccessPage from "./components/hosted/CheckoutSuccessPage.tsx";
|
||||
import FaqsPage from "./components/hosted/FaqsPage.tsx";
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
<React.StrictMode>
|
||||
<Router>
|
||||
<Routes>
|
||||
<Route path="/" element={<App />} />
|
||||
<Route path="/evals" element={<EvalsPage />} />
|
||||
</Routes>
|
||||
</Router>
|
||||
<Toaster toastOptions={{ className: "dark:bg-zinc-950 dark:text-white" }} />
|
||||
<ClerkProvider
|
||||
publishableKey={CLERK_PUBLISHABLE_KEY}
|
||||
localization={{
|
||||
footerPageLink__privacy:
|
||||
"By signing up, you accept our terms of service and consent to receiving occasional product updates via email.",
|
||||
}}
|
||||
>
|
||||
<Router>
|
||||
<Routes>
|
||||
<Route path="/" element={<AppContainer />} />
|
||||
<Route path="/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>
|
||||
);
|
||||
|
||||
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
|
||||
interface ProjectStore {
|
||||
// Inputs
|
||||
inputMode: "image" | "video";
|
||||
setInputMode: (mode: "image" | "video") => void;
|
||||
inputMode: "image" | "video" | "text";
|
||||
setInputMode: (mode: "image" | "video" | "text") => void;
|
||||
isImportedFromCode: boolean;
|
||||
setIsImportedFromCode: (imported: boolean) => void;
|
||||
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 {
|
||||
generationType: "create" | "update";
|
||||
inputMode: "image" | "video";
|
||||
inputMode: "image" | "video" | "text";
|
||||
image: string;
|
||||
resultImage?: string;
|
||||
history?: string[];
|
||||
isImportedFromCode?: boolean;
|
||||
authToken?: string;
|
||||
}
|
||||
|
||||
export type FullGenerationSettings = CodeGenerationParams & Settings;
|
||||
|
||||
@ -16,7 +16,7 @@ export default ({ mode }) => {
|
||||
inject: {
|
||||
data: {
|
||||
injectHead: process.env.VITE_IS_DEPLOYED
|
||||
? '<script defer="" data-domain="screenshottocode.com" src="https://plausible.io/js/script.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"
|
||||
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
|
||||
|
||||
"@clerk/clerk-react@5.4.2":
|
||||
version "5.4.2"
|
||||
resolved "https://registry.yarnpkg.com/@clerk/clerk-react/-/clerk-react-5.4.2.tgz#866c23b83ef32cd27ff402c9e75ce8ab1149495a"
|
||||
integrity sha512-F6F9yZ2lZDv365M6rn4Cpct0V2ZxFMoSxxRGag1du9QP73+twL7YoEeIimp+j4/RxH5uSsGv9JL2I7a6jnktHw==
|
||||
dependencies:
|
||||
"@clerk/shared" "2.5.2"
|
||||
"@clerk/types" "4.14.0"
|
||||
tslib "2.4.1"
|
||||
|
||||
"@clerk/shared@2.5.2":
|
||||
version "2.5.2"
|
||||
resolved "https://registry.yarnpkg.com/@clerk/shared/-/shared-2.5.2.tgz#03aba401cdd4ce91eede0978bba7f3ffd771f16c"
|
||||
integrity sha512-3+I5vMhkn3wSqCuoxIIXRma3m8zpLJBH11MO2uxKOAeZJRyeALY0jGPeyptskl+Fl9j9Rtan0OWJIisMN8TiAA==
|
||||
dependencies:
|
||||
"@clerk/types" "4.14.0"
|
||||
glob-to-regexp "0.4.1"
|
||||
js-cookie "3.0.5"
|
||||
std-env "^3.7.0"
|
||||
swr "^2.2.0"
|
||||
|
||||
"@clerk/types@4.14.0":
|
||||
version "4.14.0"
|
||||
resolved "https://registry.yarnpkg.com/@clerk/types/-/types-4.14.0.tgz#2215d3a8337f984a401c5a819b793de622d223a5"
|
||||
integrity sha512-d3MUcWtXGTOS7QYCRVrOra7NYAGRiNWlb+Ke2KMSb+Z3lea6WlKeHa18uVnAkIVxBXtHlzdU2kwW0PrHkx8j9Q==
|
||||
dependencies:
|
||||
csstype "3.1.1"
|
||||
|
||||
"@codemirror/autocomplete@^6.0.0":
|
||||
version "6.11.0"
|
||||
resolved "https://registry.yarnpkg.com/@codemirror/autocomplete/-/autocomplete-6.11.0.tgz#406dee8bf5342dfb48920ad75454d3406ddf9963"
|
||||
@ -894,6 +921,11 @@
|
||||
resolved "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz"
|
||||
integrity sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==
|
||||
|
||||
"@intercom/messenger-js-sdk@^0.0.11":
|
||||
version "0.0.11"
|
||||
resolved "https://registry.yarnpkg.com/@intercom/messenger-js-sdk/-/messenger-js-sdk-0.0.11.tgz#ffdf37891826296d514a496e13a8d07e3d101c7e"
|
||||
integrity sha512-jBHXO2+cGoBHYQMPaLP8eUm4AREcTWXlfd9shlBLSyEkFuW8+So/ynUDlftvWYz81KvGohRWYauw6vLRH/AlfA==
|
||||
|
||||
"@istanbuljs/load-nyc-config@^1.0.0":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced"
|
||||
@ -1308,6 +1340,17 @@
|
||||
"@babel/runtime" "^7.13.10"
|
||||
"@radix-ui/react-primitive" "1.0.3"
|
||||
|
||||
"@radix-ui/react-avatar@^1.0.4":
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-avatar/-/react-avatar-1.0.4.tgz#de9a5349d9e3de7bbe990334c4d2011acbbb9623"
|
||||
integrity sha512-kVK2K7ZD3wwj3qhle0ElXhOjbezIgyl2hVvgwfIdexL3rN6zJmy5AqqIf+D31lxVppdzV8CjAfZ6PklkmInZLw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
"@radix-ui/react-context" "1.0.1"
|
||||
"@radix-ui/react-primitive" "1.0.3"
|
||||
"@radix-ui/react-use-callback-ref" "1.0.1"
|
||||
"@radix-ui/react-use-layout-effect" "1.0.1"
|
||||
|
||||
"@radix-ui/react-checkbox@^1.0.4":
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-checkbox/-/react-checkbox-1.0.4.tgz#98f22c38d5010dd6df4c5744cac74087e3275f4b"
|
||||
@ -1403,6 +1446,20 @@
|
||||
"@radix-ui/react-use-callback-ref" "1.0.1"
|
||||
"@radix-ui/react-use-escape-keydown" "1.0.3"
|
||||
|
||||
"@radix-ui/react-dropdown-menu@^2.0.6":
|
||||
version "2.0.6"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.0.6.tgz#cdf13c956c5e263afe4e5f3587b3071a25755b63"
|
||||
integrity sha512-i6TuFOoWmLWq+M/eCLGd/bQ2HfAX1RJgvrBQ6AQLmzfvsLdefxbWu8G9zczcPFfcSPehz9GcpF6K9QYreFV8hA==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
"@radix-ui/primitive" "1.0.1"
|
||||
"@radix-ui/react-compose-refs" "1.0.1"
|
||||
"@radix-ui/react-context" "1.0.1"
|
||||
"@radix-ui/react-id" "1.0.1"
|
||||
"@radix-ui/react-menu" "2.0.6"
|
||||
"@radix-ui/react-primitive" "1.0.3"
|
||||
"@radix-ui/react-use-controllable-state" "1.0.1"
|
||||
|
||||
"@radix-ui/react-focus-guards@1.0.1":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-guards/-/react-focus-guards-1.0.1.tgz#1ea7e32092216b946397866199d892f71f7f98ad"
|
||||
@ -1457,6 +1514,31 @@
|
||||
"@babel/runtime" "^7.13.10"
|
||||
"@radix-ui/react-primitive" "1.0.3"
|
||||
|
||||
"@radix-ui/react-menu@2.0.6":
|
||||
version "2.0.6"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-menu/-/react-menu-2.0.6.tgz#2c9e093c1a5d5daa87304b2a2f884e32288ae79e"
|
||||
integrity sha512-BVkFLS+bUC8HcImkRKPSiVumA1VPOOEC5WBMiT+QAVsPzW1FJzI9KnqgGxVDPBcql5xXrHkD3JOVoXWEXD8SYA==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
"@radix-ui/primitive" "1.0.1"
|
||||
"@radix-ui/react-collection" "1.0.3"
|
||||
"@radix-ui/react-compose-refs" "1.0.1"
|
||||
"@radix-ui/react-context" "1.0.1"
|
||||
"@radix-ui/react-direction" "1.0.1"
|
||||
"@radix-ui/react-dismissable-layer" "1.0.5"
|
||||
"@radix-ui/react-focus-guards" "1.0.1"
|
||||
"@radix-ui/react-focus-scope" "1.0.4"
|
||||
"@radix-ui/react-id" "1.0.1"
|
||||
"@radix-ui/react-popper" "1.1.3"
|
||||
"@radix-ui/react-portal" "1.0.4"
|
||||
"@radix-ui/react-presence" "1.0.1"
|
||||
"@radix-ui/react-primitive" "1.0.3"
|
||||
"@radix-ui/react-roving-focus" "1.0.4"
|
||||
"@radix-ui/react-slot" "1.0.2"
|
||||
"@radix-ui/react-use-callback-ref" "1.0.1"
|
||||
aria-hidden "^1.1.1"
|
||||
react-remove-scroll "2.5.5"
|
||||
|
||||
"@radix-ui/react-popover@^1.0.7":
|
||||
version "1.0.7"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-popover/-/react-popover-1.0.7.tgz#23eb7e3327330cb75ec7b4092d685398c1654e3c"
|
||||
@ -1795,6 +1877,18 @@
|
||||
dependencies:
|
||||
"@sinonjs/commons" "^3.0.0"
|
||||
|
||||
"@stripe/stripe-js@^2.2.2":
|
||||
version "2.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@stripe/stripe-js/-/stripe-js-2.4.0.tgz#7a7e5b187b9e9bb43073edd946ec3e9a778e61bd"
|
||||
integrity sha512-WFkQx1mbs2b5+7looI9IV1BLa3bIApuN3ehp9FP58xGg7KL9hCHDECgW3BwO9l9L+xBPVAD7Yjn1EhGe6EDTeA==
|
||||
|
||||
"@swc/helpers@^0.5.3":
|
||||
version "0.5.11"
|
||||
resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.5.11.tgz#5bab8c660a6e23c13b2d23fcd1ee44a2db1b0cb7"
|
||||
integrity sha512-YNlnKRWF2sVojTpIyzwou9XoTNbzbzONwRhOoniEioF1AtaitTvVZblaQRrAzChWQ1bLYyYSWzM18y4WwgzJ+A==
|
||||
dependencies:
|
||||
tslib "^2.4.0"
|
||||
|
||||
"@tootallnate/quickjs-emscripten@^0.23.0":
|
||||
version "0.23.0"
|
||||
resolved "https://registry.yarnpkg.com/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz#db4ecfd499a9765ab24002c3b696d02e6d32a12c"
|
||||
@ -1858,6 +1952,11 @@
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/gtag.js@^0.0.20":
|
||||
version "0.0.20"
|
||||
resolved "https://registry.yarnpkg.com/@types/gtag.js/-/gtag.js-0.0.20.tgz#e47edabb4ed5ecac90a079275958e6c929d7c08a"
|
||||
integrity sha512-wwAbk3SA2QeU67unN7zPxjEHmPmlXwZXZvQEpbEUQuMCRGgKyE1m6XDuTUA9b6pCGb/GqJmdfMOY5LuDjJSbbg==
|
||||
|
||||
"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1":
|
||||
version "2.0.6"
|
||||
resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz#7739c232a1fee9b4d3ce8985f314c0c6d33549d7"
|
||||
@ -2599,6 +2698,11 @@ clean-css@^5.2.2:
|
||||
dependencies:
|
||||
source-map "~0.6.0"
|
||||
|
||||
client-only@^0.0.1:
|
||||
version "0.0.1"
|
||||
resolved "https://registry.yarnpkg.com/client-only/-/client-only-0.0.1.tgz#38bba5d403c41ab150bff64a95c85013cf73bca1"
|
||||
integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==
|
||||
|
||||
cliui@^8.0.1:
|
||||
version "8.0.1"
|
||||
resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa"
|
||||
@ -2772,6 +2876,11 @@ cssesc@^3.0.0:
|
||||
resolved "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz"
|
||||
integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==
|
||||
|
||||
csstype@3.1.1:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.1.tgz#841b532c45c758ee546a11d5bd7b7b473c8c30b9"
|
||||
integrity sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==
|
||||
|
||||
csstype@^3.0.2:
|
||||
version "3.1.2"
|
||||
resolved "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz"
|
||||
@ -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:
|
||||
ms "2.1.2"
|
||||
|
||||
debug@^2.6.6:
|
||||
version "2.6.9"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
|
||||
integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
|
||||
dependencies:
|
||||
ms "2.0.0"
|
||||
|
||||
dedent@^1.0.0:
|
||||
version "1.5.3"
|
||||
resolved "https://registry.yarnpkg.com/dedent/-/dedent-1.5.3.tgz#99aee19eb9bae55a67327717b6e848d0bf777e5a"
|
||||
@ -3237,9 +3353,9 @@ extract-zip@2.0.1:
|
||||
optionalDependencies:
|
||||
"@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"
|
||||
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==
|
||||
|
||||
fast-fifo@^1.1.0, fast-fifo@^1.2.0:
|
||||
@ -3289,6 +3405,11 @@ fd-slicer@~1.1.0:
|
||||
dependencies:
|
||||
pend "~1.2.0"
|
||||
|
||||
fflate@^0.4.8:
|
||||
version "0.4.8"
|
||||
resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.4.8.tgz#f90b82aefbd8ac174213abb338bd7ef848f0f5ae"
|
||||
integrity sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==
|
||||
|
||||
file-entry-cache@^6.0.1:
|
||||
version "6.0.1"
|
||||
resolved "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz"
|
||||
@ -3460,6 +3581,11 @@ glob-parent@^6.0.2:
|
||||
dependencies:
|
||||
is-glob "^4.0.3"
|
||||
|
||||
glob-to-regexp@0.4.1:
|
||||
version "0.4.1"
|
||||
resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e"
|
||||
integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==
|
||||
|
||||
glob@7.1.6:
|
||||
version "7.1.6"
|
||||
resolved "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz"
|
||||
@ -4153,6 +4279,11 @@ jiti@^1.19.1:
|
||||
resolved "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz"
|
||||
integrity sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==
|
||||
|
||||
js-cookie@3.0.5:
|
||||
version "3.0.5"
|
||||
resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-3.0.5.tgz#0b7e2fd0c01552c58ba86e0841f94dc2557dcdbc"
|
||||
integrity sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==
|
||||
|
||||
"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz"
|
||||
@ -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"
|
||||
integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==
|
||||
|
||||
load-script@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/load-script/-/load-script-1.0.0.tgz#0491939e0bee5643ee494a7e3da3d2bac70c6ca4"
|
||||
integrity sha512-kPEjMFtZvwL9TaZo0uZ2ml+Ye9HUMmPwbYRJ324qF9tqMejwykJ5ggTyvzmrbBeapCAbk98BSbTeovHEEP1uCA==
|
||||
|
||||
local-pkg@^0.5.0:
|
||||
version "0.5.0"
|
||||
resolved "https://registry.yarnpkg.com/local-pkg/-/local-pkg-0.5.0.tgz#093d25a346bae59a99f80e75f6e9d36d7e8c925c"
|
||||
@ -4422,6 +4558,11 @@ mlly@^1.2.0, mlly@^1.4.2:
|
||||
pkg-types "^1.0.3"
|
||||
ufo "^1.3.0"
|
||||
|
||||
ms@2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
|
||||
integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==
|
||||
|
||||
ms@2.1.2:
|
||||
version "2.1.2"
|
||||
resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz"
|
||||
@ -4804,6 +4945,19 @@ postcss@^8.4.32:
|
||||
picocolors "^1.0.0"
|
||||
source-map-js "^1.0.2"
|
||||
|
||||
posthog-js@^1.128.1:
|
||||
version "1.136.2"
|
||||
resolved "https://registry.yarnpkg.com/posthog-js/-/posthog-js-1.136.2.tgz#a90bc908665f7bbc9b366e16a0a38e40aacef7dc"
|
||||
integrity sha512-9oTUB/JDayzV+hB4f7u+ZNUbfnkGHLxyZw+FOE59pCgmbWHcJxhpGbu2Xlyv027/iHIjQbn1mtm2wJmBI2BuqA==
|
||||
dependencies:
|
||||
fflate "^0.4.8"
|
||||
preact "^10.19.3"
|
||||
|
||||
preact@^10.19.3:
|
||||
version "10.22.0"
|
||||
resolved "https://registry.yarnpkg.com/preact/-/preact-10.22.0.tgz#a50f38006ae438d255e2631cbdaf7488e6dd4e16"
|
||||
integrity sha512-RRurnSjJPj4rp5K6XoP45Ui33ncb7e4H7WiOHVpjbkvqvA3U+N8Z6Qbo0AE6leGYBV66n8EhEaFixvIu3SkxFw==
|
||||
|
||||
prelude-ls@^1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz"
|
||||
@ -4831,7 +4985,7 @@ prompts@^2.0.1:
|
||||
kleur "^3.0.3"
|
||||
sisteransi "^1.0.5"
|
||||
|
||||
prop-types@^15.8.1:
|
||||
prop-types@15.8.1, prop-types@^15.8.1:
|
||||
version "15.8.1"
|
||||
resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz"
|
||||
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
|
||||
@ -5016,6 +5170,24 @@ react-style-singleton@^2.2.1:
|
||||
invariant "^2.2.4"
|
||||
tslib "^2.0.0"
|
||||
|
||||
react-tweet@^3.2.0:
|
||||
version "3.2.1"
|
||||
resolved "https://registry.yarnpkg.com/react-tweet/-/react-tweet-3.2.1.tgz#000d9bf2b2ce919fdec0e14241f05631e8917143"
|
||||
integrity sha512-dktP3RMuwRB4pnSDocKpSsW5Hq1IXRW6fONkHhxT5EBIXsKZzdQuI70qtub1XN2dtZdkJWWxfBm/Q+kN+vRYFA==
|
||||
dependencies:
|
||||
"@swc/helpers" "^0.5.3"
|
||||
clsx "^2.0.0"
|
||||
swr "^2.2.4"
|
||||
|
||||
react-youtube@^10.1.0:
|
||||
version "10.1.0"
|
||||
resolved "https://registry.yarnpkg.com/react-youtube/-/react-youtube-10.1.0.tgz#7e5670c764f12eb408166e8eb438d788dc64e8b5"
|
||||
integrity sha512-ZfGtcVpk0SSZtWCSTYOQKhfx5/1cfyEW1JN/mugGNfAxT3rmVJeMbGpA9+e78yG21ls5nc/5uZJETE3cm3knBg==
|
||||
dependencies:
|
||||
fast-deep-equal "3.1.3"
|
||||
prop-types "15.8.1"
|
||||
youtube-player "5.5.2"
|
||||
|
||||
react@^18.2.0:
|
||||
version "18.2.0"
|
||||
resolved "https://registry.npmjs.org/react/-/react-18.2.0.tgz"
|
||||
@ -5186,6 +5358,11 @@ signal-exit@^4.1.0:
|
||||
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04"
|
||||
integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==
|
||||
|
||||
sister@^3.0.0:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/sister/-/sister-3.0.2.tgz#bb3e39f07b1f75bbe1945f29a27ff1e5a2f26be4"
|
||||
integrity sha512-p19rtTs+NksBRKW9qn0UhZ8/TUI9BPw9lmtHny+Y3TinWlOa9jWh9xB0AtPSdmOy49NJJJSSe0Ey4C7h0TrcYA==
|
||||
|
||||
sisteransi@^1.0.5:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed"
|
||||
@ -5271,6 +5448,11 @@ std-env@^3.5.0:
|
||||
resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.6.0.tgz#94807562bddc68fa90f2e02c5fd5b6865bb4e98e"
|
||||
integrity sha512-aFZ19IgVmhdB2uX599ve2kE6BIE3YMnQ6Gp6BURhW/oIzpXGKr878TQfAQZn1+i0Flcc/UKUy1gOlcfaUBCryg==
|
||||
|
||||
std-env@^3.7.0:
|
||||
version "3.7.0"
|
||||
resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.7.0.tgz#c9f7386ced6ecf13360b6c6c55b8aaa4ef7481d2"
|
||||
integrity sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==
|
||||
|
||||
streamx@^2.13.0, streamx@^2.15.0:
|
||||
version "2.16.1"
|
||||
resolved "https://registry.yarnpkg.com/streamx/-/streamx-2.16.1.tgz#2b311bd34832f08aa6bb4d6a80297c9caef89614"
|
||||
@ -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"
|
||||
integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
|
||||
|
||||
swr@^2.2.0, swr@^2.2.4:
|
||||
version "2.2.5"
|
||||
resolved "https://registry.yarnpkg.com/swr/-/swr-2.2.5.tgz#063eea0e9939f947227d5ca760cc53696f46446b"
|
||||
integrity sha512-QtxqyclFeAsxEUeZIYmsaQ0UjimSq1RZ9Un7I68/0ClKK/U3LoyQunwkQfJZr2fc22DfIXLNDc2wFyTEikCUpg==
|
||||
dependencies:
|
||||
client-only "^0.0.1"
|
||||
use-sync-external-store "^1.2.0"
|
||||
|
||||
tailwind-merge@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/tailwind-merge/-/tailwind-merge-2.0.0.tgz#a0f3a8c874ebae5feec5595614d08245a5f88a39"
|
||||
@ -5557,6 +5747,11 @@ ts-jest@^29.1.2:
|
||||
semver "^7.5.3"
|
||||
yargs-parser "^21.0.1"
|
||||
|
||||
tslib@2.4.1:
|
||||
version "2.4.1"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.1.tgz#0d0bfbaac2880b91e22df0768e55be9753a5b17e"
|
||||
integrity sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==
|
||||
|
||||
tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.4.0:
|
||||
version "2.6.2"
|
||||
resolved "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz"
|
||||
@ -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"
|
||||
integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==
|
||||
|
||||
use-sync-external-store@^1.2.0:
|
||||
version "1.2.2"
|
||||
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz#c3b6390f3a30eba13200d2302dcdf1e7b57b2ef9"
|
||||
integrity sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==
|
||||
|
||||
util-deprecate@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz"
|
||||
@ -5943,6 +6143,15 @@ yocto-queue@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-1.0.0.tgz#7f816433fb2cbc511ec8bf7d263c3b58a1a3c251"
|
||||
integrity sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==
|
||||
|
||||
youtube-player@5.5.2:
|
||||
version "5.5.2"
|
||||
resolved "https://registry.yarnpkg.com/youtube-player/-/youtube-player-5.5.2.tgz#052b86b1eabe21ff331095ffffeae285fa7f7cb5"
|
||||
integrity sha512-ZGtsemSpXnDky2AUYWgxjaopgB+shFHgXVpiJFeNB5nWEugpW1KWYDaHKuLqh2b67r24GtP6HoSW5swvf0fFIQ==
|
||||
dependencies:
|
||||
debug "^2.6.6"
|
||||
load-script "^1.0.0"
|
||||
sister "^3.0.0"
|
||||
|
||||
zod@3.22.4:
|
||||
version "3.22.4"
|
||||
resolved "https://registry.yarnpkg.com/zod/-/zod-3.22.4.tgz#f31c3a9386f61b1f228af56faa9255e845cf3fff"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user