diff --git a/backend/access_token.py b/backend/access_token.py new file mode 100644 index 0000000..e61ef12 --- /dev/null +++ b/backend/access_token.py @@ -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 diff --git a/backend/main.py b/backend/main.py index 4b75f71..4c5823b 100644 --- a/backend/main.py +++ b/backend/main.py @@ -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,13 +79,27 @@ 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. - if params["openAiApiKey"]: - openai_api_key = params["openAiApiKey"] - print("Using OpenAI API key from client-side settings dialog") + 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: - openai_api_key = os.environ.get("OPENAI_API_KEY") - if openai_api_key: - print("Using OpenAI API key from environment variable") + if params["openAiApiKey"]: + openai_api_key = params["openAiApiKey"] + print("Using OpenAI API key from client-side settings dialog") + else: + openai_api_key = os.environ.get("OPENAI_API_KEY") + if openai_api_key: + print("Using OpenAI API key from environment variable") if not openai_api_key: print("OpenAI API key not found") diff --git a/frontend/index.html b/frontend/index.html index f747064..2a7fa0e 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -21,6 +21,34 @@ <%- injectHead %> Screenshot to Code + + + + + + + + + + + + + + + +
diff --git a/frontend/package.json b/frontend/package.json index 0d180b6..476a72d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -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", diff --git a/frontend/public/brand/twitter-summary-card.png b/frontend/public/brand/twitter-summary-card.png new file mode 100644 index 0000000..6fcedbf Binary files /dev/null and b/frontend/public/brand/twitter-summary-card.png differ diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 11aa9a0..54de508 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -51,12 +51,13 @@ function App() { isImageGenerationEnabled: true, editorTheme: EditorTheme.COBALT, isTermOfServiceAccepted: false, + accessCode: null, }, "setting" ); const [outputSettings, setOutputSettings] = useState({ css: CSSOption.TAILWIND, - js: JSFrameworkOption.VANILLA, + js: JSFrameworkOption.NO_FRAMEWORK, }); const [shouldIncludeResultImage, setShouldIncludeResultImage] = useState(false); @@ -169,9 +170,8 @@ function App() { })); }; - return (
- {IS_RUNNING_ON_CLOUD && } + {IS_RUNNING_ON_CLOUD && } {IS_RUNNING_ON_CLOUD && (
-
+

Screenshot to Code

- {appState === AppState.INITIAL && ( -

- Drag & drop a screenshot to get started. -

- )} - {appState === AppState.INITIAL && ( - - )} + - {IS_RUNNING_ON_CLOUD && !settings.openAiApiKey && } + {IS_RUNNING_ON_CLOUD && + !(settings.openAiApiKey || settings.accessCode) && ( + + )} {(appState === AppState.CODING || appState === AppState.CODE_READY) && ( diff --git a/frontend/src/components/OnboardingNote.tsx b/frontend/src/components/OnboardingNote.tsx index 5bd5fb4..acf919f 100644 --- a/frontend/src/components/OnboardingNote.tsx +++ b/frontend/src/components/OnboardingNote.tsx @@ -2,8 +2,15 @@ export function OnboardingNote() { return (
- To use Screenshot to Code, you need an OpenAI API key with GPT4 vision - access.{" "} + To use Screenshot to Code,{" "} + + buy some credits (100 generations for $36) + {" "} + or use your own OpenAI API key with GPT4 vision access.{" "} Follow these instructions to get yourself a key. {" "} - Then, paste it in the Settings dialog (gear icon above). - - - Your key is only stored in your browser. Never stored on our servers. If - you prefer, you can also run this app completely locally.{" "} - - See the Github project for instructions. - + and paste it in the Settings dialog (gear icon above). Your key is only + stored in your browser. Never stored on our servers.
); diff --git a/frontend/src/components/OutputSettingsSection.tsx b/frontend/src/components/OutputSettingsSection.tsx index fd76281..43318f8 100644 --- a/frontend/src/components/OutputSettingsSection.tsx +++ b/frontend/src/components/OutputSettingsSection.tsx @@ -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 ( +
+ Generating React +{" "} + Tailwind code +
+ ); + } else if ( + settings.js === JSFrameworkOption.NO_FRAMEWORK && + settings.css === CSSOption.TAILWIND + ) { + return ( +
+ Generating HTML +{" "} + Tailwind code +
+ ); + } else if ( + settings.js === JSFrameworkOption.NO_FRAMEWORK && + settings.css === CSSOption.BOOTSTRAP + ) { + return ( +
+ Generating HTML +{" "} + Bootstrap code +
+ ); + } +} + interface Props { outputSettings: OutputSettings; setOutputSettings: React.Dispatch>; + 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 ( - - - -
Output Settings
-
- -
- CSS - -
-
- JS Framework - -
-
-
-
+
+ {generateDisplayString(outputSettings)}{" "} + {!shouldDisableUpdates && ( + + + + + +
+
+

Code Settings

+

+ Customize your code output +

+
+
+
+ + +
+
+ + +
+
+
+
+
+ )} +
); } diff --git a/frontend/src/components/PicoBadge.tsx b/frontend/src/components/PicoBadge.tsx index bdd0083..70a4f20 100644 --- a/frontend/src/components/PicoBadge.tsx +++ b/frontend/src/components/PicoBadge.tsx @@ -1,4 +1,6 @@ -export function PicoBadge() { +import { Settings } from "../types"; + +export function PicoBadge({ settings }: { settings: Settings }) { return ( <> - -
+
- an open source project by Pico -
-
+ > + an open source project by Pico +
+ + )} + {settings.accessCode && ( + +
+ email support +
+
+ )} ); } diff --git a/frontend/src/components/SettingsDialog.tsx b/frontend/src/components/SettingsDialog.tsx index 73844f4..9d9a70b 100644 --- a/frontend/src/components/SettingsDialog.tsx +++ b/frontend/src/components/SettingsDialog.tsx @@ -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) { Settings + + {/* Access code */} + {IS_RUNNING_ON_CLOUD && ( +
+ + + + setSettings((s) => ({ + ...s, + accessCode: e.target.value, + })) + } + /> +
+ )} +