From 0dcefb34fbe9c66c6883e4f2e4a1bb6466f6fce0 Mon Sep 17 00:00:00 2001 From: Abi Raja Date: Thu, 16 Nov 2023 10:51:46 -0500 Subject: [PATCH 01/29] remove readme in pyproject.toml --- backend/pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 9983a61..9aa721d 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -4,7 +4,6 @@ version = "0.1.0" description = "" authors = ["Abi Raja "] license = "MIT" -readme = "README.md" [tool.poetry.dependencies] python = "^3.10" From e11b9c929b275d1056ee22b35d5569837b4eac88 Mon Sep 17 00:00:00 2001 From: Abi Raja Date: Thu, 16 Nov 2023 11:33:11 -0500 Subject: [PATCH 02/29] Add FAQ section to README --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 538cef6..83410e2 100644 --- a/README.md +++ b/README.md @@ -38,9 +38,11 @@ Open http://localhost:5173 to use the app. If you prefer to run the backend on a different port, update VITE_WS_BACKEND_URL in `frontend/.env.local` -## Feedback +## FAQs -If you have feature requests, bug reports or other feedback, open an issue or ping me on [Twitter](https://twitter.com/_abi_). +* **I'm running into an error when setting up the backend. How can I fix it?** [Try this](https://github.com/abi/screenshot-to-code/issues/3#issuecomment-1814777959). If that still doesn't work, open an issue. +* **How do I get an OpenAI API key that has the GPT4 Vision model available?** Create an OpenAI account. And then, you need to buy at least $1 worth of credit on the [Billing dashboard](https://platform.openai.com/account/billing/overview). +* **How can I provide feedback?** For feedback, feature requests and bug reports, open an issue or ping me on [Twitter](https://twitter.com/_abi_). ## Examples From 67f710521d46fdae3a6d9456faa768efb03c5d7d Mon Sep 17 00:00:00 2001 From: Abi Raja Date: Thu, 16 Nov 2023 11:56:54 -0500 Subject: [PATCH 03/29] support desktop and mobile preview --- frontend/src/App.tsx | 28 ++++++++++++++++++-------- frontend/src/components/CodeMirror.tsx | 2 +- frontend/src/components/Preview.tsx | 16 +++++++++++---- 3 files changed, 33 insertions(+), 13 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 452e0be..c34d5cd 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -5,7 +5,13 @@ import Preview from "./components/Preview"; import { CodeGenerationParams, generateCode } from "./generateCode"; import Spinner from "./components/Spinner"; import classNames from "classnames"; -import { FaCode, FaDownload, FaEye, FaUndo } from "react-icons/fa"; +import { + FaCode, + FaDesktop, + FaDownload, + FaMobile, + FaUndo, +} from "react-icons/fa"; import { Button } from "@/components/ui/button"; import { Textarea } from "@/components/ui/textarea"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "./components/ui/tabs"; @@ -83,7 +89,7 @@ function App() { } return ( -
+

Screenshot to Code

