From bf23d3198af068f86e01a96b549835acc5f68f10 Mon Sep 17 00:00:00 2001 From: Abi Raja Date: Mon, 20 Nov 2023 11:48:33 -0500 Subject: [PATCH 1/4] support screenshotting a URL with ScreenshotOne --- backend/main.py | 21 +++++- backend/poetry.lock | 2 +- backend/pyproject.toml | 1 + backend/routes/screenshot.py | 64 ++++++++++++++++ frontend/src/App.tsx | 17 ++++- frontend/src/components/SettingsDialog.tsx | 20 +++++ frontend/src/components/UrlInputSection.tsx | 83 +++++++++++++++++++++ frontend/src/config.ts | 3 + frontend/src/types.ts | 1 + 9 files changed, 205 insertions(+), 7 deletions(-) create mode 100644 backend/routes/screenshot.py create mode 100644 frontend/src/components/UrlInputSection.tsx diff --git a/backend/main.py b/backend/main.py index c6e5556..4eb09fa 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1,5 +1,6 @@ # Load environment variables first from dotenv import load_dotenv +from pydantic import BaseModel load_dotenv() @@ -9,19 +10,35 @@ import os import traceback from datetime import datetime from fastapi import FastAPI, WebSocket - +from fastapi.middleware.cors import CORSMiddleware from llm import stream_openai_response from mock import mock_completion from image_generation import create_alt_url_mapping, generate_images from prompts import assemble_prompt +from routes import screenshot + +app = FastAPI(openapi_url=None, docs_url=None, redoc_url=None) + +# Configure CORS + +# Configure CORS settings +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) -app = FastAPI() # Useful for debugging purposes when you don't want to waste GPT4-Vision credits # Setting to True will stream a mock response instead of calling the OpenAI API SHOULD_MOCK_AI_RESPONSE = False +app.include_router(screenshot.router) + + def write_logs(prompt_messages, completion): # Get the logs path from environment, default to the current working directory logs_path = os.environ.get("LOGS_PATH", os.getcwd()) diff --git a/backend/poetry.lock b/backend/poetry.lock index 7a623b9..29e7937 100644 --- a/backend/poetry.lock +++ b/backend/poetry.lock @@ -471,4 +471,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "5e4aa03dda279f66a9b3d30f7327109bcfd395795470d95f8c563897ce1bff84" +content-hash = "b8d248a44a5eea9638a7726096de77d7a9aa8c00673da806534da2c228ffabb4" diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 9aa721d..cacdda9 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -13,6 +13,7 @@ websockets = "^12.0" openai = "^1.2.4" python-dotenv = "^1.0.0" beautifulsoup4 = "^4.12.2" +httpx = "^0.25.1" [build-system] requires = ["poetry-core"] diff --git a/backend/routes/screenshot.py b/backend/routes/screenshot.py new file mode 100644 index 0000000..7efcfb8 --- /dev/null +++ b/backend/routes/screenshot.py @@ -0,0 +1,64 @@ +import base64 +from fastapi import APIRouter +from pydantic import BaseModel +import httpx + +router = APIRouter() + + +def bytes_to_data_url(image_bytes: bytes, mime_type: str) -> str: + base64_image = base64.b64encode(image_bytes).decode("utf-8") + return f"data:{mime_type};base64,{base64_image}" + + +async def capture_screenshot(target_url, api_key, device="desktop") -> bytes: + api_base_url = "https://api.screenshotone.com/take" + + params = { + "access_key": api_key, + "url": target_url, + "full_page": "true", + "device_scale_factor": "1", + "format": "png", + "block_ads": "true", + "block_cookie_banners": "true", + "block_trackers": "true", + "cache": "false", + "viewport_width": "342", + "viewport_height": "684", + } + + if device == "desktop": + params["viewport_width"] = "1280" + params["viewport_height"] = "832" + + async with httpx.AsyncClient(timeout=60) as client: + response = await client.get(api_base_url, params=params) + if response.status_code == 200 and response.content: + return response.content + else: + raise Exception("Error taking screenshot") + + +class ScreenshotRequest(BaseModel): + url: str + apiKey: str + + +class ScreenshotResponse(BaseModel): + url: str + + +@router.post("/api/screenshot") +async def app_screenshot(request: ScreenshotRequest): + # Extract the URL from the request body + url = request.url + api_key = request.apiKey + + # TODO: Add error handling + image_bytes = await capture_screenshot(url, api_key=api_key) + + # Convert the image bytes to a data url + data_url = bytes_to_data_url(image_bytes, "image/png") + + return ScreenshotResponse(url=data_url) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index f579882..1e76d55 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -21,6 +21,7 @@ import { Settings } from "./types"; import { IS_RUNNING_ON_CLOUD } from "./config"; import { PicoBadge } from "./components/PicoBadge"; import { OnboardingNote } from "./components/OnboardingNote"; +import { UrlInputSection } from "./components/UrlInputSection"; function App() { const [appState, setAppState] = useState<"INITIAL" | "CODING" | "CODE_READY">( @@ -33,8 +34,9 @@ function App() { const [history, setHistory] = useState([]); const [settings, setSettings] = useState({ openAiApiKey: null, + screenshotOneApiKey: null, isImageGenerationEnabled: true, - editorTheme: "cobalt" + editorTheme: "cobalt", }); const downloadCode = () => { @@ -202,9 +204,13 @@ function App() {
{appState === "INITIAL" && ( - <> +
- + +
)} {(appState === "CODING" || appState === "CODE_READY") && ( @@ -231,7 +237,10 @@ function App() { - + diff --git a/frontend/src/components/SettingsDialog.tsx b/frontend/src/components/SettingsDialog.tsx index 33dd401..58caded 100644 --- a/frontend/src/components/SettingsDialog.tsx +++ b/frontend/src/components/SettingsDialog.tsx @@ -74,6 +74,26 @@ function SettingsDialog({ settings, setSettings }: Props) { })) } /> + + + + + setSettings((s) => ({ + ...s, + screenshotOneApiKey: e.target.value, + })) + } + /> + diff --git a/frontend/src/components/UrlInputSection.tsx b/frontend/src/components/UrlInputSection.tsx new file mode 100644 index 0000000..4c9c1fb --- /dev/null +++ b/frontend/src/components/UrlInputSection.tsx @@ -0,0 +1,83 @@ +import { useState } from "react"; +import { HTTP_BACKEND_URL } from "../config"; +import { Button } from "./ui/button"; +import { Input } from "./ui/input"; +import { toast } from "react-hot-toast"; + +interface Props { + screenshotOneApiKey: string | null; + doCreate: (urls: string[]) => void; +} + +export function UrlInputSection({ doCreate, screenshotOneApiKey }: Props) { + const [isLoading, setIsLoading] = useState(false); + const [referenceUrl, setReferenceUrl] = useState(""); + const isDisabled = !screenshotOneApiKey; + + async function takeScreenshot() { + if (!screenshotOneApiKey) { + toast.error("Please add a Screenshot API key in the settings dialog"); + return; + } + + if (!referenceUrl) { + toast.error("Please enter a URL"); + return; + } + + if (referenceUrl) { + try { + setIsLoading(true); + const response = await fetch(`${HTTP_BACKEND_URL}/api/screenshot`, { + method: "POST", + body: JSON.stringify({ + url: referenceUrl, + apiKey: screenshotOneApiKey, + }), + headers: { + "Content-Type": "application/json", + }, + }); + + if (!response.ok) { + throw new Error("Failed to capture screenshot"); + } + + const res = await response.json(); + doCreate([res.url]); + } catch (error) { + console.error(error); + toast.error( + "Failed to capture screenshot. Look at the console and your backend logs for more details." + ); + } finally { + setIsLoading(false); + } + } + } + + return ( +
+
Or screenshot a URL...
+ setReferenceUrl(e.target.value)} + value={referenceUrl} + /> + + {isDisabled && ( +
+ + To screenshot a URL, add a{" "} + + ScreenshotOne API key + {" "} + in the settings dialog. + +
+ )} +
+ ); +} diff --git a/frontend/src/config.ts b/frontend/src/config.ts index 054f6ed..0cb949c 100644 --- a/frontend/src/config.ts +++ b/frontend/src/config.ts @@ -1,3 +1,6 @@ // Default to false if set to anything other than "true" or unset export const IS_RUNNING_ON_CLOUD = import.meta.env.VITE_IS_DEPLOYED === "true" || false; + +export const HTTP_BACKEND_URL = + import.meta.env.VITE_HTTP_BACKEND_URL || "http://127.0.0.1:7001"; diff --git a/frontend/src/types.ts b/frontend/src/types.ts index bc66547..6f44e9f 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -1,5 +1,6 @@ export interface Settings { openAiApiKey: string | null; + screenshotOneApiKey: string | null; isImageGenerationEnabled: boolean; editorTheme: string; } From 103d1ce12ce56c273ae531d4e1dfd80ef3a62144 Mon Sep 17 00:00:00 2001 From: Abi Raja Date: Mon, 20 Nov 2023 11:50:06 -0500 Subject: [PATCH 2/4] centralize config in config.ts --- frontend/src/config.ts | 3 +++ frontend/src/generateCode.ts | 3 +-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/frontend/src/config.ts b/frontend/src/config.ts index 0cb949c..7a6251c 100644 --- a/frontend/src/config.ts +++ b/frontend/src/config.ts @@ -2,5 +2,8 @@ export const IS_RUNNING_ON_CLOUD = import.meta.env.VITE_IS_DEPLOYED === "true" || false; +export const WS_BACKEND_URL = + import.meta.env.VITE_WS_BACKEND_URL || "ws://127.0.0.1:7001"; + export const HTTP_BACKEND_URL = import.meta.env.VITE_HTTP_BACKEND_URL || "http://127.0.0.1:7001"; diff --git a/frontend/src/generateCode.ts b/frontend/src/generateCode.ts index 3b0f3d6..6fea92d 100644 --- a/frontend/src/generateCode.ts +++ b/frontend/src/generateCode.ts @@ -1,7 +1,6 @@ import toast from "react-hot-toast"; +import { WS_BACKEND_URL } from "./config"; -const WS_BACKEND_URL = - import.meta.env.VITE_WS_BACKEND_URL || "ws://127.0.0.1:7001"; const ERROR_MESSAGE = "Error generating code. Check the Developer Console for details. Feel free to open a Github issue"; From e7c05d7d23b907e5129c24f0c25087a344f386ae Mon Sep 17 00:00:00 2001 From: Abi Raja Date: Mon, 20 Nov 2023 11:54:31 -0500 Subject: [PATCH 3/4] fix screenshotone links --- frontend/src/components/SettingsDialog.tsx | 9 ++++++++- frontend/src/components/UrlInputSection.tsx | 6 +++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/SettingsDialog.tsx b/frontend/src/components/SettingsDialog.tsx index 58caded..9b46b90 100644 --- a/frontend/src/components/SettingsDialog.tsx +++ b/frontend/src/components/SettingsDialog.tsx @@ -78,7 +78,14 @@ function SettingsDialog({ settings, setSettings }: Props) { diff --git a/frontend/src/components/UrlInputSection.tsx b/frontend/src/components/UrlInputSection.tsx index 4c9c1fb..20e9c54 100644 --- a/frontend/src/components/UrlInputSection.tsx +++ b/frontend/src/components/UrlInputSection.tsx @@ -71,7 +71,11 @@ export function UrlInputSection({ doCreate, screenshotOneApiKey }: Props) {
To screenshot a URL, add a{" "} - + ScreenshotOne API key {" "} in the settings dialog. From d7187b0a1b99f35c738dd135c937d70c264d7aba Mon Sep 17 00:00:00 2001 From: Abi Raja Date: Mon, 20 Nov 2023 12:01:03 -0500 Subject: [PATCH 4/4] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 80964f8..dba1217 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ See the [Examples](#examples) section below for more demos. ## 🌟 Recent Updates +- Nov 20 - Paste in a URL to screenshot and replicate (requires [ScreenshotOne free API key](https://screenshotone.com?via=screenshot-to-code)) - Nov 19 - Support for dark/light code editor theme - thanks https://github.com/kachbit - Nov 16 - Added a setting to disable DALL-E image generation if you don't need that - Nov 16 - View code directly within the app