Merge branch 'main' into feat/add-image-comparison
This commit is contained in:
commit
cc22292941
7
.gitignore
vendored
7
.gitignore
vendored
@ -1,6 +1,13 @@
|
|||||||
.aider*
|
.aider*
|
||||||
|
|
||||||
|
# Project-related files
|
||||||
|
|
||||||
# Run logs
|
# Run logs
|
||||||
backend/run_logs/*
|
backend/run_logs/*
|
||||||
|
|
||||||
|
# Weird Docker setup related files
|
||||||
|
backend/backend/*
|
||||||
|
|
||||||
|
# Env vars
|
||||||
|
frontend/.env.local
|
||||||
.env
|
.env
|
||||||
@ -1,6 +1,6 @@
|
|||||||
# screenshot-to-code
|
# 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
|
https://github.com/abi/screenshot-to-code/assets/23818/6cebadae-2fe3-4986-ac6a-8fb9db030045
|
||||||
|
|
||||||
|
|||||||
@ -59,7 +59,7 @@ def write_logs(prompt_messages, completion):
|
|||||||
|
|
||||||
|
|
||||||
@app.websocket("/generate-code")
|
@app.websocket("/generate-code")
|
||||||
async def stream_code_test(websocket: WebSocket):
|
async def stream_code(websocket: WebSocket):
|
||||||
await websocket.accept()
|
await websocket.accept()
|
||||||
|
|
||||||
params = await websocket.receive_json()
|
params = await websocket.receive_json()
|
||||||
|
|||||||
@ -23,6 +23,7 @@
|
|||||||
"clsx": "^2.0.0",
|
"clsx": "^2.0.0",
|
||||||
"codemirror": "^6.0.1",
|
"codemirror": "^6.0.1",
|
||||||
"html2canvas": "^1.4.1",
|
"html2canvas": "^1.4.1",
|
||||||
|
"copy-to-clipboard": "^3.3.3",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-dropzone": "^14.2.3",
|
"react-dropzone": "^14.2.3",
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { useRef, useState } from "react";
|
import { useRef, useState, useCallback } from "react";
|
||||||
import ImageUpload from "./components/ImageUpload";
|
import ImageUpload from "./components/ImageUpload";
|
||||||
import CodePreview from "./components/CodePreview";
|
import CodePreview from "./components/CodePreview";
|
||||||
import Preview from "./components/Preview";
|
import Preview from "./components/Preview";
|
||||||
@ -7,18 +7,22 @@ import Spinner from "./components/Spinner";
|
|||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import {
|
import {
|
||||||
FaCode,
|
FaCode,
|
||||||
|
FaCopy,
|
||||||
FaDesktop,
|
FaDesktop,
|
||||||
FaDownload,
|
FaDownload,
|
||||||
FaMobile,
|
FaMobile,
|
||||||
FaUndo,
|
FaUndo,
|
||||||
} from "react-icons/fa";
|
} from "react-icons/fa";
|
||||||
|
|
||||||
import { Switch } from "./components/ui/switch";
|
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 { Button } from "@/components/ui/button";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "./components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "./components/ui/tabs";
|
||||||
import CodeMirror from "./components/CodeMirror";
|
import CodeMirror from "./components/CodeMirror";
|
||||||
import SettingsDialog from "./components/SettingsDialog";
|
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 { IS_RUNNING_ON_CLOUD } from "./config";
|
||||||
import { PicoBadge } from "./components/PicoBadge";
|
import { PicoBadge } from "./components/PicoBadge";
|
||||||
import { OnboardingNote } from "./components/OnboardingNote";
|
import { OnboardingNote } from "./components/OnboardingNote";
|
||||||
@ -26,12 +30,10 @@ import { usePersistedState } from "./hooks/usePersistedState";
|
|||||||
import { UrlInputSection } from "./components/UrlInputSection";
|
import { UrlInputSection } from "./components/UrlInputSection";
|
||||||
import TermsOfServiceDialog from "./components/TermsOfServiceDialog";
|
import TermsOfServiceDialog from "./components/TermsOfServiceDialog";
|
||||||
import html2canvas from 'html2canvas';
|
import html2canvas from 'html2canvas';
|
||||||
|
import { USER_CLOSE_WEB_SOCKET_CODE } from "./constants";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [appState, setAppState] = useState<AppStatus>(
|
const [appState, setAppState] = useState<AppState>(AppState.INITIAL);
|
||||||
AppStatus.INITIAL
|
|
||||||
);
|
|
||||||
const [generatedCode, setGeneratedCode] = useState<string>("");
|
const [generatedCode, setGeneratedCode] = useState<string>("");
|
||||||
const [referenceImages, setReferenceImages] = useState<string[]>([]);
|
const [referenceImages, setReferenceImages] = useState<string[]>([]);
|
||||||
const [executionConsole, setExecutionConsole] = useState<string[]>([]);
|
const [executionConsole, setExecutionConsole] = useState<string[]>([]);
|
||||||
@ -78,7 +80,7 @@ function App() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const reset = () => {
|
const reset = () => {
|
||||||
setAppState(AppStatus.INITIAL);
|
setAppState(AppState.INITIAL);
|
||||||
setGeneratedCode("");
|
setGeneratedCode("");
|
||||||
setReferenceImages([]);
|
setReferenceImages([]);
|
||||||
setExecutionConsole([]);
|
setExecutionConsole([]);
|
||||||
@ -93,7 +95,7 @@ function App() {
|
|||||||
|
|
||||||
function doGenerateCode(params: CodeGenerationParams) {
|
function doGenerateCode(params: CodeGenerationParams) {
|
||||||
setExecutionConsole([]);
|
setExecutionConsole([]);
|
||||||
setAppState(AppStatus.CODING);
|
setAppState(AppState.CODING);
|
||||||
|
|
||||||
// Merge settings with params
|
// Merge settings with params
|
||||||
const updatedParams = { ...params, ...settings };
|
const updatedParams = { ...params, ...settings };
|
||||||
@ -104,7 +106,7 @@ function App() {
|
|||||||
(token) => setGeneratedCode((prev) => prev + token),
|
(token) => setGeneratedCode((prev) => prev + token),
|
||||||
(code) => setGeneratedCode(code),
|
(code) => setGeneratedCode(code),
|
||||||
(line) => setExecutionConsole((prev) => [...prev, line]),
|
(line) => setExecutionConsole((prev) => [...prev, line]),
|
||||||
() => setAppState(AppStatus.CODE_READY)
|
() => setAppState(AppState.CODE_READY)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -143,6 +145,11 @@ function App() {
|
|||||||
setUpdateInstruction("");
|
setUpdateInstruction("");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const doCopyCode = useCallback(() => {
|
||||||
|
copy(generatedCode);
|
||||||
|
toast.success("Copied to clipboard");
|
||||||
|
}, [generatedCode]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
{IS_RUNNING_ON_CLOUD && <PicoBadge />}
|
{IS_RUNNING_ON_CLOUD && <PicoBadge />}
|
||||||
@ -154,7 +161,7 @@ function App() {
|
|||||||
<h1 className="text-2xl ">Screenshot to Code</h1>
|
<h1 className="text-2xl ">Screenshot to Code</h1>
|
||||||
<SettingsDialog settings={settings} setSettings={setSettings} />
|
<SettingsDialog settings={settings} setSettings={setSettings} />
|
||||||
</div>
|
</div>
|
||||||
{appState === AppStatus.INITIAL && (
|
{appState === AppState.INITIAL && (
|
||||||
<h2 className="text-sm text-gray-500 mb-2">
|
<h2 className="text-sm text-gray-500 mb-2">
|
||||||
Drag & drop a screenshot to get started.
|
Drag & drop a screenshot to get started.
|
||||||
</h2>
|
</h2>
|
||||||
@ -162,20 +169,18 @@ function App() {
|
|||||||
|
|
||||||
{IS_RUNNING_ON_CLOUD && !settings.openAiApiKey && <OnboardingNote />}
|
{IS_RUNNING_ON_CLOUD && !settings.openAiApiKey && <OnboardingNote />}
|
||||||
|
|
||||||
{(appState === AppStatus.CODING || appState === AppStatus.CODE_READY) && (
|
{(appState === AppState.CODING ||
|
||||||
|
appState === AppState.CODE_READY) && (
|
||||||
<>
|
<>
|
||||||
{/* Show code preview only when coding */}
|
{/* Show code preview only when coding */}
|
||||||
{appState === AppStatus.CODING && (
|
{appState === AppState.CODING && (
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<div className="flex items-center gap-x-1">
|
<div className="flex items-center gap-x-1">
|
||||||
<Spinner />
|
<Spinner />
|
||||||
{executionConsole.slice(-1)[0]}
|
{executionConsole.slice(-1)[0]}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex mt-4 w-full">
|
<div className="flex mt-4 w-full">
|
||||||
<Button
|
<Button onClick={stop} className="w-full">
|
||||||
onClick={stop}
|
|
||||||
className="w-full"
|
|
||||||
>
|
|
||||||
Stop
|
Stop
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@ -183,7 +188,7 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{appState === AppStatus.CODE_READY && (
|
{appState === AppState.CODE_READY && (
|
||||||
<div>
|
<div>
|
||||||
<div className="grid w-full gap-2">
|
<div className="grid w-full gap-2">
|
||||||
<div className="flex justify-between items-center gap-x-2">
|
<div className="flex justify-between items-center gap-x-2">
|
||||||
@ -225,7 +230,7 @@ function App() {
|
|||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<div
|
<div
|
||||||
className={classNames({
|
className={classNames({
|
||||||
"scanning relative": appState === AppStatus.CODING,
|
"scanning relative": appState === AppState.CODING,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
@ -258,7 +263,7 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<main className="py-2 lg:pl-96">
|
<main className="py-2 lg:pl-96">
|
||||||
{appState === AppStatus.INITIAL && (
|
{appState === AppState.INITIAL && (
|
||||||
<div className="flex flex-col justify-center items-center gap-y-10">
|
<div className="flex flex-col justify-center items-center gap-y-10">
|
||||||
<ImageUpload setReferenceImages={doCreate} />
|
<ImageUpload setReferenceImages={doCreate} />
|
||||||
<UrlInputSection
|
<UrlInputSection
|
||||||
@ -268,7 +273,7 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{(appState === AppStatus.CODING || appState === AppStatus.CODE_READY) && (
|
{(appState === AppState.CODING || appState === AppState.CODE_READY) && (
|
||||||
<div className="ml-4">
|
<div className="ml-4">
|
||||||
<Tabs defaultValue="desktop">
|
<Tabs defaultValue="desktop">
|
||||||
<div className="flex justify-end mr-8 mb-4">
|
<div className="flex justify-end mr-8 mb-4">
|
||||||
@ -292,10 +297,19 @@ function App() {
|
|||||||
<Preview code={generatedCode} device="mobile" />
|
<Preview code={generatedCode} device="mobile" />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="code">
|
<TabsContent value="code">
|
||||||
<CodeMirror
|
<div className="relative">
|
||||||
code={generatedCode}
|
<CodeMirror
|
||||||
editorTheme={settings.editorTheme}
|
code={generatedCode}
|
||||||
/>
|
editorTheme={settings.editorTheme}
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
title="Copy Code"
|
||||||
|
className="flex items-center justify-center w-10 h-10 text-gray-500 hover:bg-gray-100 cursor-pointer rounded-lg text-sm p-2.5 absolute top-[20px] right-[20px]"
|
||||||
|
onClick={doCopyCode}
|
||||||
|
>
|
||||||
|
<FaCopy />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
1
frontend/src/constants.ts
Normal file
1
frontend/src/constants.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export const USER_CLOSE_WEB_SOCKET_CODE = 4333;
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
import { WS_BACKEND_URL } from "./config";
|
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 =
|
const ERROR_MESSAGE =
|
||||||
"Error generating code. Check the Developer Console for details. Feel free to open a Github issue";
|
"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) {
|
} else if (event.code !== 1000) {
|
||||||
console.error("WebSocket error code", event);
|
console.error("WebSocket error code", event);
|
||||||
toast.error(ERROR_MESSAGE);
|
toast.error(ERROR_MESSAGE);
|
||||||
|
onComplete();
|
||||||
}
|
}
|
||||||
onComplete();
|
onComplete();
|
||||||
});
|
});
|
||||||
|
|||||||
@ -5,10 +5,8 @@ export interface Settings {
|
|||||||
editorTheme: string;
|
editorTheme: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum AppStatus {
|
export enum AppState {
|
||||||
INITIAL = "INITIAL",
|
INITIAL = "INITIAL",
|
||||||
CODING = "CODING",
|
CODING = "CODING",
|
||||||
CODE_READY = "CODE_READY",
|
CODE_READY = "CODE_READY",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const USER_CLOSE_WEB_SOCKET_CODE = 4333;
|
|
||||||
@ -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"
|
resolved "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz"
|
||||||
integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==
|
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:
|
crelt@^1.0.5:
|
||||||
version "1.0.6"
|
version "1.0.6"
|
||||||
resolved "https://registry.yarnpkg.com/crelt/-/crelt-1.0.6.tgz#7cc898ea74e190fb6ef9dae57f8f81cf7302df72"
|
resolved "https://registry.yarnpkg.com/crelt/-/crelt-1.0.6.tgz#7cc898ea74e190fb6ef9dae57f8f81cf7302df72"
|
||||||
@ -2447,6 +2454,11 @@ to-regex-range@^5.0.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
is-number "^7.0.0"
|
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:
|
ts-api-utils@^1.0.1:
|
||||||
version "1.0.3"
|
version "1.0.3"
|
||||||
resolved "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz"
|
resolved "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user