@@ -181,11 +187,14 @@ function App() { {(appState === "CODING" || appState === "CODE_READY") && (
- -
+ +
- - Preview + + Desktop + + + Mobile @@ -193,8 +202,11 @@ function App() {
- - + + + + + diff --git a/frontend/src/components/CodeMirror.tsx b/frontend/src/components/CodeMirror.tsx index 7c6d9df..3ba1c51 100644 --- a/frontend/src/components/CodeMirror.tsx +++ b/frontend/src/components/CodeMirror.tsx @@ -60,6 +60,6 @@ function CodeMirror({ code }: Props) { } }, [code]); - return
; + return
; } export default CodeMirror; diff --git a/frontend/src/components/Preview.tsx b/frontend/src/components/Preview.tsx index 4728a73..de0134f 100644 --- a/frontend/src/components/Preview.tsx +++ b/frontend/src/components/Preview.tsx @@ -1,19 +1,27 @@ +import classNames from "classnames"; import useThrottle from "../hooks/useThrottle"; interface Props { code: string; + device: "mobile" | "desktop"; } -function Preview({ code }: Props) { +function Preview({ code, device }: Props) { const throttledCode = useThrottle(code, 200); return ( -
+
); From d1945524308a0a5fc81d1da8e848e652d79aa75a Mon Sep 17 00:00:00 2001 From: Abi Raja Date: Thu, 16 Nov 2023 12:21:16 -0500 Subject: [PATCH 04/29] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 83410e2..919a5b9 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ See Examples section below for more demos. ## Updates +* Nov 16 - Toggle between desktop & mobile preview * Nov 16 - View code directly within the app * Nov 15 - 🔥 You can now instruct the AI to update the code as you wish. Useful if the AI messed up some styles or missed a section. From ebedef2bef4454868e719b3fd1056b7ec03b9d5d Mon Sep 17 00:00:00 2001 From: Abi Raja Date: Thu, 16 Nov 2023 12:26:29 -0500 Subject: [PATCH 05/29] Update README.md --- README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 919a5b9..88954c8 100644 --- a/README.md +++ b/README.md @@ -47,10 +47,16 @@ If you prefer to run the backend on a different port, update VITE_WS_BACKEND_URL ## Examples -Hacker News but it gets the colors wrong at first so we nudge it +**Hacker News** but it gets the colors wrong at first so we nudge it https://github.com/abi/screenshot-to-code/assets/23818/3fec0f77-44e8-4fb3-a769-ac7410315e5d +**NYTimes** + +Screenshot 2023-11-16 at 12 06 09 PM +Screenshot 2023-11-16 at 12 07 25 PM + + ## Hosted Version From 640f41619c2445b78e6145e59df2dbfd4ca7d3b3 Mon Sep 17 00:00:00 2001 From: Abi Raja Date: Thu, 16 Nov 2023 14:43:50 -0500 Subject: [PATCH 06/29] prettify code after image generation --- backend/image_generation.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/image_generation.py b/backend/image_generation.py index 5d4b81c..336cc6e 100644 --- a/backend/image_generation.py +++ b/backend/image_generation.py @@ -110,4 +110,5 @@ async def generate_images(code, image_cache): print("Image generation failed for alt text:" + img.get("alt")) # Return the modified HTML - return str(soup) + # (need to prettify it because BeautifulSoup messes up the formatting) + return soup.prettify() From 68dd33cf064c29d90a8e333040778a06abd19329 Mon Sep 17 00:00:00 2001 From: Abi Raja Date: Thu, 16 Nov 2023 15:11:54 -0500 Subject: [PATCH 07/29] return early if there are no images to replace --- backend/image_generation.py | 4 ++ backend/main.py | 3 +- backend/mock.py | 73 +++++++++++++++++++++++++++++++++++-- 3 files changed, 76 insertions(+), 4 deletions(-) diff --git a/backend/image_generation.py b/backend/image_generation.py index 336cc6e..e1230a8 100644 --- a/backend/image_generation.py +++ b/backend/image_generation.py @@ -82,6 +82,10 @@ async def generate_images(code, image_cache): # Remove duplicates prompts = list(set(alts)) + # Return early if there are no images to replace + if len(prompts) == 0: + return code + # Generate images results = await process_tasks(prompts) diff --git a/backend/main.py b/backend/main.py index 5abd333..3980ffe 100644 --- a/backend/main.py +++ b/backend/main.py @@ -11,7 +11,7 @@ from datetime import datetime from fastapi import FastAPI, WebSocket from llm import stream_openai_response -from mock import MOCK_HTML, mock_completion +from mock import mock_completion from image_generation import create_alt_url_mapping, generate_images from prompts import assemble_prompt @@ -41,6 +41,7 @@ async def stream_code_test(websocket: WebSocket): params = await websocket.receive_json() + print("generating code...") await websocket.send_json({"type": "status", "value": "Generating code..."}) async def process_chunk(content): diff --git a/backend/mock.py b/backend/mock.py index ec26339..90dc7d3 100644 --- a/backend/mock.py +++ b/backend/mock.py @@ -2,7 +2,7 @@ import asyncio async def mock_completion(process_chunk): - code_to_return = MOCK_HTML_2 + code_to_return = NO_IMAGES_NYTIMES_MOCK_CODE for i in range(0, len(code_to_return), 10): await process_chunk(code_to_return[i : i + 10]) @@ -11,7 +11,7 @@ async def mock_completion(process_chunk): return code_to_return -MOCK_HTML = """ +APPLE_MOCK_CODE = """ @@ -68,7 +68,7 @@ MOCK_HTML = """ """ -MOCK_HTML_2 = """ +NYTIMES_MOCK_CODE = """ @@ -138,3 +138,70 @@ MOCK_HTML_2 = """ """ + +NO_IMAGES_NYTIMES_MOCK_CODE = """ + + + + + The New York Times - News + + + + + + +
+
+
+
+ + +
Tuesday, November 14, 2023
Today's Paper
+
+
+ +
Account
+
+
+ +
+
+
+
+
+
+

Israeli Military Raids Gaza’s Largest Hospital

+

Israeli troops have entered the Al-Shifa Hospital complex, where conditions have grown dire and Israel says Hamas fighters are embedded.

+ See more updates +
+ +
+
+
+

From Elvis to Elopements, the Evolution of the Las Vegas Wedding

+

The glittering city that attracts thousands of couples seeking unconventional nuptials has grown beyond the drive-through wedding.

+ 8 MIN READ +
+ +
+
+
+
+
+ + +""" From 371dddda3f23503bc48e1ee77e5ecab64f187671 Mon Sep 17 00:00:00 2001 From: Abi Raja Date: Thu, 16 Nov 2023 17:37:03 -0500 Subject: [PATCH 08/29] add a settings dialog to allow you to disable the image generation --- backend/main.py | 17 +- frontend/package.json | 3 + frontend/src/App.tsx | 16 +- frontend/src/components/SettingsDialog.tsx | 46 ++++++ frontend/src/components/ui/dialog.tsx | 120 ++++++++++++++ frontend/src/components/ui/label.tsx | 24 +++ frontend/src/components/ui/switch.tsx | 27 ++++ frontend/src/generateCode.ts | 3 +- frontend/src/types.ts | 3 + frontend/yarn.lock | 174 ++++++++++++++++++++- 10 files changed, 424 insertions(+), 9 deletions(-) create mode 100644 frontend/src/components/SettingsDialog.tsx create mode 100644 frontend/src/components/ui/dialog.tsx create mode 100644 frontend/src/components/ui/label.tsx create mode 100644 frontend/src/components/ui/switch.tsx create mode 100644 frontend/src/types.ts diff --git a/backend/main.py b/backend/main.py index 3980ffe..42b3f2f 100644 --- a/backend/main.py +++ b/backend/main.py @@ -41,6 +41,12 @@ async def stream_code_test(websocket: WebSocket): params = await websocket.receive_json() + should_generate_images = ( + params["isImageGenerationEnabled"] + if "isImageGenerationEnabled" in params + else True + ) + print("generating code...") await websocket.send_json({"type": "status", "value": "Generating code..."}) @@ -73,11 +79,14 @@ async def stream_code_test(websocket: WebSocket): # Write the messages dict into a log so that we can debug later write_logs(prompt_messages, completion) - # Generate images - await websocket.send_json({"type": "status", "value": "Generating images..."}) - try: - updated_html = await generate_images(completion, image_cache=image_cache) + if should_generate_images: + await websocket.send_json( + {"type": "status", "value": "Generating images..."} + ) + updated_html = await generate_images(completion, image_cache=image_cache) + else: + updated_html = completion await websocket.send_json({"type": "setCode", "value": updated_html}) await websocket.send_json( {"type": "status", "value": "Code generation complete."} diff --git a/frontend/package.json b/frontend/package.json index 2b37f02..c5933e5 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -11,9 +11,12 @@ }, "dependencies": { "@codemirror/lang-html": "^6.4.6", + "@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-icons": "^1.3.0", + "@radix-ui/react-label": "^2.0.2", "@radix-ui/react-separator": "^1.0.3", "@radix-ui/react-slot": "^1.0.2", + "@radix-ui/react-switch": "^1.0.3", "@radix-ui/react-tabs": "^1.0.4", "class-variance-authority": "^0.7.0", "classnames": "^2.3.2", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index c34d5cd..d5560be 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -16,6 +16,8 @@ import { Button } from "@/components/ui/button"; import { Textarea } from "@/components/ui/textarea"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "./components/ui/tabs"; import CodeMirror from "./components/CodeMirror"; +import SettingsDialog from "./components/SettingsDialog"; +import { Settings } from "./types"; function App() { const [appState, setAppState] = useState<"INITIAL" | "CODING" | "CODE_READY">( @@ -26,6 +28,9 @@ function App() { const [executionConsole, setExecutionConsole] = useState([]); const [updateInstruction, setUpdateInstruction] = useState(""); const [history, setHistory] = useState([]); + const [settings, setSettings] = useState({ + isImageGenerationEnabled: true, + }); const downloadCode = () => { // Create a blob from the generated code @@ -55,8 +60,12 @@ function App() { function doGenerateCode(params: CodeGenerationParams) { setExecutionConsole([]); setAppState("CODING"); + + // Merge settings with params + const updatedParams = { ...params, ...settings }; + generateCode( - params, + updatedParams, (token) => setGeneratedCode((prev) => prev + token), (code) => setGeneratedCode(code), (line) => setExecutionConsole((prev) => [...prev, line]), @@ -92,7 +101,10 @@ function App() {
-

Screenshot to Code

+
+

Screenshot to Code

+ +
{appState === "INITIAL" && (

Drag & drop a screenshot to get started. diff --git a/frontend/src/components/SettingsDialog.tsx b/frontend/src/components/SettingsDialog.tsx new file mode 100644 index 0000000..2e109ed --- /dev/null +++ b/frontend/src/components/SettingsDialog.tsx @@ -0,0 +1,46 @@ +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { FaCog } from "react-icons/fa"; +import { Settings } from "../types"; +import { Switch } from "./ui/switch"; +import { Label } from "./ui/label"; + +interface Props { + settings: Settings; + setSettings: React.Dispatch>; +} + +function SettingsDialog({ settings, setSettings }: Props) { + return ( + + + + + + + Settings +
+ + + setSettings((s) => ({ + ...s, + isImageGenerationEnabled: !s.isImageGenerationEnabled, + })) + } + /> +
+
+
+
+ ); +} + +export default SettingsDialog; diff --git a/frontend/src/components/ui/dialog.tsx b/frontend/src/components/ui/dialog.tsx new file mode 100644 index 0000000..776ca68 --- /dev/null +++ b/frontend/src/components/ui/dialog.tsx @@ -0,0 +1,120 @@ +import * as React from "react" +import * as DialogPrimitive from "@radix-ui/react-dialog" +import { Cross2Icon } from "@radix-ui/react-icons" + +import { cn } from "@/lib/utils" + +const Dialog = DialogPrimitive.Root + +const DialogTrigger = DialogPrimitive.Trigger + +const DialogPortal = DialogPrimitive.Portal + +const DialogClose = DialogPrimitive.Close + +const DialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName + +const DialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + {children} + + + Close + + + +)) +DialogContent.displayName = DialogPrimitive.Content.displayName + +const DialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogHeader.displayName = "DialogHeader" + +const DialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogFooter.displayName = "DialogFooter" + +const DialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogTitle.displayName = DialogPrimitive.Title.displayName + +const DialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogDescription.displayName = DialogPrimitive.Description.displayName + +export { + Dialog, + DialogPortal, + DialogOverlay, + DialogTrigger, + DialogClose, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, +} diff --git a/frontend/src/components/ui/label.tsx b/frontend/src/components/ui/label.tsx new file mode 100644 index 0000000..683faa7 --- /dev/null +++ b/frontend/src/components/ui/label.tsx @@ -0,0 +1,24 @@ +import * as React from "react" +import * as LabelPrimitive from "@radix-ui/react-label" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const labelVariants = cva( + "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" +) + +const Label = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & + VariantProps +>(({ className, ...props }, ref) => ( + +)) +Label.displayName = LabelPrimitive.Root.displayName + +export { Label } diff --git a/frontend/src/components/ui/switch.tsx b/frontend/src/components/ui/switch.tsx new file mode 100644 index 0000000..f15014b --- /dev/null +++ b/frontend/src/components/ui/switch.tsx @@ -0,0 +1,27 @@ +import * as React from "react"; +import * as SwitchPrimitives from "@radix-ui/react-switch"; + +import { cn } from "@/lib/utils"; + +const Switch = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)); +Switch.displayName = SwitchPrimitives.Root.displayName; + +export { Switch }; diff --git a/frontend/src/generateCode.ts b/frontend/src/generateCode.ts index 997bd45..06ecc5b 100644 --- a/frontend/src/generateCode.ts +++ b/frontend/src/generateCode.ts @@ -3,12 +3,13 @@ import toast from "react-hot-toast"; const WS_BACKEND_URL = import.meta.env.VITE_WS_BACKEND_URL || "ws://127.0.0.1:7000"; const ERROR_MESSAGE = - "Error generating code. Check the Developer Console for details. Feel free to open a Github ticket"; + "Error generating code. Check the Developer Console for details. Feel free to open a Github issue"; export interface CodeGenerationParams { generationType: "create" | "update"; image: string; history?: string[]; + // isImageGenerationEnabled: boolean; // TODO: Merge with Settings type in types.ts } export function generateCode( diff --git a/frontend/src/types.ts b/frontend/src/types.ts new file mode 100644 index 0000000..d9fe60b --- /dev/null +++ b/frontend/src/types.ts @@ -0,0 +1,3 @@ +export interface Settings { + isImageGenerationEnabled: boolean; +} diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 20ade74..2109c60 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -618,6 +618,27 @@ dependencies: "@babel/runtime" "^7.13.10" +"@radix-ui/react-dialog@^1.0.5": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@radix-ui/react-dialog/-/react-dialog-1.0.5.tgz#71657b1b116de6c7a0b03242d7d43e01062c7300" + integrity sha512-GjWJX/AUpB703eEBanuBnIWdIXg6NvJFCXcNlSZk4xdszCdhrJgBoUd1cGk67vFO+WdA2pfI/plOpqz/5GUP6Q== + 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-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-direction@1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@radix-ui/react-direction/-/react-direction-1.0.1.tgz#9cb61bf2ccf568f3421422d182637b7f47596c9b" @@ -625,6 +646,35 @@ dependencies: "@babel/runtime" "^7.13.10" +"@radix-ui/react-dismissable-layer@1.0.5": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.0.5.tgz#3f98425b82b9068dfbab5db5fff3df6ebf48b9d4" + integrity sha512-aJeDjQhywg9LBu2t/At58hCvr7pEm0o2Ke1x33B+MhjNmmZ17sy4KImo0KPLgsnc/zN7GPdce8Cnn0SWvwZO7g== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/primitive" "1.0.1" + "@radix-ui/react-compose-refs" "1.0.1" + "@radix-ui/react-primitive" "1.0.3" + "@radix-ui/react-use-callback-ref" "1.0.1" + "@radix-ui/react-use-escape-keydown" "1.0.3" + +"@radix-ui/react-focus-guards@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-guards/-/react-focus-guards-1.0.1.tgz#1ea7e32092216b946397866199d892f71f7f98ad" + integrity sha512-Rect2dWbQ8waGzhMavsIbmSVCgYxkXLxxR3ZvCX79JOglzdEy4JXMb98lq4hPxUbLr77nP0UOGf4rcMU+s1pUA== + dependencies: + "@babel/runtime" "^7.13.10" + +"@radix-ui/react-focus-scope@1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-scope/-/react-focus-scope-1.0.4.tgz#2ac45fce8c5bb33eb18419cdc1905ef4f1906525" + integrity sha512-sL04Mgvf+FmyvZeYfNu1EPAaaxD+aw7cYeIB9L9Fvq8+urhltTRaEo5ysKOpHuKPclsZcSUMKlN05x4u+CINpA== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-compose-refs" "1.0.1" + "@radix-ui/react-primitive" "1.0.3" + "@radix-ui/react-use-callback-ref" "1.0.1" + "@radix-ui/react-icons@^1.3.0": version "1.3.0" resolved "https://registry.yarnpkg.com/@radix-ui/react-icons/-/react-icons-1.3.0.tgz#c61af8f323d87682c5ca76b856d60c2312dbcb69" @@ -638,6 +688,22 @@ "@babel/runtime" "^7.13.10" "@radix-ui/react-use-layout-effect" "1.0.1" +"@radix-ui/react-label@^2.0.2": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@radix-ui/react-label/-/react-label-2.0.2.tgz#9c72f1d334aac996fdc27b48a8bdddd82108fb6d" + integrity sha512-N5ehvlM7qoTLx7nWPodsPYPgMzA5WM8zZChQg8nyFJKnDO5WHdba1vv5/H6IO5LtJMfD2Q3wh1qHFGNtK0w3bQ== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-primitive" "1.0.3" + +"@radix-ui/react-portal@1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@radix-ui/react-portal/-/react-portal-1.0.4.tgz#df4bfd353db3b1e84e639e9c63a5f2565fb00e15" + integrity sha512-Qki+C/EuGUVCQTOTD5vzJzJuMUlewbzuKyUy+/iHM2uwGiru9gZeBJtHAPKAEkB5KWGi9mP/CHKcY0wt1aW45Q== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-primitive" "1.0.3" + "@radix-ui/react-presence@1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@radix-ui/react-presence/-/react-presence-1.0.1.tgz#491990ba913b8e2a5db1b06b203cb24b5cdef9ba" @@ -687,6 +753,20 @@ "@babel/runtime" "^7.13.10" "@radix-ui/react-compose-refs" "1.0.1" +"@radix-ui/react-switch@^1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@radix-ui/react-switch/-/react-switch-1.0.3.tgz#6119f16656a9eafb4424c600fdb36efa5ec5837e" + integrity sha512-mxm87F88HyHztsI7N+ZUmEoARGkC22YVW5CaC+Byc+HRpuvCrOBPTAnXgf+tZ/7i0Sg/eOePGdMhUKhPaQEqow== + 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-primitive" "1.0.3" + "@radix-ui/react-use-controllable-state" "1.0.1" + "@radix-ui/react-use-previous" "1.0.1" + "@radix-ui/react-use-size" "1.0.1" + "@radix-ui/react-tabs@^1.0.4": version "1.0.4" resolved "https://registry.yarnpkg.com/@radix-ui/react-tabs/-/react-tabs-1.0.4.tgz#993608eec55a5d1deddd446fa9978d2bc1053da2" @@ -717,6 +797,14 @@ "@babel/runtime" "^7.13.10" "@radix-ui/react-use-callback-ref" "1.0.1" +"@radix-ui/react-use-escape-keydown@1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.0.3.tgz#217b840c250541609c66f67ed7bab2b733620755" + integrity sha512-vyL82j40hcFicA+M4Ex7hVkB9vHgSse1ZWomAqV2Je3RleKGO5iM8KMOEtfoSB0PnIelMd2lATjTGMYqN5ylTg== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-use-callback-ref" "1.0.1" + "@radix-ui/react-use-layout-effect@1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.1.tgz#be8c7bc809b0c8934acf6657b577daf948a75399" @@ -724,6 +812,21 @@ dependencies: "@babel/runtime" "^7.13.10" +"@radix-ui/react-use-previous@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-previous/-/react-use-previous-1.0.1.tgz#b595c087b07317a4f143696c6a01de43b0d0ec66" + integrity sha512-cV5La9DPwiQ7S0gf/0qiD6YgNqM5Fk97Kdrlc5yBcrF3jyEZQwm7vYFqMo4IfeHgJXsRaMvLABFtd0OVEmZhDw== + dependencies: + "@babel/runtime" "^7.13.10" + +"@radix-ui/react-use-size@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-size/-/react-use-size-1.0.1.tgz#1c5f5fea940a7d7ade77694bb98116fb49f870b2" + integrity sha512-ibay+VqrgcaI6veAojjofPATwledXiSmX+C0KrBk/xgpX9rBzPV3OsfwlhQdUOFbh+LKQorLYT+xTXW9V8yd0g== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-use-layout-effect" "1.0.1" + "@types/babel__core@^7.20.3": version "7.20.4" resolved "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.4.tgz" @@ -970,6 +1073,13 @@ argparse@^2.0.1: resolved "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz" integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== +aria-hidden@^1.1.1: + version "1.2.3" + resolved "https://registry.yarnpkg.com/aria-hidden/-/aria-hidden-1.2.3.tgz#14aeb7fb692bbb72d69bebfa47279c1fd725e954" + integrity sha512-xcLxITLe2HYa1cnYnwCjkOO1PqUHQpozB8x9AR0OgWN2woOBi5kSDVxKfd0b7sb1hw5qFeJhXm9H1nu3xSfLeQ== + dependencies: + tslib "^2.0.0" + array-union@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz" @@ -1184,6 +1294,11 @@ deep-is@^0.1.3: resolved "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz" integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== +detect-node-es@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/detect-node-es/-/detect-node-es-1.1.0.tgz#163acdf643330caa0b4cd7c21e7ee7755d6fa493" + integrity sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ== + didyoumean@^1.2.2: version "1.2.2" resolved "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz" @@ -1466,6 +1581,11 @@ gensync@^1.0.0-beta.2: resolved "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz" integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== +get-nonce@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/get-nonce/-/get-nonce-1.0.1.tgz#fdf3f0278073820d2ce9426c18f07481b1e0cdf3" + integrity sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q== + glob-parent@^5.1.2, glob-parent@~5.1.2: version "5.1.2" resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz" @@ -1591,6 +1711,13 @@ inherits@2: resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== +invariant@^2.2.4: + version "2.2.4" + resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" + integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA== + dependencies: + loose-envify "^1.0.0" + is-binary-path@~2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz" @@ -1730,7 +1857,7 @@ lodash.pick@^4.4.0: resolved "https://registry.npmjs.org/lodash.pick/-/lodash.pick-4.4.0.tgz" integrity sha512-hXt6Ul/5yWjfklSGvLQl8vM//l3FtyHZeuelpzK6mm99pNvN9yTDruNZPEJZD1oWrqo+izBmB7oUfWgcCX7s4Q== -loose-envify@^1.1.0, loose-envify@^1.4.0: +loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz" integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== @@ -2028,6 +2155,34 @@ react-refresh@^0.14.0: resolved "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz" integrity sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ== +react-remove-scroll-bar@^2.3.3: + version "2.3.4" + resolved "https://registry.yarnpkg.com/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.4.tgz#53e272d7a5cb8242990c7f144c44d8bd8ab5afd9" + integrity sha512-63C4YQBUt0m6ALadE9XV56hV8BgJWDmmTPY758iIJjfQKt2nYwoUrPk0LXRXcB/yIj82T1/Ixfdpdk68LwIB0A== + dependencies: + react-style-singleton "^2.2.1" + tslib "^2.0.0" + +react-remove-scroll@2.5.5: + version "2.5.5" + resolved "https://registry.yarnpkg.com/react-remove-scroll/-/react-remove-scroll-2.5.5.tgz#1e31a1260df08887a8a0e46d09271b52b3a37e77" + integrity sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw== + dependencies: + react-remove-scroll-bar "^2.3.3" + react-style-singleton "^2.2.1" + tslib "^2.1.0" + use-callback-ref "^1.3.0" + use-sidecar "^1.1.2" + +react-style-singleton@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/react-style-singleton/-/react-style-singleton-2.2.1.tgz#f99e420492b2d8f34d38308ff660b60d0b1205b4" + integrity sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g== + dependencies: + get-nonce "^1.0.0" + invariant "^2.2.4" + tslib "^2.0.0" + react@^18.2.0: version "18.2.0" resolved "https://registry.npmjs.org/react/-/react-18.2.0.tgz" @@ -2275,7 +2430,7 @@ ts-interface-checker@^0.1.9: resolved "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz" integrity sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA== -tslib@^2.4.0: +tslib@^2.0.0, tslib@^2.1.0, tslib@^2.4.0: version "2.6.2" resolved "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz" integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== @@ -2327,6 +2482,21 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" +use-callback-ref@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/use-callback-ref/-/use-callback-ref-1.3.0.tgz#772199899b9c9a50526fedc4993fc7fa1f7e32d5" + integrity sha512-3FT9PRuRdbB9HfXhEq35u4oZkvpJ5kuYbpqhCfmiZyReuRgpnhDlbr2ZEnnuS0RrJAPn6l23xjFg9kpDM+Ms7w== + dependencies: + tslib "^2.0.0" + +use-sidecar@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/use-sidecar/-/use-sidecar-1.1.2.tgz#2f43126ba2d7d7e117aa5855e5d8f0276dfe73c2" + integrity sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw== + dependencies: + detect-node-es "^1.1.0" + tslib "^2.0.0" + util-deprecate@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" From e27c278a797010256dc1c096b09d506a661d3f5f Mon Sep 17 00:00:00 2001 From: Abi Raja Date: Thu, 16 Nov 2023 17:41:24 -0500 Subject: [PATCH 09/29] update copy for Placeholder Image Generation setting --- frontend/src/components/SettingsDialog.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/SettingsDialog.tsx b/frontend/src/components/SettingsDialog.tsx index 2e109ed..294ec59 100644 --- a/frontend/src/components/SettingsDialog.tsx +++ b/frontend/src/components/SettingsDialog.tsx @@ -25,7 +25,12 @@ function SettingsDialog({ settings, setSettings }: Props) { Settings
- + Date: Thu, 16 Nov 2023 17:42:03 -0500 Subject: [PATCH 10/29] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 88954c8..bbfd610 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ See Examples section below for more demos. ## Updates +* Nov 16 - Added a setting to disable DALL-E image generation if you don't need that * Nov 16 - Toggle between desktop & mobile preview * Nov 16 - View code directly within the app * Nov 15 - 🔥 You can now instruct the AI to update the code as you wish. Useful if the AI messed up some styles or missed a section. From ed1143e9b9ab4a1d56b1e7767b6c87db08491743 Mon Sep 17 00:00:00 2001 From: Abi Raja Date: Thu, 16 Nov 2023 17:43:00 -0500 Subject: [PATCH 11/29] Update README.md --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index bbfd610..df5e72e 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,6 @@ See Examples section below for more demos. ## Updates * Nov 16 - Added a setting to disable DALL-E image generation if you don't need that -* Nov 16 - Toggle between desktop & mobile preview * Nov 16 - View code directly within the app * Nov 15 - 🔥 You can now instruct the AI to update the code as you wish. Useful if the AI messed up some styles or missed a section. From ee9b40d99040ba6a8f59e1ebc80e0c4ecb812c3d Mon Sep 17 00:00:00 2001 From: Abi Raja Date: Thu, 16 Nov 2023 18:12:07 -0500 Subject: [PATCH 12/29] support setting openai api key on the client side --- backend/image_generation.py | 12 ++--- backend/llm.py | 6 ++- backend/main.py | 26 ++++++++- frontend/src/App.tsx | 1 + frontend/src/components/SettingsDialog.tsx | 62 +++++++++++++++------- frontend/src/components/ui/input.tsx | 25 +++++++++ frontend/src/generateCode.ts | 3 ++ frontend/src/types.ts | 1 + 8 files changed, 108 insertions(+), 28 deletions(-) create mode 100644 frontend/src/components/ui/input.tsx diff --git a/backend/image_generation.py b/backend/image_generation.py index e1230a8..080334f 100644 --- a/backend/image_generation.py +++ b/backend/image_generation.py @@ -5,8 +5,8 @@ from openai import AsyncOpenAI from bs4 import BeautifulSoup -async def process_tasks(prompts): - tasks = [generate_image(prompt) for prompt in prompts] +async def process_tasks(prompts, api_key): + tasks = [generate_image(prompt, api_key) for prompt in prompts] results = await asyncio.gather(*tasks, return_exceptions=True) processed_results = [] @@ -20,8 +20,8 @@ async def process_tasks(prompts): return processed_results -async def generate_image(prompt): - client = AsyncOpenAI(api_key=os.environ.get("OPENAI_API_KEY")) +async def generate_image(prompt, api_key): + client = AsyncOpenAI(api_key=api_key) 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, image_cache): +async def generate_images(code, api_key, image_cache): # Find all images soup = BeautifulSoup(code, "html.parser") images = soup.find_all("img") @@ -87,7 +87,7 @@ async def generate_images(code, image_cache): return code # Generate images - results = await process_tasks(prompts) + results = await process_tasks(prompts, api_key) # Create a dict mapping alt text to image URL mapped_image_urls = dict(zip(prompts, results)) diff --git a/backend/llm.py b/backend/llm.py index 686b008..b52c3c9 100644 --- a/backend/llm.py +++ b/backend/llm.py @@ -4,10 +4,12 @@ from openai import AsyncOpenAI MODEL_GPT_4_VISION = "gpt-4-vision-preview" -client = AsyncOpenAI(api_key=os.environ.get("OPENAI_API_KEY")) +async def stream_openai_response( + messages, api_key: str, callback: Callable[[str], Awaitable[None]] +): + client = AsyncOpenAI(api_key=api_key) -async def stream_openai_response(messages, callback: Callable[[str], Awaitable[None]]): model = MODEL_GPT_4_VISION # Base parameters diff --git a/backend/main.py b/backend/main.py index 42b3f2f..a3a74ad 100644 --- a/backend/main.py +++ b/backend/main.py @@ -41,6 +41,25 @@ async def stream_code_test(websocket: WebSocket): params = await websocket.receive_json() + # 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") + else: + openai_api_key = os.environ.get("OPENAI_API_KEY") + print("Using OpenAI API key from environment variable") + + if not openai_api_key: + print("OpenAI API key not found") + await websocket.send_json( + { + "type": "error", + "value": "OpenAI API key found. Please add your API key in the settings dialog or add it to backend/.env file.", + } + ) + return + should_generate_images = ( params["isImageGenerationEnabled"] if "isImageGenerationEnabled" in params @@ -73,7 +92,8 @@ async def stream_code_test(websocket: WebSocket): else: completion = await stream_openai_response( prompt_messages, - lambda x: process_chunk(x), + api_key=openai_api_key, + callback=lambda x: process_chunk(x), ) # Write the messages dict into a log so that we can debug later @@ -84,7 +104,9 @@ async def stream_code_test(websocket: WebSocket): await websocket.send_json( {"type": "status", "value": "Generating images..."} ) - updated_html = await generate_images(completion, image_cache=image_cache) + updated_html = await generate_images( + completion, api_key=openai_api_key, image_cache=image_cache + ) else: updated_html = completion await websocket.send_json({"type": "setCode", "value": updated_html}) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index d5560be..634c800 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -29,6 +29,7 @@ function App() { const [updateInstruction, setUpdateInstruction] = useState(""); const [history, setHistory] = useState([]); const [settings, setSettings] = useState({ + openAiApiKey: null, isImageGenerationEnabled: true, }); diff --git a/frontend/src/components/SettingsDialog.tsx b/frontend/src/components/SettingsDialog.tsx index 294ec59..512894f 100644 --- a/frontend/src/components/SettingsDialog.tsx +++ b/frontend/src/components/SettingsDialog.tsx @@ -1,6 +1,8 @@ import { Dialog, + DialogClose, DialogContent, + DialogFooter, DialogHeader, DialogTitle, DialogTrigger, @@ -9,6 +11,7 @@ import { FaCog } from "react-icons/fa"; import { Settings } from "../types"; import { Switch } from "./ui/switch"; import { Label } from "./ui/label"; +import { Input } from "./ui/input"; interface Props { settings: Settings; @@ -24,25 +27,48 @@ function SettingsDialog({ settings, setSettings }: Props) { Settings -
- - - setSettings((s) => ({ - ...s, - isImageGenerationEnabled: !s.isImageGenerationEnabled, - })) - } - /> -
+
+ + + setSettings((s) => ({ + ...s, + isImageGenerationEnabled: !s.isImageGenerationEnabled, + })) + } + /> +
+
+ + + + setSettings((s) => ({ + ...s, + openAiApiKey: e.target.value, + })) + } + /> +
+ + Save +
); diff --git a/frontend/src/components/ui/input.tsx b/frontend/src/components/ui/input.tsx new file mode 100644 index 0000000..a92b8e0 --- /dev/null +++ b/frontend/src/components/ui/input.tsx @@ -0,0 +1,25 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +export interface InputProps + extends React.InputHTMLAttributes {} + +const Input = React.forwardRef( + ({ className, type, ...props }, ref) => { + return ( + + ) + } +) +Input.displayName = "Input" + +export { Input } diff --git a/frontend/src/generateCode.ts b/frontend/src/generateCode.ts index 06ecc5b..c3e9218 100644 --- a/frontend/src/generateCode.ts +++ b/frontend/src/generateCode.ts @@ -36,6 +36,9 @@ export function generateCode( onStatusUpdate(response.value); } else if (response.type === "setCode") { onSetCode(response.value); + } else if (response.type === "error") { + console.error("Error generating code", response.value); + toast.error(response.value); } }); diff --git a/frontend/src/types.ts b/frontend/src/types.ts index d9fe60b..b2c6305 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -1,3 +1,4 @@ export interface Settings { + openAiApiKey: string | null; isImageGenerationEnabled: boolean; } From 6491e60f58806fc407cf0f736444332a5f34a36d Mon Sep 17 00:00:00 2001 From: Abi Raja Date: Thu, 16 Nov 2023 18:48:30 -0500 Subject: [PATCH 13/29] fix for render deployment --- frontend/package.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/frontend/package.json b/frontend/package.json index c5933e5..7a34ab2 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -47,5 +47,8 @@ "tailwindcss": "^3.3.5", "typescript": "^5.0.2", "vite": "^4.4.5" + }, + "engines": { + "node": ">=14.18.0" } } From 3adec32ef10498d8c1f025ab159d438500418875 Mon Sep 17 00:00:00 2001 From: Abi Raja Date: Thu, 16 Nov 2023 19:03:05 -0500 Subject: [PATCH 14/29] add build script for render backend deployment --- backend/build.sh | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 backend/build.sh diff --git a/backend/build.sh b/backend/build.sh new file mode 100644 index 0000000..baab923 --- /dev/null +++ b/backend/build.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +# exit on error +set -o errexit + +echo "Installing the latest version of poetry..." +pip install --upgrade pip +pip install poetry==1.4.1 + +rm poetry.lock +poetry lock +python -m poetry install From 02ddd63db53a66b6e9219c3159a5b2895c083df2 Mon Sep 17 00:00:00 2001 From: Abi Raja Date: Thu, 16 Nov 2023 19:12:41 -0500 Subject: [PATCH 15/29] set up vite base URL for when it's deployed --- frontend/vite.config.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 8b0c365..3e7414d 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -5,6 +5,7 @@ import react from "@vitejs/plugin-react"; // https://vitejs.dev/config/ export default defineConfig({ + base: process.env.VITE_IS_DEPLOYED ? "/free-tools/screenshot-to-code/" : "", plugins: [react(), checker({ typescript: true })], resolve: { alias: { From f87d93ed75105e44801b978cd4bb77472fb1aad0 Mon Sep 17 00:00:00 2001 From: Abi Raja Date: Thu, 16 Nov 2023 19:29:33 -0500 Subject: [PATCH 16/29] Update README.md --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index df5e72e..8566f93 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,8 @@ https://github.com/abi/screenshot-to-code/assets/23818/6cebadae-2fe3-4986-ac6a-8 See Examples section below for more demos. +You can now [try it here](https://picoapps.xyz/free-tools/screenshot-to-code) (bring your own OpenAI key). Or see Getting Started below for local install instructions. + ## Updates * Nov 16 - Added a setting to disable DALL-E image generation if you don't need that @@ -15,6 +17,8 @@ See Examples section below for more demos. ## Getting Started +If you don't want to run this locally, you can [try it here](https://picoapps.xyz/free-tools/screenshot-to-code) (bring your own OpenAI key). + The app has a React/Vite frontend and a FastAPI backend. You will need an OpenAI API key with access to the GPT-4 Vision API. Run the backend (I use Poetry for package management - `pip install poetry` if you don't have it): From 867f30af635c1d570112d9927e4482c0d6c87bc9 Mon Sep 17 00:00:00 2001 From: Abi Raja Date: Thu, 16 Nov 2023 19:44:06 -0500 Subject: [PATCH 17/29] Update README.md --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index 8566f93..7d0a3e6 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ https://github.com/abi/screenshot-to-code/assets/23818/6cebadae-2fe3-4986-ac6a-8 See Examples section below for more demos. -You can now [try it here](https://picoapps.xyz/free-tools/screenshot-to-code) (bring your own OpenAI key). Or see Getting Started below for local install instructions. +🆕 [Try it here](https://picoapps.xyz/free-tools/screenshot-to-code) (bring your own OpenAI key). Or see Getting Started below for local install instructions. ## Updates @@ -17,8 +17,6 @@ You can now [try it here](https://picoapps.xyz/free-tools/screenshot-to-code) (b ## Getting Started -If you don't want to run this locally, you can [try it here](https://picoapps.xyz/free-tools/screenshot-to-code) (bring your own OpenAI key). - The app has a React/Vite frontend and a FastAPI backend. You will need an OpenAI API key with access to the GPT-4 Vision API. Run the backend (I use Poetry for package management - `pip install poetry` if you don't have it): From 6f588b41a6f6e8df9a7040e5e0bd791924690c9a Mon Sep 17 00:00:00 2001 From: Abi Raja Date: Thu, 16 Nov 2023 20:00:57 -0500 Subject: [PATCH 18/29] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7d0a3e6..cab2b1c 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ https://github.com/abi/screenshot-to-code/assets/23818/6cebadae-2fe3-4986-ac6a-8 See Examples section below for more demos. -🆕 [Try it here](https://picoapps.xyz/free-tools/screenshot-to-code) (bring your own OpenAI key). Or see Getting Started below for local install instructions. +🆕 [Try it here](https://picoapps.xyz/free-tools/screenshot-to-code) (bring your own OpenAI key - **your key must have access to GPT-4 Vision. See FAQ section below for details.**). Or see Getting Started below for local install instructions. ## Updates From 15bb3e836fc320437e5a6b554c33e8815ce57b4d Mon Sep 17 00:00:00 2001 From: Abi Raja Date: Thu, 16 Nov 2023 20:01:15 -0500 Subject: [PATCH 19/29] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cab2b1c..82968dd 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ https://github.com/abi/screenshot-to-code/assets/23818/6cebadae-2fe3-4986-ac6a-8 See Examples section below for more demos. -🆕 [Try it here](https://picoapps.xyz/free-tools/screenshot-to-code) (bring your own OpenAI key - **your key must have access to GPT-4 Vision. See FAQ section below for details.**). Or see Getting Started below for local install instructions. +🆕 [Try it here](https://picoapps.xyz/free-tools/screenshot-to-code) (bring your own OpenAI key - **your key must have access to GPT-4 Vision. See FAQ section below for details**). Or see Getting Started below for local install instructions. ## Updates From 49cf817072633fe9af0704b8e6bbe378277c9f80 Mon Sep 17 00:00:00 2001 From: Abi Raja Date: Thu, 16 Nov 2023 20:01:51 -0500 Subject: [PATCH 20/29] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 82968dd..1529ec0 100644 --- a/README.md +++ b/README.md @@ -62,4 +62,4 @@ https://github.com/abi/screenshot-to-code/assets/23818/3fec0f77-44e8-4fb3-a769-a ## Hosted Version -Hosted version coming soon on [Pico](https://picoapps.xyz?ref=github). +🆕 [Try it here](https://picoapps.xyz/free-tools/screenshot-to-code) (bring your own OpenAI key - **your key must have access to GPT-4 Vision. See FAQ section below for details**). Or see Getting Started below for local install instructions. From ba6a6828c4749fca08417a9e08e3bdb60aacd6a9 Mon Sep 17 00:00:00 2001 From: Abi Raja Date: Thu, 16 Nov 2023 22:16:34 -0500 Subject: [PATCH 21/29] update error message --- backend/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/main.py b/backend/main.py index a3a74ad..6c0fbe1 100644 --- a/backend/main.py +++ b/backend/main.py @@ -55,7 +55,7 @@ async def stream_code_test(websocket: WebSocket): await websocket.send_json( { "type": "error", - "value": "OpenAI API key found. Please add your API key in the settings dialog or add it to backend/.env file.", + "value": "No OpenAI API key found. Please add your API key in the settings dialog or add it to backend/.env file.", } ) return From 30bb78083c82cf24614988dbd0ff196066e39f7f Mon Sep 17 00:00:00 2001 From: Abi Raja Date: Thu, 16 Nov 2023 22:38:45 -0500 Subject: [PATCH 22/29] Update README.md --- README.md | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 1529ec0..0359a1d 100644 --- a/README.md +++ b/README.md @@ -49,17 +49,20 @@ If you prefer to run the backend on a different port, update VITE_WS_BACKEND_URL ## Examples +**NYTimes** + +| Original | Replica | +|----------|---------| +| ![Original](https://github.com/abi/screenshot-to-code/assets/23818/01b062c2-f39a-46dd-84ca-bec6c986463a) | ![Replica](https://github.com/abi/screenshot-to-code/assets/23818/e1f662b6-68f7-4578-a64d-ea4be52e31f5) | + +**Instagram page (with not Taylor Swift pics)** + +https://github.com/abi/screenshot-to-code/assets/23818/503eb86a-356e-4dfc-926a-dabdb1ac7ba1 + **Hacker News** but it gets the colors wrong at first so we nudge it https://github.com/abi/screenshot-to-code/assets/23818/3fec0f77-44e8-4fb3-a769-ac7410315e5d -**NYTimes** - -Screenshot 2023-11-16 at 12 06 09 PM -Screenshot 2023-11-16 at 12 07 25 PM - - - ## Hosted Version 🆕 [Try it here](https://picoapps.xyz/free-tools/screenshot-to-code) (bring your own OpenAI key - **your key must have access to GPT-4 Vision. See FAQ section below for details**). Or see Getting Started below for local install instructions. From 1cf4d544d5800ddf037ced30e4fed58af997704c Mon Sep 17 00:00:00 2001 From: Abi Raja Date: Thu, 16 Nov 2023 22:45:36 -0500 Subject: [PATCH 23/29] fix bug with print statement when no key is sent in --- backend/main.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/main.py b/backend/main.py index 6c0fbe1..0da9960 100644 --- a/backend/main.py +++ b/backend/main.py @@ -48,7 +48,8 @@ async def stream_code_test(websocket: WebSocket): print("Using OpenAI API key from client-side settings dialog") else: openai_api_key = os.environ.get("OPENAI_API_KEY") - print("Using OpenAI API key from environment variable") + if openai_api_key: + print("Using OpenAI API key from environment variable") if not openai_api_key: print("OpenAI API key not found") From dbf89928ec57f9479dd102cadf9162d9c3bca989 Mon Sep 17 00:00:00 2001 From: Abi Raja Date: Fri, 17 Nov 2023 12:15:38 -0500 Subject: [PATCH 24/29] add env var for where to store logs --- backend/main.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/backend/main.py b/backend/main.py index 0da9960..c6e5556 100644 --- a/backend/main.py +++ b/backend/main.py @@ -23,12 +23,18 @@ SHOULD_MOCK_AI_RESPONSE = False def write_logs(prompt_messages, completion): - # Create run_logs directory if it doesn't exist - if not os.path.exists("run_logs"): - os.makedirs("run_logs") + # Get the logs path from environment, default to the current working directory + logs_path = os.environ.get("LOGS_PATH", os.getcwd()) - # Generate a unique filename using the current timestamp - filename = datetime.now().strftime("run_logs/messages_%Y%m%d_%H%M%S.json") + # Create run_logs directory if it doesn't exist within the specified logs path + logs_directory = os.path.join(logs_path, "run_logs") + if not os.path.exists(logs_directory): + os.makedirs(logs_directory) + + print("Writing to logs directory:", logs_directory) + + # Generate a unique filename using the current timestamp within the logs directory + filename = datetime.now().strftime(f"{logs_directory}/messages_%Y%m%d_%H%M%S.json") # Write the messages dict into a new file for each run with open(filename, "w") as f: From 3d2978f5d2136e5b253a7f40699dc7daac3e0fd5 Mon Sep 17 00:00:00 2001 From: Jirawat Boonkumnerd Date: Sat, 18 Nov 2023 06:30:11 +0700 Subject: [PATCH 25/29] feat: add paste from clipboard - add paste image from clipboard (ctrl+v) - validate if the file is an image --- frontend/src/App.tsx | 10 ++++--- frontend/src/components/ImageUpload.tsx | 35 +++++++++++++++++++++++-- 2 files changed, 39 insertions(+), 6 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 634c800..0ce890f 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -77,10 +77,12 @@ function App() { // Initial version creation function doCreate(referenceImages: string[]) { setReferenceImages(referenceImages); - doGenerateCode({ - generationType: "create", - image: referenceImages[0], - }); + if (referenceImages.length > 0) { + doGenerateCode({ + generationType: "create", + image: referenceImages[0], + }); + } } // Subsequent updates diff --git a/frontend/src/components/ImageUpload.tsx b/frontend/src/components/ImageUpload.tsx index 16b5373..35a9c5a 100644 --- a/frontend/src/components/ImageUpload.tsx +++ b/frontend/src/components/ImageUpload.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useMemo } from "react"; +import { useState, useEffect, useMemo, useCallback } from "react"; import { useDropzone } from "react-dropzone"; // import { PromptImage } from "../../../types"; import { toast } from "react-hot-toast"; @@ -89,6 +89,37 @@ function ImageUpload({ setReferenceImages }: Props) { }, }); + const pasteEvent = useCallback( + (event: ClipboardEvent) => { + const clipboardData = event.clipboardData; + if (!clipboardData) return; + + const items = clipboardData.items; + const files = []; + for (let i = 0; i < items.length; i++) { + const file = items[i].getAsFile(); + if (file && file.type.startsWith("image/")) { + files.push(file); + } + } + + // Convert images to data URLs and set the prompt images state + Promise.all(files.map((file) => fileToDataURL(file))) + .then((dataUrls) => { + setReferenceImages(dataUrls.map((dataUrl) => dataUrl as string)); + }) + .catch((error) => { + // TODO: Display error to user + console.error("Error reading files:", error); + }); + }, + [setReferenceImages] + ); + + useEffect(() => { + window.addEventListener("paste", pasteEvent); + }, [pasteEvent]); + useEffect(() => { return () => files.forEach((file) => URL.revokeObjectURL(file.preview)); }, [files]); // Added files as a dependency @@ -108,7 +139,7 @@ function ImageUpload({ setReferenceImages }: Props) { {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
-

Drop a screenshot here, or click to select

+

Drop a screenshot here, paste from clipboard, or click to select

); From f7c504fac4c2cc8b208cd716ca651949ec2ec0fc Mon Sep 17 00:00:00 2001 From: Guspan Tanadi <36249910+guspan-tanadi@users.noreply.github.com> Date: Sat, 18 Nov 2023 14:57:45 +0700 Subject: [PATCH 26/29] style(README): create section links Examples FAQ Getting Started --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 0359a1d..a8f7ec0 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,9 @@ This is a simple app that converts a screenshot to HTML/Tailwind CSS. It uses GP https://github.com/abi/screenshot-to-code/assets/23818/6cebadae-2fe3-4986-ac6a-8fb9db030045 -See Examples section below for more demos. +See [Examples](#examples) section below for more demos. -🆕 [Try it here](https://picoapps.xyz/free-tools/screenshot-to-code) (bring your own OpenAI key - **your key must have access to GPT-4 Vision. See FAQ section below for details**). Or see Getting Started below for local install instructions. +🆕 [Try it here](https://picoapps.xyz/free-tools/screenshot-to-code) (bring your own OpenAI key - **your key must have access to GPT-4 Vision. See [FAQ](#faqs) section below for details**). Or see [Getting Started](#getting-started) below for local install instructions. ## Updates @@ -65,4 +65,4 @@ https://github.com/abi/screenshot-to-code/assets/23818/3fec0f77-44e8-4fb3-a769-a ## Hosted Version -🆕 [Try it here](https://picoapps.xyz/free-tools/screenshot-to-code) (bring your own OpenAI key - **your key must have access to GPT-4 Vision. See FAQ section below for details**). Or see Getting Started below for local install instructions. +🆕 [Try it here](https://picoapps.xyz/free-tools/screenshot-to-code) (bring your own OpenAI key - **your key must have access to GPT-4 Vision. See [FAQ](#faqs) section for details**). Or see [Getting Started](#getting-started) for local install instructions. From 8ed1f6173122eabeab7d88e2422291e7e1daadaa Mon Sep 17 00:00:00 2001 From: Abi Raja Date: Sat, 18 Nov 2023 11:23:56 -0500 Subject: [PATCH 27/29] improve hosted version experience but disable these elements when running locally --- frontend/src/App.tsx | 7 +++++++ frontend/src/components/OnboardingNote.tsx | 20 ++++++++++++++++++++ frontend/src/components/PicoBadge.tsx | 12 ++++++++++++ frontend/src/config.ts | 3 +++ 4 files changed, 42 insertions(+) create mode 100644 frontend/src/components/OnboardingNote.tsx create mode 100644 frontend/src/components/PicoBadge.tsx create mode 100644 frontend/src/config.ts diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 634c800..396b3e8 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -18,6 +18,9 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "./components/ui/tabs"; import CodeMirror from "./components/CodeMirror"; import SettingsDialog from "./components/SettingsDialog"; import { Settings } from "./types"; +import { IS_RUNNING_ON_CLOUD } from "./config"; +import { PicoBadge } from "./components/PicoBadge"; +import { OnboardingNote } from "./components/OnboardingNote"; function App() { const [appState, setAppState] = useState<"INITIAL" | "CODING" | "CODE_READY">( @@ -100,6 +103,8 @@ function App() { return (
+ {IS_RUNNING_ON_CLOUD && } +
@@ -112,6 +117,8 @@ function App() {

)} + {IS_RUNNING_ON_CLOUD && !settings.openAiApiKey && } + {(appState === "CODING" || appState === "CODE_READY") && ( <> {/* Show code preview only when coding */} diff --git a/frontend/src/components/OnboardingNote.tsx b/frontend/src/components/OnboardingNote.tsx new file mode 100644 index 0000000..437bc1e --- /dev/null +++ b/frontend/src/components/OnboardingNote.tsx @@ -0,0 +1,20 @@ +export function OnboardingNote() { + return ( +
+ Please add your OpenAI API key (must have GPT4 vision access) in the + settings dialog (gear icon above). +
+
+ How do you get an OpenAI API key that has the GPT4 Vision model available? + Create an OpenAI account. And then, you need to buy at least $1 worth of + credit on the Billing dashboard. +
+ + This key is never stored. This app is open source. You can{" "} + + check the code to confirm. + + +
+ ); +} diff --git a/frontend/src/components/PicoBadge.tsx b/frontend/src/components/PicoBadge.tsx new file mode 100644 index 0000000..077a99d --- /dev/null +++ b/frontend/src/components/PicoBadge.tsx @@ -0,0 +1,12 @@ +export function PicoBadge() { + return ( + +
+ an open source project by Pico +
+
+ ); +} diff --git a/frontend/src/config.ts b/frontend/src/config.ts new file mode 100644 index 0000000..054f6ed --- /dev/null +++ b/frontend/src/config.ts @@ -0,0 +1,3 @@ +// 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; From 6651b884bb26b4510211f52acb7dd080ff19b321 Mon Sep 17 00:00:00 2001 From: Abi Raja Date: Sat, 18 Nov 2023 15:38:34 -0500 Subject: [PATCH 28/29] minor fixes --- frontend/src/components/ImageUpload.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/ImageUpload.tsx b/frontend/src/components/ImageUpload.tsx index 35a9c5a..2f0b059 100644 --- a/frontend/src/components/ImageUpload.tsx +++ b/frontend/src/components/ImageUpload.tsx @@ -80,7 +80,7 @@ function ImageUpload({ setReferenceImages }: Props) { setReferenceImages(dataUrls.map((dataUrl) => dataUrl as string)); }) .catch((error) => { - // TODO: Display error to user + toast.error("Error reading files" + error); console.error("Error reading files:", error); }); }, @@ -116,6 +116,7 @@ function ImageUpload({ setReferenceImages }: Props) { [setReferenceImages] ); + // TODO: Make sure we don't listen to paste events in text input components useEffect(() => { window.addEventListener("paste", pasteEvent); }, [pasteEvent]); From fad46574fdcba91c61272a9205f6ef9f883e716d Mon Sep 17 00:00:00 2001 From: Ikko Eltociear Ashimine Date: Mon, 20 Nov 2023 00:57:27 +0900 Subject: [PATCH 29/29] Update ImageUpload.tsx seperate -> separate --- frontend/src/components/ImageUpload.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/ImageUpload.tsx b/frontend/src/components/ImageUpload.tsx index 2f0b059..4a46d99 100644 --- a/frontend/src/components/ImageUpload.tsx +++ b/frontend/src/components/ImageUpload.tsx @@ -35,7 +35,7 @@ const rejectStyle = { borderColor: "#ff1744", }; -// TODO: Move to a seperate file +// TODO: Move to a separate file function fileToDataURL(file: File) { return new Promise((resolve, reject) => { const reader = new FileReader();