Merge branch 'main' into pr/122

This commit is contained in:
Abi Raja 2023-11-30 13:56:38 -05:00
commit 45a64326f6
19 changed files with 594 additions and 215 deletions

View File

@ -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
View 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

View File

@ -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))

View File

@ -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

View File

@ -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

View File

@ -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>

View File

@ -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",

Binary file not shown.

After

Width:  |  Height:  |  Size: 330 KiB

View File

@ -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

View File

@ -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>
);

View File

@ -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 => {
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
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 => {
const isHiddenOption = (
option: CSSOption | JSFrameworkOption | UIComponentOption
): boolean => {
if (Object.values(CSSOption).includes(option as CSSOption)) {
return checkCSSOptionOrDefault(option as CSSOption) != option
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>
);
}

View File

@ -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>
)}
</>
);
}

View File

@ -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>

View 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 }

View File

@ -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%;

View File

@ -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>
);

View File

@ -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 {

View File

@ -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"