diff --git a/.gitignore b/.gitignore index 39aee32..7b377b1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,13 @@ .aider* +# Project-related files + # Run logs backend/run_logs/* -.env \ No newline at end of file +# Weird Docker setup related files +backend/backend/* + +# Env vars +frontend/.env.local +.env diff --git a/README.md b/README.md index 97bc183..81cc82a 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # screenshot-to-code -This simple app converts a screenshot to HTML/Tailwind CSS. It uses GPT-4 Vision to generate the code and DALL-E 3 to generate similar-looking images. +This simple app converts a screenshot to HTML/Tailwind CSS. It uses GPT-4 Vision to generate the code and DALL-E 3 to generate similar-looking images. You can now also enter a URL to clone a live website! https://github.com/abi/screenshot-to-code/assets/23818/6cebadae-2fe3-4986-ac6a-8fb9db030045 diff --git a/backend/main.py b/backend/main.py index 2fa9e9f..8fabb39 100644 --- a/backend/main.py +++ b/backend/main.py @@ -59,7 +59,7 @@ def write_logs(prompt_messages, completion): @app.websocket("/generate-code") -async def stream_code_test(websocket: WebSocket): +async def stream_code(websocket: WebSocket): await websocket.accept() params = await websocket.receive_json() diff --git a/frontend/package.json b/frontend/package.json index 78ce51e..ef840e7 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -23,6 +23,7 @@ "clsx": "^2.0.0", "codemirror": "^6.0.1", "html2canvas": "^1.4.1", + "copy-to-clipboard": "^3.3.3", "react": "^18.2.0", "react-dom": "^18.2.0", "react-dropzone": "^14.2.3", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 72805ae..50bb48a 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,4 +1,4 @@ -import { useRef, useState } from "react"; +import { useRef, useState, useCallback } from "react"; import ImageUpload from "./components/ImageUpload"; import CodePreview from "./components/CodePreview"; import Preview from "./components/Preview"; @@ -7,18 +7,22 @@ import Spinner from "./components/Spinner"; import classNames from "classnames"; import { FaCode, + FaCopy, FaDesktop, FaDownload, FaMobile, FaUndo, } from "react-icons/fa"; + import { Switch } from "./components/ui/switch"; +import copy from "copy-to-clipboard"; +import toast from "react-hot-toast"; 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 { AppStatus, Settings, USER_CLOSE_WEB_SOCKET_CODE } from "@/types"; +import { AppStatus, Settings } from "@/types"; import { IS_RUNNING_ON_CLOUD } from "./config"; import { PicoBadge } from "./components/PicoBadge"; import { OnboardingNote } from "./components/OnboardingNote"; @@ -26,12 +30,10 @@ import { usePersistedState } from "./hooks/usePersistedState"; import { UrlInputSection } from "./components/UrlInputSection"; import TermsOfServiceDialog from "./components/TermsOfServiceDialog"; import html2canvas from 'html2canvas'; - +import { USER_CLOSE_WEB_SOCKET_CODE } from "./constants"; function App() { - const [appState, setAppState] = useState( - AppStatus.INITIAL - ); + const [appState, setAppState] = useState(AppState.INITIAL); const [generatedCode, setGeneratedCode] = useState(""); const [referenceImages, setReferenceImages] = useState([]); const [executionConsole, setExecutionConsole] = useState([]); @@ -78,7 +80,7 @@ function App() { }; const reset = () => { - setAppState(AppStatus.INITIAL); + setAppState(AppState.INITIAL); setGeneratedCode(""); setReferenceImages([]); setExecutionConsole([]); @@ -93,7 +95,7 @@ function App() { function doGenerateCode(params: CodeGenerationParams) { setExecutionConsole([]); - setAppState(AppStatus.CODING); + setAppState(AppState.CODING); // Merge settings with params const updatedParams = { ...params, ...settings }; @@ -104,7 +106,7 @@ function App() { (token) => setGeneratedCode((prev) => prev + token), (code) => setGeneratedCode(code), (line) => setExecutionConsole((prev) => [...prev, line]), - () => setAppState(AppStatus.CODE_READY) + () => setAppState(AppState.CODE_READY) ); } @@ -143,6 +145,11 @@ function App() { setUpdateInstruction(""); } + const doCopyCode = useCallback(() => { + copy(generatedCode); + toast.success("Copied to clipboard"); + }, [generatedCode]); + return (
{IS_RUNNING_ON_CLOUD && } @@ -154,7 +161,7 @@ function App() {

Screenshot to Code

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

Drag & drop a screenshot to get started.

@@ -162,20 +169,18 @@ function App() { {IS_RUNNING_ON_CLOUD && !settings.openAiApiKey && } - {(appState === AppStatus.CODING || appState === AppStatus.CODE_READY) && ( + {(appState === AppState.CODING || + appState === AppState.CODE_READY) && ( <> {/* Show code preview only when coding */} - {appState === AppStatus.CODING && ( + {appState === AppState.CODING && (
{executionConsole.slice(-1)[0]}
-
@@ -183,7 +188,7 @@ function App() {
)} - {appState === AppStatus.CODE_READY && ( + {appState === AppState.CODE_READY && (
@@ -225,7 +230,7 @@ function App() {
- {appState === AppStatus.INITIAL && ( + {appState === AppState.INITIAL && (
)} - {(appState === AppStatus.CODING || appState === AppStatus.CODE_READY) && ( + {(appState === AppState.CODING || appState === AppState.CODE_READY) && (
@@ -292,10 +297,19 @@ function App() { - +
+ + + + +
diff --git a/frontend/src/constants.ts b/frontend/src/constants.ts new file mode 100644 index 0000000..a5da189 --- /dev/null +++ b/frontend/src/constants.ts @@ -0,0 +1 @@ +export const USER_CLOSE_WEB_SOCKET_CODE = 4333; diff --git a/frontend/src/generateCode.ts b/frontend/src/generateCode.ts index b848b78..893164e 100644 --- a/frontend/src/generateCode.ts +++ b/frontend/src/generateCode.ts @@ -1,6 +1,6 @@ import toast from "react-hot-toast"; import { WS_BACKEND_URL } from "./config"; -import { USER_CLOSE_WEB_SOCKET_CODE } from "./types"; +import { USER_CLOSE_WEB_SOCKET_CODE } from "./constants"; const ERROR_MESSAGE = "Error generating code. Check the Developer Console for details. Feel free to open a Github issue"; @@ -53,6 +53,7 @@ export function generateCode( } else if (event.code !== 1000) { console.error("WebSocket error code", event); toast.error(ERROR_MESSAGE); + onComplete(); } onComplete(); }); diff --git a/frontend/src/types.ts b/frontend/src/types.ts index 8797195..83f578e 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -5,10 +5,8 @@ export interface Settings { editorTheme: string; } -export enum AppStatus { +export enum AppState { INITIAL = "INITIAL", CODING = "CODING", CODE_READY = "CODE_READY", } - -export const USER_CLOSE_WEB_SOCKET_CODE = 4333; \ No newline at end of file diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 4779a96..1230589 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -1263,6 +1263,13 @@ convert-source-map@^2.0.0: resolved "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz" integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== +copy-to-clipboard@^3.3.3: + version "3.3.3" + resolved "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz#55ac43a1db8ae639a4bd99511c148cdd1b83a1b0" + integrity sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA== + dependencies: + toggle-selection "^1.0.6" + crelt@^1.0.5: version "1.0.6" resolved "https://registry.yarnpkg.com/crelt/-/crelt-1.0.6.tgz#7cc898ea74e190fb6ef9dae57f8f81cf7302df72" @@ -2447,6 +2454,11 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" +toggle-selection@^1.0.6: + version "1.0.6" + resolved "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz#6e45b1263f2017fa0acc7d89d78b15b8bf77da32" + integrity sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ== + ts-api-utils@^1.0.1: version "1.0.3" resolved "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz"