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
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)
@ -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.
# 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")

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

@ -51,12 +51,13 @@ function App() {
isImageGenerationEnabled: true,
editorTheme: EditorTheme.COBALT,
isTermOfServiceAccepted: false,
accessCode: null,
},
"setting"
);
const [outputSettings, setOutputSettings] = useState<OutputSettings>({
css: CSSOption.TAILWIND,
js: JSFrameworkOption.VANILLA,
js: JSFrameworkOption.NO_FRAMEWORK,
});
const [shouldIncludeResultImage, setShouldIncludeResultImage] =
useState<boolean>(false);
@ -169,9 +170,8 @@ function App() {
}));
};
return (
<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 && (
<TermsOfServiceDialog
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="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>
<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) && (

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

@ -6,14 +6,10 @@ import {
SelectTrigger,
} from "./ui/select";
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 { Label } from "@radix-ui/react-label";
import { Button } from "./ui/button";
import { Popover, PopoverTrigger, PopoverContent } from "./ui/popover";
function displayCSSOption(option: CSSOption) {
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) {
switch (option) {
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 {
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 {
@ -79,48 +125,76 @@ function OutputSettingsSection({ outputSettings, setOutputSettings }: Props) {
};
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}>React</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
<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>
</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,7 @@ 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";
interface Props {
settings: Settings;
@ -38,6 +39,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">
<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">
<Label htmlFor="image-generation">
<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 {
VANILLA = "vanilla",
NO_FRAMEWORK = "vanilla",
REACT = "react",
VUE = "vue",
}
@ -25,6 +25,7 @@ export interface Settings {
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"