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:
|
||||
|
||||
@ -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
|
||||
<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.
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
async def process_tasks(prompts, api_key):
|
||||
tasks = [generate_image(prompt, api_key) for prompt in prompts]
|
||||
async def process_tasks(prompts, api_key, base_url):
|
||||
tasks = [generate_image(prompt, api_key, base_url) for prompt in prompts]
|
||||
results = await asyncio.gather(*tasks, return_exceptions=True)
|
||||
|
||||
processed_results = []
|
||||
@ -20,8 +20,8 @@ async def process_tasks(prompts, api_key):
|
||||
return processed_results
|
||||
|
||||
|
||||
async def generate_image(prompt, api_key):
|
||||
client = AsyncOpenAI(api_key=api_key)
|
||||
async def generate_image(prompt, api_key, base_url):
|
||||
client = AsyncOpenAI(api_key=api_key, base_url=base_url)
|
||||
image_params = {
|
||||
"model": "dall-e-3",
|
||||
"quality": "standard",
|
||||
@ -60,7 +60,7 @@ def create_alt_url_mapping(code):
|
||||
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
|
||||
soup = BeautifulSoup(code, "html.parser")
|
||||
images = soup.find_all("img")
|
||||
@ -87,7 +87,7 @@ async def generate_images(code, api_key, image_cache):
|
||||
return code
|
||||
|
||||
# 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
|
||||
mapped_image_urls = dict(zip(prompts, results))
|
||||
|
||||
@ -6,9 +6,12 @@ MODEL_GPT_4_VISION = "gpt-4-vision-preview"
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
# Load environment variables first
|
||||
from dotenv import load_dotenv
|
||||
from pydantic import BaseModel
|
||||
|
||||
load_dotenv()
|
||||
|
||||
@ -16,6 +15,7 @@ from mock import mock_completion
|
||||
from image_generation import create_alt_url_mapping, generate_images
|
||||
from prompts import assemble_prompt
|
||||
from routes import screenshot
|
||||
from access_token import validate_access_token
|
||||
|
||||
app = FastAPI(openapi_url=None, docs_url=None, redoc_url=None)
|
||||
|
||||
@ -81,6 +81,20 @@ async def stream_code(websocket: WebSocket):
|
||||
|
||||
# Get the OpenAI API key from the request. Fall back to environment variable if not provided.
|
||||
# If neither is provided, we throw an error.
|
||||
openai_api_key = None
|
||||
if "accessCode" in params and params["accessCode"]:
|
||||
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:
|
||||
if params["openAiApiKey"]:
|
||||
openai_api_key = params["openAiApiKey"]
|
||||
print("Using OpenAI API key from client-side settings dialog")
|
||||
@ -99,6 +113,22 @@ async def stream_code(websocket: WebSocket):
|
||||
)
|
||||
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 = (
|
||||
params["isImageGenerationEnabled"]
|
||||
if "isImageGenerationEnabled" in params
|
||||
@ -137,6 +167,7 @@ async def stream_code(websocket: WebSocket):
|
||||
completion = await stream_openai_response(
|
||||
prompt_messages,
|
||||
api_key=openai_api_key,
|
||||
base_url=openai_base_url,
|
||||
callback=lambda x: process_chunk(x),
|
||||
)
|
||||
|
||||
@ -149,7 +180,10 @@ async def stream_code(websocket: WebSocket):
|
||||
{"type": "status", "value": "Generating 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:
|
||||
updated_html = completion
|
||||
|
||||
@ -21,6 +21,34 @@
|
||||
<%- injectHead %>
|
||||
|
||||
<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>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@ -19,6 +19,7 @@
|
||||
"@radix-ui/react-dialog": "^1.0.5",
|
||||
"@radix-ui/react-icons": "^1.3.0",
|
||||
"@radix-ui/react-label": "^2.0.2",
|
||||
"@radix-ui/react-popover": "^1.0.7",
|
||||
"@radix-ui/react-select": "^2.0.0",
|
||||
"@radix-ui/react-separator": "^1.0.3",
|
||||
"@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>(
|
||||
{
|
||||
openAiApiKey: null,
|
||||
openAiBaseURL: null,
|
||||
screenshotOneApiKey: null,
|
||||
isImageGenerationEnabled: true,
|
||||
editorTheme: EditorTheme.COBALT,
|
||||
isTermOfServiceAccepted: false,
|
||||
accessCode: null,
|
||||
},
|
||||
"setting"
|
||||
);
|
||||
const [outputSettings, setOutputSettings] = useState<OutputSettings>({
|
||||
css: CSSOption.TAILWIND,
|
||||
js: JSFrameworkOption.VANILLA,
|
||||
components: UIComponentOption.HTML,
|
||||
js: JSFrameworkOption.NO_FRAMEWORK,
|
||||
});
|
||||
const [shouldIncludeResultImage, setShouldIncludeResultImage] =
|
||||
useState<boolean>(false);
|
||||
@ -172,35 +174,33 @@ function App() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mt-2">
|
||||
{IS_RUNNING_ON_CLOUD && <PicoBadge />}
|
||||
<div className="mt-2 dark:bg-black dark:text-white">
|
||||
{IS_RUNNING_ON_CLOUD && <PicoBadge settings={settings} />}
|
||||
{IS_RUNNING_ON_CLOUD && (
|
||||
<TermsOfServiceDialog
|
||||
open={!settings.isTermOfServiceAccepted}
|
||||
onOpenChange={handleTermDialogOpenChange}
|
||||
/>
|
||||
)}
|
||||
|
||||
<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 items-center justify-between mt-10">
|
||||
<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 mb-2">
|
||||
<h1 className="text-2xl ">Screenshot to Code</h1>
|
||||
<SettingsDialog settings={settings} setSettings={setSettings} />
|
||||
</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
|
||||
outputSettings={outputSettings}
|
||||
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.CODE_READY) && (
|
||||
@ -213,7 +213,10 @@ function App() {
|
||||
{executionConsole.slice(-1)[0]}
|
||||
</div>
|
||||
<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
|
||||
</Button>
|
||||
</div>
|
||||
@ -230,26 +233,32 @@ function App() {
|
||||
value={updateInstruction}
|
||||
/>
|
||||
<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?
|
||||
</div>
|
||||
<Switch
|
||||
checked={shouldIncludeResultImage}
|
||||
onCheckedChange={setShouldIncludeResultImage}
|
||||
className="dark:bg-gray-700"
|
||||
/>
|
||||
</div>
|
||||
<Button onClick={doUpdate}>Update</Button>
|
||||
<Button
|
||||
onClick={doUpdate}
|
||||
className="dark:text-white dark:bg-gray-700"
|
||||
>
|
||||
Update
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex items-center gap-x-2 mt-2">
|
||||
<Button
|
||||
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
|
||||
</Button>
|
||||
<Button
|
||||
onClick={reset}
|
||||
className="flex items-center gap-x-2"
|
||||
className="flex items-center gap-x-2 dark:text-white dark:bg-gray-700"
|
||||
>
|
||||
<FaUndo />
|
||||
Reset
|
||||
|
||||
@ -2,8 +2,15 @@ export function OnboardingNote() {
|
||||
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, you need an OpenAI API key with GPT4 vision
|
||||
access.{" "}
|
||||
To use Screenshot to Code,{" "}
|
||||
<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
|
||||
href="https://github.com/abi/screenshot-to-code/blob/main/Troubleshooting.md"
|
||||
className="inline underline hover:opacity-70"
|
||||
@ -11,18 +18,8 @@ export function OnboardingNote() {
|
||||
>
|
||||
Follow these instructions to get yourself a key.
|
||||
</a>{" "}
|
||||
Then, paste it in the Settings dialog (gear icon above).
|
||||
</span>
|
||||
<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>
|
||||
and paste it in the Settings dialog (gear icon above). Your key is only
|
||||
stored in your browser. Never stored on our servers.
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -5,16 +5,18 @@ import {
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
} from "./ui/select";
|
||||
import { CSSOption, UIComponentOption, JSFrameworkOption, OutputSettings } from "../types";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "./ui/accordion";
|
||||
CSSOption,
|
||||
UIComponentOption,
|
||||
JSFrameworkOption,
|
||||
OutputSettings,
|
||||
} from "../types";
|
||||
import { capitalize } from "../lib/utils";
|
||||
import toast from "react-hot-toast";
|
||||
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) {
|
||||
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) {
|
||||
switch (option) {
|
||||
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 {
|
||||
outputSettings: 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) => {
|
||||
setOutputSettings((prev) => {
|
||||
if (prev.js === JSFrameworkOption.REACT) {
|
||||
if (value !== CSSOption.TAILWIND) {
|
||||
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 {
|
||||
css: CSSOption.TAILWIND,
|
||||
js: JSFrameworkOption.REACT,
|
||||
components: UIComponentOption.HTML
|
||||
components: UIComponentOption.HTML,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
@ -71,7 +123,7 @@ function OutputSettingsSection({ outputSettings, setOutputSettings }: Props) {
|
||||
setOutputSettings(() => ({
|
||||
css: CSSOption.TAILWIND,
|
||||
js: value as JSFrameworkOption,
|
||||
components: UIComponentOption.HTML
|
||||
components: UIComponentOption.HTML,
|
||||
}));
|
||||
} else {
|
||||
setOutputSettings((prev) => ({
|
||||
@ -85,8 +137,8 @@ function OutputSettingsSection({ outputSettings, setOutputSettings }: Props) {
|
||||
if (value === UIComponentOption.IONIC) {
|
||||
setOutputSettings(() => ({
|
||||
css: CSSOption.TAILWIND,
|
||||
js: JSFrameworkOption.VANILLA,
|
||||
components: value as UIComponentOption
|
||||
js: JSFrameworkOption.NO_FRAMEWORK,
|
||||
components: value as UIComponentOption,
|
||||
}));
|
||||
} else {
|
||||
setOutputSettings((prev) => ({
|
||||
@ -96,128 +148,182 @@ function OutputSettingsSection({ outputSettings, setOutputSettings }: Props) {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const checkUIComponentOptionOrDefault = (valueItem: UIComponentOption ) : UIComponentOption => {
|
||||
const checkUIComponentOptionOrDefault = (
|
||||
valueItem: UIComponentOption
|
||||
): UIComponentOption => {
|
||||
switch (valueItem) {
|
||||
case UIComponentOption.IONIC:
|
||||
if (outputSettings.js != JSFrameworkOption.VANILLA || outputSettings.css != CSSOption.TAILWIND) {
|
||||
return UIComponentOption.HTML
|
||||
if (
|
||||
outputSettings.js != JSFrameworkOption.NO_FRAMEWORK ||
|
||||
outputSettings.css != CSSOption.TAILWIND
|
||||
) {
|
||||
return UIComponentOption.HTML;
|
||||
}
|
||||
}
|
||||
return valueItem;
|
||||
}
|
||||
};
|
||||
|
||||
const checkCSSOptionOrDefault = (valueItem: CSSOption ) : CSSOption => {
|
||||
const checkCSSOptionOrDefault = (valueItem: CSSOption): CSSOption => {
|
||||
switch (valueItem) {
|
||||
default:
|
||||
return valueItem;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const checkJSFrameworkOptionOrDefault = (valueItem: JSFrameworkOption ) : JSFrameworkOption => {
|
||||
const checkJSFrameworkOptionOrDefault = (
|
||||
valueItem: JSFrameworkOption
|
||||
): JSFrameworkOption => {
|
||||
switch (valueItem) {
|
||||
case JSFrameworkOption.REACT:
|
||||
if(outputSettings.css != CSSOption.TAILWIND) {
|
||||
return JSFrameworkOption.VANILLA
|
||||
if (outputSettings.css != CSSOption.TAILWIND) {
|
||||
return JSFrameworkOption.NO_FRAMEWORK;
|
||||
}
|
||||
break;
|
||||
}
|
||||
return valueItem;
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
checkOutputSettingsOptions();
|
||||
}, [outputSettings]);
|
||||
|
||||
const checkOutputSettingsOptions = () => {
|
||||
if ( isHiddenOption(outputSettings.css) || isHiddenOption(outputSettings.js) || isHiddenOption(outputSettings.components))
|
||||
{
|
||||
if (
|
||||
isHiddenOption(outputSettings.css) ||
|
||||
isHiddenOption(outputSettings.js) ||
|
||||
isHiddenOption(outputSettings.components)
|
||||
) {
|
||||
setOutputSettings((prev) => {
|
||||
return {
|
||||
css: checkCSSOptionOrDefault(prev.css),
|
||||
js: checkJSFrameworkOptionOrDefault(prev.js),
|
||||
components: checkUIComponentOptionOrDefault(prev.components),
|
||||
};
|
||||
})
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const isHiddenOption = ( option : CSSOption| JSFrameworkOption | UIComponentOption ) : boolean => {
|
||||
if (Object.values(CSSOption).includes(option as CSSOption)){
|
||||
return checkCSSOptionOrDefault(option as CSSOption) != option
|
||||
const isHiddenOption = (
|
||||
option: CSSOption | JSFrameworkOption | UIComponentOption
|
||||
): boolean => {
|
||||
if (Object.values(CSSOption).includes(option as CSSOption)) {
|
||||
return checkCSSOptionOrDefault(option as CSSOption) != option;
|
||||
}
|
||||
if (Object.values(JSFrameworkOption).includes(option as JSFrameworkOption)){
|
||||
return checkJSFrameworkOptionOrDefault(option as JSFrameworkOption) != option
|
||||
if (
|
||||
Object.values(JSFrameworkOption).includes(option as JSFrameworkOption)
|
||||
) {
|
||||
return (
|
||||
checkJSFrameworkOptionOrDefault(option as JSFrameworkOption) != option
|
||||
);
|
||||
}
|
||||
if (Object.values(UIComponentOption).includes(option as UIComponentOption)){
|
||||
return checkUIComponentOptionOrDefault(option as UIComponentOption) != option
|
||||
}
|
||||
return true
|
||||
if (
|
||||
Object.values(UIComponentOption).includes(option as UIComponentOption)
|
||||
) {
|
||||
return (
|
||||
checkUIComponentOptionOrDefault(option as UIComponentOption) != option
|
||||
);
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
return (
|
||||
<Accordion type="single" collapsible className="w-full">
|
||||
<AccordionItem value="item-1">
|
||||
<AccordionTrigger>
|
||||
<div className="flex gap-x-2">Output Settings </div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="gap-y-2 flex flex-col pt-2">
|
||||
<div className="flex justify-between items-center pr-2">
|
||||
<span className="text-sm">CSS</span>
|
||||
<Select value={outputSettings.css} onValueChange={onCSSValueChange}>
|
||||
<SelectTrigger className="w-[180px]">
|
||||
{displayCSSOption(outputSettings.css)}
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem value={CSSOption.TAILWIND}>Tailwind</SelectItem>
|
||||
<SelectItem value={CSSOption.BOOTSTRAP}>Bootstrap</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div className="flex flex-col gap-y-2 justify-between text-sm">
|
||||
{generateDisplayString(outputSettings)}{" "}
|
||||
{!shouldDisableUpdates && (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="outline">Customize</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-80 text-sm">
|
||||
<div className="grid gap-4">
|
||||
<div className="space-y-2">
|
||||
<h4 className="font-medium leading-none">Code Settings</h4>
|
||||
<p className="text-muted-foreground">
|
||||
Customize your code output
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex justify-between items-center pr-2">
|
||||
<span className="text-sm">JS Framework</span>
|
||||
<div className="grid gap-2">
|
||||
<div className="grid grid-cols-3 items-center gap-4">
|
||||
<Label htmlFor="output-settings-js">JS</Label>
|
||||
<Select
|
||||
value={outputSettings.js}
|
||||
onValueChange={onJsFrameworkChange}
|
||||
>
|
||||
<SelectTrigger className="w-[180px]">
|
||||
{capitalize(outputSettings.js)}
|
||||
<SelectTrigger
|
||||
className="col-span-2 h-8"
|
||||
id="output-settings-js"
|
||||
>
|
||||
{displayJSOption(outputSettings.js)}
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem value={JSFrameworkOption.VANILLA}>
|
||||
Vanilla
|
||||
<SelectItem value={JSFrameworkOption.NO_FRAMEWORK}>
|
||||
No Framework
|
||||
</SelectItem>
|
||||
<SelectItem value={JSFrameworkOption.REACT}>
|
||||
React
|
||||
</SelectItem>
|
||||
<SelectItem value={JSFrameworkOption.REACT} disabled={isHiddenOption(JSFrameworkOption.REACT)}>React</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex justify-between items-center pr-2">
|
||||
<span className="text-sm">Component Library</span>
|
||||
<div className="grid grid-cols-3 items-center gap-4">
|
||||
<Label htmlFor="output-settings-css">CSS</Label>
|
||||
<Select
|
||||
value={outputSettings.css}
|
||||
onValueChange={onCSSValueChange}
|
||||
>
|
||||
<SelectTrigger
|
||||
className="col-span-2 h-8"
|
||||
id="output-settings-css"
|
||||
>
|
||||
{displayCSSOption(outputSettings.css)}
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem value={CSSOption.TAILWIND}>
|
||||
Tailwind
|
||||
</SelectItem>
|
||||
<SelectItem value={CSSOption.BOOTSTRAP}>
|
||||
Bootstrap
|
||||
</SelectItem>
|
||||
</SelectGroup>
|
||||
</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 className="w-[180px]">
|
||||
<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>
|
||||
<SelectItem value={UIComponentOption.HTML}>
|
||||
HTML
|
||||
</SelectItem>
|
||||
<SelectItem
|
||||
value={UIComponentOption.IONIC}
|
||||
disabled={isHiddenOption(UIComponentOption.IONIC)}
|
||||
>
|
||||
Ionic
|
||||
</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex justify-between pr-2 mt-2">
|
||||
<span className="text-sm text-gray-500">Output: {outputSettings.js} + {outputSettings.css} + {outputSettings.components}</span>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
export function PicoBadge() {
|
||||
import { Settings } from "../types";
|
||||
|
||||
export function PicoBadge({ settings }: { settings: Settings }) {
|
||||
return (
|
||||
<>
|
||||
<a
|
||||
@ -12,6 +14,7 @@ export function PicoBadge() {
|
||||
feature requests?
|
||||
</div>
|
||||
</a>
|
||||
{!settings.accessCode && (
|
||||
<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
|
||||
@ -20,6 +23,17 @@ export function PicoBadge() {
|
||||
an open source project by Pico
|
||||
</div>
|
||||
</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 { Select, SelectContent, SelectItem, SelectTrigger } from "./ui/select";
|
||||
import { capitalize } from "../lib/utils";
|
||||
import { IS_RUNNING_ON_CLOUD } from "../config";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "./ui/accordion";
|
||||
|
||||
interface Props {
|
||||
settings: Settings;
|
||||
@ -38,6 +45,31 @@ function SettingsDialog({ settings, setSettings }: Props) {
|
||||
<DialogHeader>
|
||||
<DialogTitle className="mb-4">Settings</DialogTitle>
|
||||
</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">
|
||||
<Label htmlFor="image-generation">
|
||||
<div>DALL-E Placeholder Image Generation</div>
|
||||
@ -77,13 +109,37 @@ function SettingsDialog({ settings, setSettings }: Props) {
|
||||
}
|
||||
/>
|
||||
|
||||
<Label htmlFor="screenshot-one-api-key">
|
||||
<div>
|
||||
ScreenshotOne API key (optional - only needed if you want to use
|
||||
URLs directly instead of taking the screenshot yourself)
|
||||
</div>
|
||||
{!IS_RUNNING_ON_CLOUD && (
|
||||
<>
|
||||
<Label htmlFor="openai-api-key">
|
||||
<div>OpenAI Base URL (optional)</div>
|
||||
<div className="font-light mt-2 leading-relaxed">
|
||||
Only stored in your browser. Never stored on servers.{" "}
|
||||
Replace with a proxy URL if you don't want to use the default.
|
||||
</div>
|
||||
</Label>
|
||||
|
||||
<Input
|
||||
id="openai-base-url"
|
||||
placeholder="OpenAI Base URL"
|
||||
value={settings.openAiBaseURL || ""}
|
||||
onChange={(e) =>
|
||||
setSettings((s) => ({
|
||||
...s,
|
||||
openAiBaseURL: e.target.value,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Accordion type="single" collapsible className="w-full">
|
||||
<AccordionItem value="item-1">
|
||||
<AccordionTrigger>Screenshot by URL Config</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<Label htmlFor="screenshot-one-api-key">
|
||||
<div className="leading-normal font-normal text-xs">
|
||||
If you want to use URLs directly instead of taking the
|
||||
screenshot yourself, add a ScreenshotOne API key.{" "}
|
||||
<a
|
||||
href="https://screenshotone.com?via=screenshot-to-code"
|
||||
className="underline"
|
||||
@ -96,6 +152,7 @@ function SettingsDialog({ settings, setSettings }: Props) {
|
||||
|
||||
<Input
|
||||
id="screenshot-one-api-key"
|
||||
className="mt-2"
|
||||
placeholder="ScreenshotOne API key"
|
||||
value={settings.screenshotOneApiKey || ""}
|
||||
onChange={(e) =>
|
||||
@ -105,15 +162,48 @@ function SettingsDialog({ settings, setSettings }: Props) {
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</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>Editor Theme</div>
|
||||
<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)}
|
||||
onValueChange={(value) =>
|
||||
handleThemeChange(value as EditorTheme)
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="w-[180px]">
|
||||
{capitalize(settings.editorTheme)}
|
||||
@ -125,6 +215,10 @@ function SettingsDialog({ settings, setSettings }: Props) {
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<DialogClose>Save</DialogClose>
|
||||
|
||||
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 }
|
||||
@ -62,8 +62,20 @@
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
body.dark {
|
||||
background-color: black;
|
||||
}
|
||||
|
||||
div[role="presentation"].dark {
|
||||
background-color: #09090b !important;
|
||||
}
|
||||
|
||||
iframe {
|
||||
background-color: white !important;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 222.2 84% 4.9%;
|
||||
--background: 222.2 0% 0%;
|
||||
--foreground: 210 40% 98%;
|
||||
|
||||
--card: 222.2 84% 4.9%;
|
||||
|
||||
@ -7,6 +7,6 @@ import { Toaster } from "react-hot-toast";
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
<Toaster />
|
||||
<Toaster toastOptions={{ className:"dark:bg-zinc-950 dark:text-white" }}/>
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
||||
@ -9,7 +9,7 @@ export enum CSSOption {
|
||||
}
|
||||
|
||||
export enum JSFrameworkOption {
|
||||
VANILLA = "vanilla",
|
||||
NO_FRAMEWORK = "vanilla",
|
||||
REACT = "react",
|
||||
VUE = "vue",
|
||||
}
|
||||
@ -27,10 +27,12 @@ export interface OutputSettings {
|
||||
|
||||
export interface Settings {
|
||||
openAiApiKey: string | null;
|
||||
openAiBaseURL: string | null;
|
||||
screenshotOneApiKey: string | null;
|
||||
isImageGenerationEnabled: boolean;
|
||||
editorTheme: EditorTheme;
|
||||
isTermOfServiceAccepted: boolean; // Only relevant for hosted version
|
||||
accessCode: string | null; // Only relevant for hosted version
|
||||
}
|
||||
|
||||
export enum AppState {
|
||||
|
||||
@ -805,6 +805,28 @@
|
||||
"@babel/runtime" "^7.13.10"
|
||||
"@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":
|
||||
version "1.1.3"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-popper/-/react-popper-1.1.3.tgz#24c03f527e7ac348fabf18c89795d85d21b00b42"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user