Merge branch 'main' into main

This commit is contained in:
kacher 2023-11-29 17:46:01 -07:00 committed by GitHub
commit f4ecafad02
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 329 additions and 97 deletions

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

@ -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)
@ -79,6 +79,20 @@ 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.
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"]: if params["openAiApiKey"]:
openai_api_key = params["openAiApiKey"] openai_api_key = params["openAiApiKey"]
print("Using OpenAI API key from client-side settings dialog") print("Using OpenAI API key from client-side settings dialog")

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 330 KiB

View File

@ -51,12 +51,13 @@ function App() {
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, js: JSFrameworkOption.NO_FRAMEWORK,
}); });
const [shouldIncludeResultImage, setShouldIncludeResultImage] = const [shouldIncludeResultImage, setShouldIncludeResultImage] =
useState<boolean>(false); useState<boolean>(false);
@ -169,9 +170,8 @@ function App() {
})); }));
}; };
return (
<div className="mt-2 dark:bg-black dark:text-white"> <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}
@ -180,24 +180,23 @@ function App() {
)} )}
<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 dark:bg-zinc-950 dark:text-white"> <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) && (

View File

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

View File

@ -6,14 +6,10 @@ import {
SelectTrigger, SelectTrigger,
} from "./ui/select"; } from "./ui/select";
import { CSSOption, JSFrameworkOption, OutputSettings } from "../types"; import { CSSOption, JSFrameworkOption, OutputSettings } from "../types";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "./ui/accordion";
import { capitalize } from "../lib/utils";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
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) {
@ -26,6 +22,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":
@ -37,18 +44,57 @@ function convertStringToCSSOption(option: string) {
} }
} }
function generateDisplayString(settings: OutputSettings) {
if (
settings.js === JSFrameworkOption.REACT &&
settings.css === CSSOption.TAILWIND
) {
return (
<div>
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">
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>
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 {
@ -79,48 +125,76 @@ function OutputSettingsSection({ outputSettings, setOutputSettings }: Props) {
}; };
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>
<SelectItem value={CSSOption.BOOTSTRAP}>Bootstrap</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</div> </div>
<div className="flex justify-between items-center pr-2"> <div className="grid gap-2">
<span className="text-sm">JS Framework</span> <div className="grid grid-cols-3 items-center gap-4">
<Label htmlFor="output-settings-js">JS</Label>
<Select <Select
value={outputSettings.js} value={outputSettings.js}
onValueChange={onJsFrameworkChange} onValueChange={onJsFrameworkChange}
> >
<SelectTrigger className="w-[180px]"> <SelectTrigger
{capitalize(outputSettings.js)} className="col-span-2 h-8"
id="output-settings-js"
>
{displayJSOption(outputSettings.js)}
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectGroup> <SelectGroup>
<SelectItem value={JSFrameworkOption.VANILLA}> <SelectItem value={JSFrameworkOption.NO_FRAMEWORK}>
Vanilla No Framework
</SelectItem>
<SelectItem value={JSFrameworkOption.REACT}>
React
</SelectItem> </SelectItem>
<SelectItem value={JSFrameworkOption.REACT}>React</SelectItem>
</SelectGroup> </SelectGroup>
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
</AccordionContent> <div className="grid grid-cols-3 items-center gap-4">
</AccordionItem> <Label htmlFor="output-settings-css">CSS</Label>
</Accordion> <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>
</div>
</PopoverContent>
</Popover>
)}
</div>
); );
} }

View File

@ -1,4 +1,6 @@
export function PicoBadge() { import { Settings } from "../types";
export function PicoBadge({ settings }: { settings: Settings }) {
return ( return (
<> <>
<a <a
@ -12,6 +14,7 @@ export function PicoBadge() {
feature requests? feature requests?
</div> </div>
</a> </a>
{!settings.accessCode && (
<a href="https://picoapps.xyz?ref=screenshot-to-code" target="_blank"> <a href="https://picoapps.xyz?ref=screenshot-to-code" target="_blank">
<div <div
className="fixed z-50 bottom-5 right-5 rounded-md shadow text-black 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 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>
)}
</> </>
); );
} }

View File

@ -15,6 +15,7 @@ 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";
interface Props { interface Props {
settings: Settings; settings: Settings;
@ -38,6 +39,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">
<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"
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>

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

@ -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",
} }
@ -25,6 +25,7 @@ export interface Settings {
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 {

View File

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