Merge branch 'main' into pr/122
This commit is contained in:
commit
45a64326f6
@ -1,4 +1,4 @@
|
|||||||
### Getting an OpenAI API key
|
### Getting an OpenAI API key with GPT4-Vision model access
|
||||||
|
|
||||||
You don't need a ChatGPT Pro account. Screenshot to code uses API keys from your OpenAI developer account. In order to get access to the GPT4 Vision model, log into your OpenAI account and then, follow these instructions:
|
You don't need a ChatGPT Pro account. Screenshot to code uses API keys from your OpenAI developer account. In order to get access to the GPT4 Vision model, log into your OpenAI account and then, follow these instructions:
|
||||||
|
|
||||||
@ -10,6 +10,7 @@ You don't need a ChatGPT Pro account. Screenshot to code uses API keys from your
|
|||||||
|
|
||||||
5. Go to Settings > Limits and check at the bottom of the page, your current tier has to be "Tier 1" to have GPT4 access
|
5. Go to Settings > Limits and check at the bottom of the page, your current tier has to be "Tier 1" to have GPT4 access
|
||||||
<img width="785" alt="285636973-da38bd4d-8a78-4904-8027-ca67d729b933" src="https://github.com/abi/screenshot-to-code/assets/23818/8d07cd84-0cf9-4f88-bc00-80eba492eadf">
|
<img width="785" alt="285636973-da38bd4d-8a78-4904-8027-ca67d729b933" src="https://github.com/abi/screenshot-to-code/assets/23818/8d07cd84-0cf9-4f88-bc00-80eba492eadf">
|
||||||
|
6. Go to Screenshot to code and paste it in the Settings dialog under OpenAI key (gear icon). Your key is only stored in your browser. Never stored on our servers.
|
||||||
|
|
||||||
Some users have also reported that it can take upto 30 minutes after your credit purchase for the GPT4 vision model to be activated.
|
Some users have also reported that it can take upto 30 minutes after your credit purchase for the GPT4 vision model to be activated.
|
||||||
|
|
||||||
|
|||||||
27
backend/access_token.py
Normal file
27
backend/access_token.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import json
|
||||||
|
import os
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
|
||||||
|
async def validate_access_token(access_code: str):
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
url = (
|
||||||
|
"https://backend.buildpicoapps.com/screenshot_to_code/validate_access_token"
|
||||||
|
)
|
||||||
|
data = json.dumps(
|
||||||
|
{
|
||||||
|
"access_code": access_code,
|
||||||
|
"secret": os.environ.get("PICO_BACKEND_SECRET"),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
headers = {"Content-Type": "application/json"}
|
||||||
|
|
||||||
|
response = await client.post(url, content=data, headers=headers)
|
||||||
|
response_data = response.json()
|
||||||
|
|
||||||
|
if response_data["success"]:
|
||||||
|
print("Access token is valid.")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print(f"Access token validation failed: {response_data['failure_reason']}")
|
||||||
|
return False
|
||||||
@ -5,8 +5,8 @@ from openai import AsyncOpenAI
|
|||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
|
|
||||||
|
|
||||||
async def process_tasks(prompts, api_key):
|
async def process_tasks(prompts, api_key, base_url):
|
||||||
tasks = [generate_image(prompt, api_key) for prompt in prompts]
|
tasks = [generate_image(prompt, api_key, base_url) for prompt in prompts]
|
||||||
results = await asyncio.gather(*tasks, return_exceptions=True)
|
results = await asyncio.gather(*tasks, return_exceptions=True)
|
||||||
|
|
||||||
processed_results = []
|
processed_results = []
|
||||||
@ -20,8 +20,8 @@ async def process_tasks(prompts, api_key):
|
|||||||
return processed_results
|
return processed_results
|
||||||
|
|
||||||
|
|
||||||
async def generate_image(prompt, api_key):
|
async def generate_image(prompt, api_key, base_url):
|
||||||
client = AsyncOpenAI(api_key=api_key)
|
client = AsyncOpenAI(api_key=api_key, base_url=base_url)
|
||||||
image_params = {
|
image_params = {
|
||||||
"model": "dall-e-3",
|
"model": "dall-e-3",
|
||||||
"quality": "standard",
|
"quality": "standard",
|
||||||
@ -60,7 +60,7 @@ def create_alt_url_mapping(code):
|
|||||||
return mapping
|
return mapping
|
||||||
|
|
||||||
|
|
||||||
async def generate_images(code, api_key, image_cache):
|
async def generate_images(code, api_key, base_url, image_cache):
|
||||||
# Find all images
|
# Find all images
|
||||||
soup = BeautifulSoup(code, "html.parser")
|
soup = BeautifulSoup(code, "html.parser")
|
||||||
images = soup.find_all("img")
|
images = soup.find_all("img")
|
||||||
@ -87,7 +87,7 @@ async def generate_images(code, api_key, image_cache):
|
|||||||
return code
|
return code
|
||||||
|
|
||||||
# Generate images
|
# Generate images
|
||||||
results = await process_tasks(prompts, api_key)
|
results = await process_tasks(prompts, api_key, base_url)
|
||||||
|
|
||||||
# Create a dict mapping alt text to image URL
|
# Create a dict mapping alt text to image URL
|
||||||
mapped_image_urls = dict(zip(prompts, results))
|
mapped_image_urls = dict(zip(prompts, results))
|
||||||
|
|||||||
@ -6,9 +6,12 @@ MODEL_GPT_4_VISION = "gpt-4-vision-preview"
|
|||||||
|
|
||||||
|
|
||||||
async def stream_openai_response(
|
async def stream_openai_response(
|
||||||
messages, api_key: str, callback: Callable[[str], Awaitable[None]]
|
messages,
|
||||||
|
api_key: str,
|
||||||
|
base_url: str | None,
|
||||||
|
callback: Callable[[str], Awaitable[None]],
|
||||||
):
|
):
|
||||||
client = AsyncOpenAI(api_key=api_key)
|
client = AsyncOpenAI(api_key=api_key, base_url=base_url)
|
||||||
|
|
||||||
model = MODEL_GPT_4_VISION
|
model = MODEL_GPT_4_VISION
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
# Load environment variables first
|
# Load environment variables first
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
from pydantic import BaseModel
|
|
||||||
|
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
|
||||||
@ -16,6 +15,7 @@ from mock import mock_completion
|
|||||||
from image_generation import create_alt_url_mapping, generate_images
|
from image_generation import create_alt_url_mapping, generate_images
|
||||||
from prompts import assemble_prompt
|
from prompts import assemble_prompt
|
||||||
from routes import screenshot
|
from routes import screenshot
|
||||||
|
from access_token import validate_access_token
|
||||||
|
|
||||||
app = FastAPI(openapi_url=None, docs_url=None, redoc_url=None)
|
app = FastAPI(openapi_url=None, docs_url=None, redoc_url=None)
|
||||||
|
|
||||||
@ -81,13 +81,27 @@ async def stream_code(websocket: WebSocket):
|
|||||||
|
|
||||||
# Get the OpenAI API key from the request. Fall back to environment variable if not provided.
|
# Get the OpenAI API key from the request. Fall back to environment variable if not provided.
|
||||||
# If neither is provided, we throw an error.
|
# If neither is provided, we throw an error.
|
||||||
if params["openAiApiKey"]:
|
openai_api_key = None
|
||||||
openai_api_key = params["openAiApiKey"]
|
if "accessCode" in params and params["accessCode"]:
|
||||||
print("Using OpenAI API key from client-side settings dialog")
|
print("Access code - using platform API key")
|
||||||
|
if await validate_access_token(params["accessCode"]):
|
||||||
|
openai_api_key = os.environ.get("PLATFORM_OPENAI_API_KEY")
|
||||||
|
else:
|
||||||
|
await websocket.send_json(
|
||||||
|
{
|
||||||
|
"type": "error",
|
||||||
|
"value": "Invalid access code or you're out of credits. Please try again.",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return
|
||||||
else:
|
else:
|
||||||
openai_api_key = os.environ.get("OPENAI_API_KEY")
|
if params["openAiApiKey"]:
|
||||||
if openai_api_key:
|
openai_api_key = params["openAiApiKey"]
|
||||||
print("Using OpenAI API key from environment variable")
|
print("Using OpenAI API key from client-side settings dialog")
|
||||||
|
else:
|
||||||
|
openai_api_key = os.environ.get("OPENAI_API_KEY")
|
||||||
|
if openai_api_key:
|
||||||
|
print("Using OpenAI API key from environment variable")
|
||||||
|
|
||||||
if not openai_api_key:
|
if not openai_api_key:
|
||||||
print("OpenAI API key not found")
|
print("OpenAI API key not found")
|
||||||
@ -99,6 +113,22 @@ async def stream_code(websocket: WebSocket):
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Get the OpenAI Base URL from the request. Fall back to environment variable if not provided.
|
||||||
|
openai_base_url = None
|
||||||
|
# Disable user-specified OpenAI Base URL in prod
|
||||||
|
if not os.environ.get("IS_PROD"):
|
||||||
|
if "openAiBaseURL" in params and params["openAiBaseURL"]:
|
||||||
|
openai_base_url = params["openAiBaseURL"]
|
||||||
|
print("Using OpenAI Base URL from client-side settings dialog")
|
||||||
|
else:
|
||||||
|
openai_base_url = os.environ.get("OPENAI_BASE_URL")
|
||||||
|
if openai_base_url:
|
||||||
|
print("Using OpenAI Base URL from environment variable")
|
||||||
|
|
||||||
|
if not openai_base_url:
|
||||||
|
print("Using official OpenAI URL")
|
||||||
|
|
||||||
|
# Get the image generation flag from the request. Fall back to True if not provided.
|
||||||
should_generate_images = (
|
should_generate_images = (
|
||||||
params["isImageGenerationEnabled"]
|
params["isImageGenerationEnabled"]
|
||||||
if "isImageGenerationEnabled" in params
|
if "isImageGenerationEnabled" in params
|
||||||
@ -137,6 +167,7 @@ async def stream_code(websocket: WebSocket):
|
|||||||
completion = await stream_openai_response(
|
completion = await stream_openai_response(
|
||||||
prompt_messages,
|
prompt_messages,
|
||||||
api_key=openai_api_key,
|
api_key=openai_api_key,
|
||||||
|
base_url=openai_base_url,
|
||||||
callback=lambda x: process_chunk(x),
|
callback=lambda x: process_chunk(x),
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -149,7 +180,10 @@ async def stream_code(websocket: WebSocket):
|
|||||||
{"type": "status", "value": "Generating images..."}
|
{"type": "status", "value": "Generating images..."}
|
||||||
)
|
)
|
||||||
updated_html = await generate_images(
|
updated_html = await generate_images(
|
||||||
completion, api_key=openai_api_key, image_cache=image_cache
|
completion,
|
||||||
|
api_key=openai_api_key,
|
||||||
|
base_url=openai_base_url,
|
||||||
|
image_cache=image_cache,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
updated_html = completion
|
updated_html = completion
|
||||||
|
|||||||
@ -21,6 +21,34 @@
|
|||||||
<%- injectHead %>
|
<%- injectHead %>
|
||||||
|
|
||||||
<title>Screenshot to Code</title>
|
<title>Screenshot to Code</title>
|
||||||
|
|
||||||
|
<!-- Open Graph Meta Tags -->
|
||||||
|
<meta property="og:title" content="Screenshot to Code" />
|
||||||
|
<meta
|
||||||
|
property="og:description"
|
||||||
|
content="Convert any screenshot or design to clean code"
|
||||||
|
/>
|
||||||
|
<meta
|
||||||
|
property="og:image"
|
||||||
|
content="https://screenshottocode.com/brand/twitter-summary-card.png"
|
||||||
|
/>
|
||||||
|
<meta property="og:image:width" content="1200" />
|
||||||
|
<meta property="og:image:height" content="628" />
|
||||||
|
<meta property="og:url" content="https://screenshottocode.com" />
|
||||||
|
<meta property="og:type" content="website" />
|
||||||
|
<!-- Twitter Card tags -->
|
||||||
|
<meta name="twitter:card" content="summary_large_image" />
|
||||||
|
<meta name="twitter:site" content="@picoapps" />
|
||||||
|
<!-- Keep in sync with og:title, og:description and og:image -->
|
||||||
|
<meta name="twitter:title" content="Screenshot to Code" />
|
||||||
|
<meta
|
||||||
|
name="twitter:description"
|
||||||
|
content="Convert any screenshot or design to clean code"
|
||||||
|
/>
|
||||||
|
<meta
|
||||||
|
name="twitter:image"
|
||||||
|
content="https://screenshottocode.com/brand/twitter-summary-card.png"
|
||||||
|
/>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@ -19,6 +19,7 @@
|
|||||||
"@radix-ui/react-dialog": "^1.0.5",
|
"@radix-ui/react-dialog": "^1.0.5",
|
||||||
"@radix-ui/react-icons": "^1.3.0",
|
"@radix-ui/react-icons": "^1.3.0",
|
||||||
"@radix-ui/react-label": "^2.0.2",
|
"@radix-ui/react-label": "^2.0.2",
|
||||||
|
"@radix-ui/react-popover": "^1.0.7",
|
||||||
"@radix-ui/react-select": "^2.0.0",
|
"@radix-ui/react-select": "^2.0.0",
|
||||||
"@radix-ui/react-separator": "^1.0.3",
|
"@radix-ui/react-separator": "^1.0.3",
|
||||||
"@radix-ui/react-slot": "^1.0.2",
|
"@radix-ui/react-slot": "^1.0.2",
|
||||||
|
|||||||
BIN
frontend/public/brand/twitter-summary-card.png
Normal file
BIN
frontend/public/brand/twitter-summary-card.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 330 KiB |
@ -48,17 +48,19 @@ function App() {
|
|||||||
const [settings, setSettings] = usePersistedState<Settings>(
|
const [settings, setSettings] = usePersistedState<Settings>(
|
||||||
{
|
{
|
||||||
openAiApiKey: null,
|
openAiApiKey: null,
|
||||||
|
openAiBaseURL: null,
|
||||||
screenshotOneApiKey: null,
|
screenshotOneApiKey: null,
|
||||||
isImageGenerationEnabled: true,
|
isImageGenerationEnabled: true,
|
||||||
editorTheme: EditorTheme.COBALT,
|
editorTheme: EditorTheme.COBALT,
|
||||||
isTermOfServiceAccepted: false,
|
isTermOfServiceAccepted: false,
|
||||||
|
accessCode: null,
|
||||||
},
|
},
|
||||||
"setting"
|
"setting"
|
||||||
);
|
);
|
||||||
const [outputSettings, setOutputSettings] = useState<OutputSettings>({
|
const [outputSettings, setOutputSettings] = useState<OutputSettings>({
|
||||||
css: CSSOption.TAILWIND,
|
css: CSSOption.TAILWIND,
|
||||||
js: JSFrameworkOption.VANILLA,
|
|
||||||
components: UIComponentOption.HTML,
|
components: UIComponentOption.HTML,
|
||||||
|
js: JSFrameworkOption.NO_FRAMEWORK,
|
||||||
});
|
});
|
||||||
const [shouldIncludeResultImage, setShouldIncludeResultImage] =
|
const [shouldIncludeResultImage, setShouldIncludeResultImage] =
|
||||||
useState<boolean>(false);
|
useState<boolean>(false);
|
||||||
@ -172,35 +174,33 @@ function App() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-2">
|
<div className="mt-2 dark:bg-black dark:text-white">
|
||||||
{IS_RUNNING_ON_CLOUD && <PicoBadge />}
|
{IS_RUNNING_ON_CLOUD && <PicoBadge settings={settings} />}
|
||||||
{IS_RUNNING_ON_CLOUD && (
|
{IS_RUNNING_ON_CLOUD && (
|
||||||
<TermsOfServiceDialog
|
<TermsOfServiceDialog
|
||||||
open={!settings.isTermOfServiceAccepted}
|
open={!settings.isTermOfServiceAccepted}
|
||||||
onOpenChange={handleTermDialogOpenChange}
|
onOpenChange={handleTermDialogOpenChange}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="lg:fixed lg:inset-y-0 lg:z-40 lg:flex lg:w-96 lg:flex-col">
|
<div className="lg:fixed lg:inset-y-0 lg:z-40 lg:flex lg:w-96 lg:flex-col">
|
||||||
<div className="flex grow flex-col gap-y-2 overflow-y-auto border-r border-gray-200 bg-white px-6">
|
<div className="flex grow flex-col gap-y-2 overflow-y-auto border-r border-gray-200 bg-white px-6 dark:bg-zinc-950 dark:text-white">
|
||||||
<div className="flex items-center justify-between mt-10">
|
<div className="flex items-center justify-between mt-10 mb-2">
|
||||||
<h1 className="text-2xl ">Screenshot to Code</h1>
|
<h1 className="text-2xl ">Screenshot to Code</h1>
|
||||||
<SettingsDialog settings={settings} setSettings={setSettings} />
|
<SettingsDialog settings={settings} setSettings={setSettings} />
|
||||||
</div>
|
</div>
|
||||||
{appState === AppState.INITIAL && (
|
|
||||||
<h2 className="text-sm text-gray-500 mb-2">
|
|
||||||
Drag & drop a screenshot to get started.
|
|
||||||
</h2>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{appState === AppState.INITIAL && (
|
<OutputSettingsSection
|
||||||
<OutputSettingsSection
|
outputSettings={outputSettings}
|
||||||
outputSettings={outputSettings}
|
setOutputSettings={setOutputSettings}
|
||||||
setOutputSettings={setOutputSettings}
|
shouldDisableUpdates={
|
||||||
/>
|
appState === AppState.CODING || appState === AppState.CODE_READY
|
||||||
)}
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
{IS_RUNNING_ON_CLOUD && !settings.openAiApiKey && <OnboardingNote />}
|
{IS_RUNNING_ON_CLOUD &&
|
||||||
|
!(settings.openAiApiKey || settings.accessCode) && (
|
||||||
|
<OnboardingNote />
|
||||||
|
)}
|
||||||
|
|
||||||
{(appState === AppState.CODING ||
|
{(appState === AppState.CODING ||
|
||||||
appState === AppState.CODE_READY) && (
|
appState === AppState.CODE_READY) && (
|
||||||
@ -213,7 +213,10 @@ function App() {
|
|||||||
{executionConsole.slice(-1)[0]}
|
{executionConsole.slice(-1)[0]}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex mt-4 w-full">
|
<div className="flex mt-4 w-full">
|
||||||
<Button onClick={stop} className="w-full">
|
<Button
|
||||||
|
onClick={stop}
|
||||||
|
className="w-full dark:text-white dark:bg-gray-700"
|
||||||
|
>
|
||||||
Stop
|
Stop
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@ -230,26 +233,32 @@ function App() {
|
|||||||
value={updateInstruction}
|
value={updateInstruction}
|
||||||
/>
|
/>
|
||||||
<div className="flex justify-between items-center gap-x-2">
|
<div className="flex justify-between items-center gap-x-2">
|
||||||
<div className="font-500 text-xs text-slate-700">
|
<div className="font-500 text-xs text-slate-700 dark:text-white">
|
||||||
Include screenshot of current version?
|
Include screenshot of current version?
|
||||||
</div>
|
</div>
|
||||||
<Switch
|
<Switch
|
||||||
checked={shouldIncludeResultImage}
|
checked={shouldIncludeResultImage}
|
||||||
onCheckedChange={setShouldIncludeResultImage}
|
onCheckedChange={setShouldIncludeResultImage}
|
||||||
|
className="dark:bg-gray-700"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Button onClick={doUpdate}>Update</Button>
|
<Button
|
||||||
|
onClick={doUpdate}
|
||||||
|
className="dark:text-white dark:bg-gray-700"
|
||||||
|
>
|
||||||
|
Update
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-x-2 mt-2">
|
<div className="flex items-center gap-x-2 mt-2">
|
||||||
<Button
|
<Button
|
||||||
onClick={downloadCode}
|
onClick={downloadCode}
|
||||||
className="flex items-center gap-x-2"
|
className="flex items-center gap-x-2 dark:text-white dark:bg-gray-700"
|
||||||
>
|
>
|
||||||
<FaDownload /> Download
|
<FaDownload /> Download
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={reset}
|
onClick={reset}
|
||||||
className="flex items-center gap-x-2"
|
className="flex items-center gap-x-2 dark:text-white dark:bg-gray-700"
|
||||||
>
|
>
|
||||||
<FaUndo />
|
<FaUndo />
|
||||||
Reset
|
Reset
|
||||||
|
|||||||
@ -2,8 +2,15 @@ export function OnboardingNote() {
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-col space-y-4 bg-green-700 p-2 rounded text-stone-200 text-sm">
|
<div className="flex flex-col space-y-4 bg-green-700 p-2 rounded text-stone-200 text-sm">
|
||||||
<span>
|
<span>
|
||||||
To use Screenshot to Code, you need an OpenAI API key with GPT4 vision
|
To use Screenshot to Code,{" "}
|
||||||
access.{" "}
|
<a
|
||||||
|
className="inline underline hover:opacity-70"
|
||||||
|
href="https://buy.stripe.com/8wM6sre70gBW1nqaEE"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
buy some credits (100 generations for $36)
|
||||||
|
</a>{" "}
|
||||||
|
or use your own OpenAI API key with GPT4 vision access.{" "}
|
||||||
<a
|
<a
|
||||||
href="https://github.com/abi/screenshot-to-code/blob/main/Troubleshooting.md"
|
href="https://github.com/abi/screenshot-to-code/blob/main/Troubleshooting.md"
|
||||||
className="inline underline hover:opacity-70"
|
className="inline underline hover:opacity-70"
|
||||||
@ -11,18 +18,8 @@ export function OnboardingNote() {
|
|||||||
>
|
>
|
||||||
Follow these instructions to get yourself a key.
|
Follow these instructions to get yourself a key.
|
||||||
</a>{" "}
|
</a>{" "}
|
||||||
Then, paste it in the Settings dialog (gear icon above).
|
and paste it in the Settings dialog (gear icon above). Your key is only
|
||||||
</span>
|
stored in your browser. Never stored on our servers.
|
||||||
<span>
|
|
||||||
Your key is only stored in your browser. Never stored on our servers. If
|
|
||||||
you prefer, you can also run this app completely locally.{" "}
|
|
||||||
<a
|
|
||||||
href="https://github.com/abi/screenshot-to-code"
|
|
||||||
className="inline underline hover:opacity-70"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
See the Github project for instructions.
|
|
||||||
</a>
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -5,16 +5,18 @@ import {
|
|||||||
SelectItem,
|
SelectItem,
|
||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
} from "./ui/select";
|
} from "./ui/select";
|
||||||
import { CSSOption, UIComponentOption, JSFrameworkOption, OutputSettings } from "../types";
|
|
||||||
import {
|
import {
|
||||||
Accordion,
|
CSSOption,
|
||||||
AccordionContent,
|
UIComponentOption,
|
||||||
AccordionItem,
|
JSFrameworkOption,
|
||||||
AccordionTrigger,
|
OutputSettings,
|
||||||
} from "./ui/accordion";
|
} from "../types";
|
||||||
import { capitalize } from "../lib/utils";
|
import { capitalize } from "../lib/utils";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
|
import { Label } from "@radix-ui/react-label";
|
||||||
|
import { Button } from "./ui/button";
|
||||||
|
import { Popover, PopoverTrigger, PopoverContent } from "./ui/popover";
|
||||||
|
|
||||||
function displayCSSOption(option: CSSOption) {
|
function displayCSSOption(option: CSSOption) {
|
||||||
switch (option) {
|
switch (option) {
|
||||||
@ -27,6 +29,17 @@ function displayCSSOption(option: CSSOption) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function displayJSOption(option: JSFrameworkOption) {
|
||||||
|
switch (option) {
|
||||||
|
case JSFrameworkOption.REACT:
|
||||||
|
return "React";
|
||||||
|
case JSFrameworkOption.NO_FRAMEWORK:
|
||||||
|
return "No Framework";
|
||||||
|
default:
|
||||||
|
return option;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function convertStringToCSSOption(option: string) {
|
function convertStringToCSSOption(option: string) {
|
||||||
switch (option) {
|
switch (option) {
|
||||||
case "tailwind":
|
case "tailwind":
|
||||||
@ -38,24 +51,63 @@ function convertStringToCSSOption(option: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function generateDisplayString(settings: OutputSettings) {
|
||||||
|
if (
|
||||||
|
settings.js === JSFrameworkOption.REACT &&
|
||||||
|
settings.css === CSSOption.TAILWIND
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<div className="text-gray-800 dark:text-white">
|
||||||
|
Generating <span className="font-bold">React</span> +{" "}
|
||||||
|
<span className="font-bold">Tailwind</span> code
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else if (
|
||||||
|
settings.js === JSFrameworkOption.NO_FRAMEWORK &&
|
||||||
|
settings.css === CSSOption.TAILWIND
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<div className="text-gray-800 dark:text-white">
|
||||||
|
Generating <span className="font-bold">HTML</span> +{" "}
|
||||||
|
<span className="font-bold">Tailwind</span> code
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else if (
|
||||||
|
settings.js === JSFrameworkOption.NO_FRAMEWORK &&
|
||||||
|
settings.css === CSSOption.BOOTSTRAP
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<div className="text-gray-800 dark:text-white">
|
||||||
|
Generating <span className="font-bold">HTML</span> +{" "}
|
||||||
|
<span className="font-bold">Bootstrap</span> code
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
outputSettings: OutputSettings;
|
outputSettings: OutputSettings;
|
||||||
setOutputSettings: React.Dispatch<React.SetStateAction<OutputSettings>>;
|
setOutputSettings: React.Dispatch<React.SetStateAction<OutputSettings>>;
|
||||||
|
shouldDisableUpdates?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function OutputSettingsSection({ outputSettings, setOutputSettings }: Props) {
|
function OutputSettingsSection({
|
||||||
|
outputSettings,
|
||||||
|
setOutputSettings,
|
||||||
|
shouldDisableUpdates = false,
|
||||||
|
}: Props) {
|
||||||
const onCSSValueChange = (value: string) => {
|
const onCSSValueChange = (value: string) => {
|
||||||
setOutputSettings((prev) => {
|
setOutputSettings((prev) => {
|
||||||
if (prev.js === JSFrameworkOption.REACT) {
|
if (prev.js === JSFrameworkOption.REACT) {
|
||||||
if (value !== CSSOption.TAILWIND) {
|
if (value !== CSSOption.TAILWIND) {
|
||||||
toast.error(
|
toast.error(
|
||||||
"React only supports Tailwind CSS. Change JS framework to Vanilla to use Bootstrap."
|
'React only supports Tailwind CSS. Change JS framework to "No Framework" to use Bootstrap.'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
css: CSSOption.TAILWIND,
|
css: CSSOption.TAILWIND,
|
||||||
js: JSFrameworkOption.REACT,
|
js: JSFrameworkOption.REACT,
|
||||||
components: UIComponentOption.HTML
|
components: UIComponentOption.HTML,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
@ -71,7 +123,7 @@ function OutputSettingsSection({ outputSettings, setOutputSettings }: Props) {
|
|||||||
setOutputSettings(() => ({
|
setOutputSettings(() => ({
|
||||||
css: CSSOption.TAILWIND,
|
css: CSSOption.TAILWIND,
|
||||||
js: value as JSFrameworkOption,
|
js: value as JSFrameworkOption,
|
||||||
components: UIComponentOption.HTML
|
components: UIComponentOption.HTML,
|
||||||
}));
|
}));
|
||||||
} else {
|
} else {
|
||||||
setOutputSettings((prev) => ({
|
setOutputSettings((prev) => ({
|
||||||
@ -85,8 +137,8 @@ function OutputSettingsSection({ outputSettings, setOutputSettings }: Props) {
|
|||||||
if (value === UIComponentOption.IONIC) {
|
if (value === UIComponentOption.IONIC) {
|
||||||
setOutputSettings(() => ({
|
setOutputSettings(() => ({
|
||||||
css: CSSOption.TAILWIND,
|
css: CSSOption.TAILWIND,
|
||||||
js: JSFrameworkOption.VANILLA,
|
js: JSFrameworkOption.NO_FRAMEWORK,
|
||||||
components: value as UIComponentOption
|
components: value as UIComponentOption,
|
||||||
}));
|
}));
|
||||||
} else {
|
} else {
|
||||||
setOutputSettings((prev) => ({
|
setOutputSettings((prev) => ({
|
||||||
@ -96,128 +148,182 @@ function OutputSettingsSection({ outputSettings, setOutputSettings }: Props) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const checkUIComponentOptionOrDefault = (
|
||||||
const checkUIComponentOptionOrDefault = (valueItem: UIComponentOption ) : UIComponentOption => {
|
valueItem: UIComponentOption
|
||||||
|
): UIComponentOption => {
|
||||||
switch (valueItem) {
|
switch (valueItem) {
|
||||||
case UIComponentOption.IONIC:
|
case UIComponentOption.IONIC:
|
||||||
if (outputSettings.js != JSFrameworkOption.VANILLA || outputSettings.css != CSSOption.TAILWIND) {
|
if (
|
||||||
return UIComponentOption.HTML
|
outputSettings.js != JSFrameworkOption.NO_FRAMEWORK ||
|
||||||
|
outputSettings.css != CSSOption.TAILWIND
|
||||||
|
) {
|
||||||
|
return UIComponentOption.HTML;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return valueItem;
|
return valueItem;
|
||||||
}
|
};
|
||||||
|
|
||||||
const checkCSSOptionOrDefault = (valueItem: CSSOption ) : CSSOption => {
|
const checkCSSOptionOrDefault = (valueItem: CSSOption): CSSOption => {
|
||||||
switch (valueItem) {
|
switch (valueItem) {
|
||||||
default:
|
default:
|
||||||
return valueItem;
|
return valueItem;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const checkJSFrameworkOptionOrDefault = (valueItem: JSFrameworkOption ) : JSFrameworkOption => {
|
const checkJSFrameworkOptionOrDefault = (
|
||||||
|
valueItem: JSFrameworkOption
|
||||||
|
): JSFrameworkOption => {
|
||||||
switch (valueItem) {
|
switch (valueItem) {
|
||||||
case JSFrameworkOption.REACT:
|
case JSFrameworkOption.REACT:
|
||||||
if(outputSettings.css != CSSOption.TAILWIND) {
|
if (outputSettings.css != CSSOption.TAILWIND) {
|
||||||
return JSFrameworkOption.VANILLA
|
return JSFrameworkOption.NO_FRAMEWORK;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
return valueItem;
|
return valueItem;
|
||||||
}
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
checkOutputSettingsOptions();
|
checkOutputSettingsOptions();
|
||||||
}, [outputSettings]);
|
}, [outputSettings]);
|
||||||
|
|
||||||
const checkOutputSettingsOptions = () => {
|
const checkOutputSettingsOptions = () => {
|
||||||
if ( isHiddenOption(outputSettings.css) || isHiddenOption(outputSettings.js) || isHiddenOption(outputSettings.components))
|
if (
|
||||||
{
|
isHiddenOption(outputSettings.css) ||
|
||||||
setOutputSettings((prev) => {
|
isHiddenOption(outputSettings.js) ||
|
||||||
return {
|
isHiddenOption(outputSettings.components)
|
||||||
css: checkCSSOptionOrDefault(prev.css),
|
) {
|
||||||
js: checkJSFrameworkOptionOrDefault(prev.js),
|
setOutputSettings((prev) => {
|
||||||
components: checkUIComponentOptionOrDefault(prev.components),
|
return {
|
||||||
};
|
css: checkCSSOptionOrDefault(prev.css),
|
||||||
})
|
js: checkJSFrameworkOptionOrDefault(prev.js),
|
||||||
}
|
components: checkUIComponentOptionOrDefault(prev.components),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const isHiddenOption = ( option : CSSOption| JSFrameworkOption | UIComponentOption ) : boolean => {
|
const isHiddenOption = (
|
||||||
if (Object.values(CSSOption).includes(option as CSSOption)){
|
option: CSSOption | JSFrameworkOption | UIComponentOption
|
||||||
return checkCSSOptionOrDefault(option as CSSOption) != option
|
): boolean => {
|
||||||
|
if (Object.values(CSSOption).includes(option as CSSOption)) {
|
||||||
|
return checkCSSOptionOrDefault(option as CSSOption) != option;
|
||||||
}
|
}
|
||||||
if (Object.values(JSFrameworkOption).includes(option as JSFrameworkOption)){
|
if (
|
||||||
return checkJSFrameworkOptionOrDefault(option as JSFrameworkOption) != option
|
Object.values(JSFrameworkOption).includes(option as JSFrameworkOption)
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
checkJSFrameworkOptionOrDefault(option as JSFrameworkOption) != option
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (Object.values(UIComponentOption).includes(option as UIComponentOption)){
|
if (
|
||||||
return checkUIComponentOptionOrDefault(option as UIComponentOption) != option
|
Object.values(UIComponentOption).includes(option as UIComponentOption)
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
checkUIComponentOptionOrDefault(option as UIComponentOption) != option
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return true
|
return true;
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Accordion type="single" collapsible className="w-full">
|
<div className="flex flex-col gap-y-2 justify-between text-sm">
|
||||||
<AccordionItem value="item-1">
|
{generateDisplayString(outputSettings)}{" "}
|
||||||
<AccordionTrigger>
|
{!shouldDisableUpdates && (
|
||||||
<div className="flex gap-x-2">Output Settings </div>
|
<Popover>
|
||||||
</AccordionTrigger>
|
<PopoverTrigger asChild>
|
||||||
<AccordionContent className="gap-y-2 flex flex-col pt-2">
|
<Button variant="outline">Customize</Button>
|
||||||
<div className="flex justify-between items-center pr-2">
|
</PopoverTrigger>
|
||||||
<span className="text-sm">CSS</span>
|
<PopoverContent className="w-80 text-sm">
|
||||||
<Select value={outputSettings.css} onValueChange={onCSSValueChange}>
|
<div className="grid gap-4">
|
||||||
<SelectTrigger className="w-[180px]">
|
<div className="space-y-2">
|
||||||
{displayCSSOption(outputSettings.css)}
|
<h4 className="font-medium leading-none">Code Settings</h4>
|
||||||
</SelectTrigger>
|
<p className="text-muted-foreground">
|
||||||
<SelectContent>
|
Customize your code output
|
||||||
<SelectGroup>
|
</p>
|
||||||
<SelectItem value={CSSOption.TAILWIND}>Tailwind</SelectItem>
|
</div>
|
||||||
<SelectItem value={CSSOption.BOOTSTRAP}>Bootstrap</SelectItem>
|
<div className="grid gap-2">
|
||||||
</SelectGroup>
|
<div className="grid grid-cols-3 items-center gap-4">
|
||||||
</SelectContent>
|
<Label htmlFor="output-settings-js">JS</Label>
|
||||||
</Select>
|
<Select
|
||||||
</div>
|
value={outputSettings.js}
|
||||||
<div className="flex justify-between items-center pr-2">
|
onValueChange={onJsFrameworkChange}
|
||||||
<span className="text-sm">JS Framework</span>
|
>
|
||||||
<Select
|
<SelectTrigger
|
||||||
value={outputSettings.js}
|
className="col-span-2 h-8"
|
||||||
onValueChange={onJsFrameworkChange}
|
id="output-settings-js"
|
||||||
>
|
>
|
||||||
<SelectTrigger className="w-[180px]">
|
{displayJSOption(outputSettings.js)}
|
||||||
{capitalize(outputSettings.js)}
|
</SelectTrigger>
|
||||||
</SelectTrigger>
|
<SelectContent>
|
||||||
<SelectContent>
|
<SelectGroup>
|
||||||
<SelectGroup>
|
<SelectItem value={JSFrameworkOption.NO_FRAMEWORK}>
|
||||||
<SelectItem value={JSFrameworkOption.VANILLA}>
|
No Framework
|
||||||
Vanilla
|
</SelectItem>
|
||||||
</SelectItem>
|
<SelectItem value={JSFrameworkOption.REACT}>
|
||||||
<SelectItem value={JSFrameworkOption.REACT} disabled={isHiddenOption(JSFrameworkOption.REACT)}>React</SelectItem>
|
React
|
||||||
</SelectGroup>
|
</SelectItem>
|
||||||
</SelectContent>
|
</SelectGroup>
|
||||||
</Select>
|
</SelectContent>
|
||||||
</div>
|
</Select>
|
||||||
<div className="flex justify-between items-center pr-2">
|
</div>
|
||||||
<span className="text-sm">Component Library</span>
|
<div className="grid grid-cols-3 items-center gap-4">
|
||||||
<Select
|
<Label htmlFor="output-settings-css">CSS</Label>
|
||||||
value={outputSettings.components}
|
<Select
|
||||||
onValueChange={onUIComponentOptionChange}
|
value={outputSettings.css}
|
||||||
>
|
onValueChange={onCSSValueChange}
|
||||||
<SelectTrigger className="w-[180px]">
|
>
|
||||||
{capitalize(outputSettings.components)}
|
<SelectTrigger
|
||||||
</SelectTrigger>
|
className="col-span-2 h-8"
|
||||||
<SelectContent>
|
id="output-settings-css"
|
||||||
<SelectGroup>
|
>
|
||||||
<SelectItem value={UIComponentOption.HTML}>HTML</SelectItem>
|
{displayCSSOption(outputSettings.css)}
|
||||||
<SelectItem value={UIComponentOption.IONIC} disabled={isHiddenOption(UIComponentOption.IONIC)}>Ionic</SelectItem>
|
</SelectTrigger>
|
||||||
</SelectGroup>
|
<SelectContent>
|
||||||
</SelectContent>
|
<SelectGroup>
|
||||||
</Select>
|
<SelectItem value={CSSOption.TAILWIND}>
|
||||||
</div>
|
Tailwind
|
||||||
<div className="flex justify-between pr-2 mt-2">
|
</SelectItem>
|
||||||
<span className="text-sm text-gray-500">Output: {outputSettings.js} + {outputSettings.css} + {outputSettings.components}</span>
|
<SelectItem value={CSSOption.BOOTSTRAP}>
|
||||||
</div>
|
Bootstrap
|
||||||
</AccordionContent>
|
</SelectItem>
|
||||||
</AccordionItem>
|
</SelectGroup>
|
||||||
</Accordion>
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-3 items-center gap-4">
|
||||||
|
<Label htmlFor="output-settings-component">Components</Label>
|
||||||
|
<Select
|
||||||
|
value={outputSettings.components}
|
||||||
|
onValueChange={onUIComponentOptionChange}
|
||||||
|
>
|
||||||
|
<SelectTrigger
|
||||||
|
id="output-settings-component"
|
||||||
|
className="col-span-2 h-8"
|
||||||
|
>
|
||||||
|
{capitalize(outputSettings.components)}
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectGroup>
|
||||||
|
<SelectItem value={UIComponentOption.HTML}>
|
||||||
|
HTML
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem
|
||||||
|
value={UIComponentOption.IONIC}
|
||||||
|
disabled={isHiddenOption(UIComponentOption.IONIC)}
|
||||||
|
>
|
||||||
|
Ionic
|
||||||
|
</SelectItem>
|
||||||
|
</SelectGroup>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
export function PicoBadge() {
|
import { Settings } from "../types";
|
||||||
|
|
||||||
|
export function PicoBadge({ settings }: { settings: Settings }) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<a
|
<a
|
||||||
@ -12,14 +14,26 @@ export function PicoBadge() {
|
|||||||
feature requests?
|
feature requests?
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://picoapps.xyz?ref=screenshot-to-code" target="_blank">
|
{!settings.accessCode && (
|
||||||
<div
|
<a href="https://picoapps.xyz?ref=screenshot-to-code" target="_blank">
|
||||||
className="fixed z-50 bottom-5 right-5 rounded-md shadow text-black
|
<div
|
||||||
|
className="fixed z-50 bottom-5 right-5 rounded-md shadow text-black
|
||||||
bg-white px-4 text-xs py-3 cursor-pointer"
|
bg-white px-4 text-xs py-3 cursor-pointer"
|
||||||
>
|
>
|
||||||
an open source project by Pico
|
an open source project by Pico
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
|
)}
|
||||||
|
{settings.accessCode && (
|
||||||
|
<a href="mailto:support@picoapps.xyz" 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"
|
||||||
|
>
|
||||||
|
email support
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,6 +15,13 @@ import { Label } from "./ui/label";
|
|||||||
import { Input } from "./ui/input";
|
import { Input } from "./ui/input";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger } from "./ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger } from "./ui/select";
|
||||||
import { capitalize } from "../lib/utils";
|
import { capitalize } from "../lib/utils";
|
||||||
|
import { IS_RUNNING_ON_CLOUD } from "../config";
|
||||||
|
import {
|
||||||
|
Accordion,
|
||||||
|
AccordionContent,
|
||||||
|
AccordionItem,
|
||||||
|
AccordionTrigger,
|
||||||
|
} from "./ui/accordion";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
settings: Settings;
|
settings: Settings;
|
||||||
@ -38,6 +45,31 @@ function SettingsDialog({ settings, setSettings }: Props) {
|
|||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="mb-4">Settings</DialogTitle>
|
<DialogTitle className="mb-4">Settings</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
|
{/* Access code */}
|
||||||
|
{IS_RUNNING_ON_CLOUD && (
|
||||||
|
<div className="flex flex-col space-y-4 bg-slate-300 p-4 rounded dark:text-white dark:bg-slate-800">
|
||||||
|
<Label htmlFor="access-code">
|
||||||
|
<div>Access Code</div>
|
||||||
|
<div className="font-light mt-1 leading-relaxed">
|
||||||
|
Buy an access code.
|
||||||
|
</div>
|
||||||
|
</Label>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
id="access-code dark:border-gray-700 dark:bg-gray-800 dark:text-white"
|
||||||
|
placeholder="Enter your Screenshot to Code access code"
|
||||||
|
value={settings.accessCode || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
setSettings((s) => ({
|
||||||
|
...s,
|
||||||
|
accessCode: e.target.value,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<Label htmlFor="image-generation">
|
<Label htmlFor="image-generation">
|
||||||
<div>DALL-E Placeholder Image Generation</div>
|
<div>DALL-E Placeholder Image Generation</div>
|
||||||
@ -77,53 +109,115 @@ function SettingsDialog({ settings, setSettings }: Props) {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Label htmlFor="screenshot-one-api-key">
|
{!IS_RUNNING_ON_CLOUD && (
|
||||||
<div>
|
<>
|
||||||
ScreenshotOne API key (optional - only needed if you want to use
|
<Label htmlFor="openai-api-key">
|
||||||
URLs directly instead of taking the screenshot yourself)
|
<div>OpenAI Base URL (optional)</div>
|
||||||
</div>
|
<div className="font-light mt-2 leading-relaxed">
|
||||||
<div className="font-light mt-2 leading-relaxed">
|
Replace with a proxy URL if you don't want to use the default.
|
||||||
Only stored in your browser. Never stored on servers.{" "}
|
</div>
|
||||||
<a
|
</Label>
|
||||||
href="https://screenshotone.com?via=screenshot-to-code"
|
|
||||||
className="underline"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
Get 100 screenshots/mo for free.
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</Label>
|
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
id="screenshot-one-api-key"
|
id="openai-base-url"
|
||||||
placeholder="ScreenshotOne API key"
|
placeholder="OpenAI Base URL"
|
||||||
value={settings.screenshotOneApiKey || ""}
|
value={settings.openAiBaseURL || ""}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setSettings((s) => ({
|
setSettings((s) => ({
|
||||||
...s,
|
...s,
|
||||||
screenshotOneApiKey: e.target.value,
|
openAiBaseURL: e.target.value,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
<Label htmlFor="editor-theme">
|
<Accordion type="single" collapsible className="w-full">
|
||||||
<div>Editor Theme</div>
|
<AccordionItem value="item-1">
|
||||||
</Label>
|
<AccordionTrigger>Screenshot by URL Config</AccordionTrigger>
|
||||||
<div>
|
<AccordionContent>
|
||||||
<Select // Use the custom Select component here
|
<Label htmlFor="screenshot-one-api-key">
|
||||||
name="editor-theme"
|
<div className="leading-normal font-normal text-xs">
|
||||||
value={settings.editorTheme}
|
If you want to use URLs directly instead of taking the
|
||||||
onValueChange={(value) => handleThemeChange(value as EditorTheme)}
|
screenshot yourself, add a ScreenshotOne API key.{" "}
|
||||||
>
|
<a
|
||||||
<SelectTrigger className="w-[180px]">
|
href="https://screenshotone.com?via=screenshot-to-code"
|
||||||
{capitalize(settings.editorTheme)}
|
className="underline"
|
||||||
</SelectTrigger>
|
target="_blank"
|
||||||
<SelectContent>
|
>
|
||||||
<SelectItem value="cobalt">Cobalt</SelectItem>
|
Get 100 screenshots/mo for free.
|
||||||
<SelectItem value="espresso">Espresso</SelectItem>
|
</a>
|
||||||
</SelectContent>
|
</div>
|
||||||
</Select>
|
</Label>
|
||||||
</div>
|
|
||||||
|
<Input
|
||||||
|
id="screenshot-one-api-key"
|
||||||
|
className="mt-2"
|
||||||
|
placeholder="ScreenshotOne API key"
|
||||||
|
value={settings.screenshotOneApiKey || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
setSettings((s) => ({
|
||||||
|
...s,
|
||||||
|
screenshotOneApiKey: e.target.value,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
|
<Accordion type="single" collapsible className="w-full">
|
||||||
|
<AccordionItem value="item-1">
|
||||||
|
<AccordionTrigger>Theme Settings</AccordionTrigger>
|
||||||
|
<AccordionContent className="space-y-4 flex flex-col">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Label htmlFor="app-theme">
|
||||||
|
<div>App Theme</div>
|
||||||
|
</Label>
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
className="flex rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50t"
|
||||||
|
onClick={() => {
|
||||||
|
document
|
||||||
|
.querySelector("div.mt-2")
|
||||||
|
?.classList.toggle("dark"); // enable dark mode for sidebar
|
||||||
|
document.body.classList.toggle("dark");
|
||||||
|
document
|
||||||
|
.querySelector('div[role="presentation"]')
|
||||||
|
?.classList.toggle("dark"); // enable dark mode for upload container
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Toggle dark mode
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Label htmlFor="editor-theme">
|
||||||
|
<div>
|
||||||
|
Code Editor Theme - requires page refresh to update
|
||||||
|
</div>
|
||||||
|
</Label>
|
||||||
|
<div>
|
||||||
|
<Select // Use the custom Select component here
|
||||||
|
name="editor-theme"
|
||||||
|
value={settings.editorTheme}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
handleThemeChange(value as EditorTheme)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-[180px]">
|
||||||
|
{capitalize(settings.editorTheme)}
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="cobalt">Cobalt</SelectItem>
|
||||||
|
<SelectItem value="espresso">Espresso</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
</Accordion>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
|
|||||||
29
frontend/src/components/ui/popover.tsx
Normal file
29
frontend/src/components/ui/popover.tsx
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import * as PopoverPrimitive from "@radix-ui/react-popover"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const Popover = PopoverPrimitive.Root
|
||||||
|
|
||||||
|
const PopoverTrigger = PopoverPrimitive.Trigger
|
||||||
|
|
||||||
|
const PopoverContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof PopoverPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
|
||||||
|
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
|
||||||
|
<PopoverPrimitive.Portal>
|
||||||
|
<PopoverPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
align={align}
|
||||||
|
sideOffset={sideOffset}
|
||||||
|
className={cn(
|
||||||
|
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none 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}
|
||||||
|
/>
|
||||||
|
</PopoverPrimitive.Portal>
|
||||||
|
))
|
||||||
|
PopoverContent.displayName = PopoverPrimitive.Content.displayName
|
||||||
|
|
||||||
|
export { Popover, PopoverTrigger, PopoverContent }
|
||||||
@ -159,4 +159,4 @@ export {
|
|||||||
SelectSeparator,
|
SelectSeparator,
|
||||||
SelectScrollUpButton,
|
SelectScrollUpButton,
|
||||||
SelectScrollDownButton,
|
SelectScrollDownButton,
|
||||||
}
|
}
|
||||||
@ -61,9 +61,21 @@
|
|||||||
|
|
||||||
--radius: 0.5rem;
|
--radius: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body.dark {
|
||||||
|
background-color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
div[role="presentation"].dark {
|
||||||
|
background-color: #09090b !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
iframe {
|
||||||
|
background-color: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
--background: 222.2 84% 4.9%;
|
--background: 222.2 0% 0%;
|
||||||
--foreground: 210 40% 98%;
|
--foreground: 210 40% 98%;
|
||||||
|
|
||||||
--card: 222.2 84% 4.9%;
|
--card: 222.2 84% 4.9%;
|
||||||
|
|||||||
@ -7,6 +7,6 @@ import { Toaster } from "react-hot-toast";
|
|||||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<App />
|
<App />
|
||||||
<Toaster />
|
<Toaster toastOptions={{ className:"dark:bg-zinc-950 dark:text-white" }}/>
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -9,7 +9,7 @@ export enum CSSOption {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export enum JSFrameworkOption {
|
export enum JSFrameworkOption {
|
||||||
VANILLA = "vanilla",
|
NO_FRAMEWORK = "vanilla",
|
||||||
REACT = "react",
|
REACT = "react",
|
||||||
VUE = "vue",
|
VUE = "vue",
|
||||||
}
|
}
|
||||||
@ -27,10 +27,12 @@ export interface OutputSettings {
|
|||||||
|
|
||||||
export interface Settings {
|
export interface Settings {
|
||||||
openAiApiKey: string | null;
|
openAiApiKey: string | null;
|
||||||
|
openAiBaseURL: string | null;
|
||||||
screenshotOneApiKey: string | null;
|
screenshotOneApiKey: string | null;
|
||||||
isImageGenerationEnabled: boolean;
|
isImageGenerationEnabled: boolean;
|
||||||
editorTheme: EditorTheme;
|
editorTheme: EditorTheme;
|
||||||
isTermOfServiceAccepted: boolean; // Only relevant for hosted version
|
isTermOfServiceAccepted: boolean; // Only relevant for hosted version
|
||||||
|
accessCode: string | null; // Only relevant for hosted version
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum AppState {
|
export enum AppState {
|
||||||
|
|||||||
@ -805,6 +805,28 @@
|
|||||||
"@babel/runtime" "^7.13.10"
|
"@babel/runtime" "^7.13.10"
|
||||||
"@radix-ui/react-primitive" "1.0.3"
|
"@radix-ui/react-primitive" "1.0.3"
|
||||||
|
|
||||||
|
"@radix-ui/react-popover@^1.0.7":
|
||||||
|
version "1.0.7"
|
||||||
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-popover/-/react-popover-1.0.7.tgz#23eb7e3327330cb75ec7b4092d685398c1654e3c"
|
||||||
|
integrity sha512-shtvVnlsxT6faMnK/a7n0wptwBD23xc1Z5mdrtKLwVEfsEMXodS0r5s0/g5P0hX//EKYZS2sxUjqfzlg52ZSnQ==
|
||||||
|
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-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-slot" "1.0.2"
|
||||||
|
"@radix-ui/react-use-controllable-state" "1.0.1"
|
||||||
|
aria-hidden "^1.1.1"
|
||||||
|
react-remove-scroll "2.5.5"
|
||||||
|
|
||||||
"@radix-ui/react-popper@1.1.3":
|
"@radix-ui/react-popper@1.1.3":
|
||||||
version "1.1.3"
|
version "1.1.3"
|
||||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-popper/-/react-popper-1.1.3.tgz#24c03f527e7ac348fabf18c89795d85d21b00b42"
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-popper/-/react-popper-1.1.3.tgz#24c03f527e7ac348fabf18c89795d85d21b00b42"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user