Merge main into sweep/add-sweep-config
This commit is contained in:
commit
e1cc58ac19
9
.gitignore
vendored
9
.gitignore
vendored
@ -1,6 +1,13 @@
|
||||
.aider*
|
||||
|
||||
# Project-related files
|
||||
|
||||
# Run logs
|
||||
backend/run_logs/*
|
||||
|
||||
.env
|
||||
# Weird Docker setup related files
|
||||
backend/backend/*
|
||||
|
||||
# Env vars
|
||||
frontend/.env.local
|
||||
.env
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -22,6 +22,7 @@
|
||||
"classnames": "^2.3.2",
|
||||
"clsx": "^2.0.0",
|
||||
"codemirror": "^6.0.1",
|
||||
"copy-to-clipboard": "^3.3.3",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-dropzone": "^14.2.3",
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { 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,28 +7,30 @@ import Spinner from "./components/Spinner";
|
||||
import classNames from "classnames";
|
||||
import {
|
||||
FaCode,
|
||||
FaCopy,
|
||||
FaDesktop,
|
||||
FaDownload,
|
||||
FaMobile,
|
||||
FaUndo,
|
||||
} from "react-icons/fa";
|
||||
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 { Settings } from "./types";
|
||||
import { Settings, EditorTheme, AppState } from "./types";
|
||||
import { IS_RUNNING_ON_CLOUD } from "./config";
|
||||
import { PicoBadge } from "./components/PicoBadge";
|
||||
import { OnboardingNote } from "./components/OnboardingNote";
|
||||
import { usePersistedState } from "./hooks/usePersistedState";
|
||||
import { UrlInputSection } from "./components/UrlInputSection";
|
||||
import TermsOfServiceDialog from "./components/TermsOfServiceDialog";
|
||||
import { USER_CLOSE_WEB_SOCKET_CODE } from "./constants";
|
||||
|
||||
function App() {
|
||||
const [appState, setAppState] = useState<"INITIAL" | "CODING" | "CODE_READY">(
|
||||
"INITIAL"
|
||||
);
|
||||
const [appState, setAppState] = useState<AppState>(AppState.INITIAL);
|
||||
const [generatedCode, setGeneratedCode] = useState<string>("");
|
||||
const [referenceImages, setReferenceImages] = useState<string[]>([]);
|
||||
const [executionConsole, setExecutionConsole] = useState<string[]>([]);
|
||||
@ -39,10 +41,11 @@ function App() {
|
||||
openAiApiKey: null,
|
||||
screenshotOneApiKey: null,
|
||||
isImageGenerationEnabled: true,
|
||||
editorTheme: "cobalt",
|
||||
editorTheme: EditorTheme.COBALT,
|
||||
},
|
||||
"setting"
|
||||
);
|
||||
const wsRef = useRef<WebSocket>(null);
|
||||
|
||||
const downloadCode = () => {
|
||||
// Create a blob from the generated code
|
||||
@ -62,26 +65,31 @@ function App() {
|
||||
};
|
||||
|
||||
const reset = () => {
|
||||
setAppState("INITIAL");
|
||||
setAppState(AppState.INITIAL);
|
||||
setGeneratedCode("");
|
||||
setReferenceImages([]);
|
||||
setExecutionConsole([]);
|
||||
setHistory([]);
|
||||
};
|
||||
|
||||
const stop = () => {
|
||||
wsRef.current?.close?.(USER_CLOSE_WEB_SOCKET_CODE);
|
||||
};
|
||||
|
||||
function doGenerateCode(params: CodeGenerationParams) {
|
||||
setExecutionConsole([]);
|
||||
setAppState("CODING");
|
||||
setAppState(AppState.CODING);
|
||||
|
||||
// Merge settings with params
|
||||
const updatedParams = { ...params, ...settings };
|
||||
|
||||
generateCode(
|
||||
wsRef,
|
||||
updatedParams,
|
||||
(token) => setGeneratedCode((prev) => prev + token),
|
||||
(code) => setGeneratedCode(code),
|
||||
(line) => setExecutionConsole((prev) => [...prev, line]),
|
||||
() => setAppState("CODE_READY")
|
||||
() => setAppState(AppState.CODE_READY)
|
||||
);
|
||||
}
|
||||
|
||||
@ -111,6 +119,11 @@ function App() {
|
||||
setUpdateInstruction("");
|
||||
}
|
||||
|
||||
const doCopyCode = useCallback(() => {
|
||||
copy(generatedCode);
|
||||
toast.success("Copied to clipboard");
|
||||
}, [generatedCode]);
|
||||
|
||||
return (
|
||||
<div className="mt-2">
|
||||
{IS_RUNNING_ON_CLOUD && <PicoBadge />}
|
||||
@ -122,7 +135,7 @@ function App() {
|
||||
<h1 className="text-2xl ">Screenshot to Code</h1>
|
||||
<SettingsDialog settings={settings} setSettings={setSettings} />
|
||||
</div>
|
||||
{appState === "INITIAL" && (
|
||||
{appState === AppState.INITIAL && (
|
||||
<h2 className="text-sm text-gray-500 mb-2">
|
||||
Drag & drop a screenshot to get started.
|
||||
</h2>
|
||||
@ -130,20 +143,26 @@ function App() {
|
||||
|
||||
{IS_RUNNING_ON_CLOUD && !settings.openAiApiKey && <OnboardingNote />}
|
||||
|
||||
{(appState === "CODING" || appState === "CODE_READY") && (
|
||||
{(appState === AppState.CODING ||
|
||||
appState === AppState.CODE_READY) && (
|
||||
<>
|
||||
{/* Show code preview only when coding */}
|
||||
{appState === "CODING" && (
|
||||
{appState === AppState.CODING && (
|
||||
<div className="flex flex-col">
|
||||
<div className="flex items-center gap-x-1">
|
||||
<Spinner />
|
||||
{executionConsole.slice(-1)[0]}
|
||||
</div>
|
||||
<div className="flex mt-4 w-full">
|
||||
<Button onClick={stop} className="w-full">
|
||||
Stop
|
||||
</Button>
|
||||
</div>
|
||||
<CodePreview code={generatedCode} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{appState === "CODE_READY" && (
|
||||
{appState === AppState.CODE_READY && (
|
||||
<div>
|
||||
<div className="grid w-full gap-2">
|
||||
<Textarea
|
||||
@ -176,7 +195,7 @@ function App() {
|
||||
<div className="flex flex-col">
|
||||
<div
|
||||
className={classNames({
|
||||
"scanning relative": appState === "CODING",
|
||||
"scanning relative": appState === AppState.CODING,
|
||||
})}
|
||||
>
|
||||
<img
|
||||
@ -209,7 +228,7 @@ function App() {
|
||||
</div>
|
||||
|
||||
<main className="py-2 lg:pl-96">
|
||||
{appState === "INITIAL" && (
|
||||
{appState === AppState.INITIAL && (
|
||||
<div className="flex flex-col justify-center items-center gap-y-10">
|
||||
<ImageUpload setReferenceImages={doCreate} />
|
||||
<UrlInputSection
|
||||
@ -219,7 +238,7 @@ function App() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(appState === "CODING" || appState === "CODE_READY") && (
|
||||
{(appState === AppState.CODING || appState === AppState.CODE_READY) && (
|
||||
<div className="ml-4">
|
||||
<Tabs defaultValue="desktop">
|
||||
<div className="flex justify-end mr-8 mb-4">
|
||||
@ -243,10 +262,20 @@ function App() {
|
||||
<Preview code={generatedCode} device="mobile" />
|
||||
</TabsContent>
|
||||
<TabsContent value="code">
|
||||
<CodeMirror
|
||||
code={generatedCode}
|
||||
editorTheme={settings.editorTheme}
|
||||
/>
|
||||
<div className="relative">
|
||||
<CodeMirror
|
||||
code={generatedCode}
|
||||
editorTheme={settings.editorTheme}
|
||||
onCodeChange={setGeneratedCode}
|
||||
/>
|
||||
<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>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { useRef, useEffect } from "react";
|
||||
import { useRef, useEffect, useMemo } from "react";
|
||||
import { EditorState } from "@codemirror/state";
|
||||
import { EditorView, keymap, lineNumbers } from "@codemirror/view";
|
||||
import { EditorView, keymap, lineNumbers, ViewUpdate } from "@codemirror/view";
|
||||
import { espresso, cobalt } from "thememirror";
|
||||
import {
|
||||
defaultKeymap,
|
||||
@ -11,39 +11,42 @@ import {
|
||||
} from "@codemirror/commands";
|
||||
import { bracketMatching } from "@codemirror/language";
|
||||
import { html } from "@codemirror/lang-html";
|
||||
import { EditorTheme } from "@/types";
|
||||
|
||||
interface Props {
|
||||
code: string;
|
||||
editorTheme: string;
|
||||
editorTheme: EditorTheme;
|
||||
onCodeChange: (code: string) => void;
|
||||
}
|
||||
|
||||
function CodeMirror({ code, editorTheme }: Props) {
|
||||
function CodeMirror({ code, editorTheme, onCodeChange }: Props) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const view = useRef<EditorView | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let selectedTheme = cobalt;
|
||||
if (editorTheme === "espresso") {
|
||||
selectedTheme = espresso;
|
||||
}
|
||||
view.current = new EditorView({
|
||||
state: EditorState.create({
|
||||
doc: code,
|
||||
extensions: [
|
||||
history(),
|
||||
keymap.of([
|
||||
...defaultKeymap,
|
||||
indentWithTab,
|
||||
{ key: "Mod-z", run: undo, preventDefault: true },
|
||||
{ key: "Mod-Shift-z", run: redo, preventDefault: true },
|
||||
]),
|
||||
lineNumbers(),
|
||||
bracketMatching(),
|
||||
html(),
|
||||
selectedTheme,
|
||||
EditorView.lineWrapping,
|
||||
],
|
||||
const editorState = useMemo(() => EditorState.create({
|
||||
extensions: [
|
||||
history(),
|
||||
keymap.of([
|
||||
...defaultKeymap,
|
||||
indentWithTab,
|
||||
{ key: "Mod-z", run: undo, preventDefault: true },
|
||||
{ key: "Mod-Shift-z", run: redo, preventDefault: true },
|
||||
]),
|
||||
lineNumbers(),
|
||||
bracketMatching(),
|
||||
html(),
|
||||
editorTheme === EditorTheme.ESPRESSO ? espresso : cobalt,
|
||||
EditorView.lineWrapping,
|
||||
EditorView.updateListener.of((update: ViewUpdate) => {
|
||||
if (update.docChanged) {
|
||||
const updatedCode = update.state.doc.toString();
|
||||
onCodeChange(updatedCode);
|
||||
}
|
||||
}),
|
||||
],
|
||||
}), [editorTheme]);
|
||||
useEffect(() => {
|
||||
view.current = new EditorView({
|
||||
state: editorState,
|
||||
parent: ref.current as Element,
|
||||
});
|
||||
|
||||
@ -53,7 +56,7 @@ function CodeMirror({ code, editorTheme }: Props) {
|
||||
view.current = null;
|
||||
}
|
||||
};
|
||||
}, [code, editorTheme]);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (view.current && view.current.state.doc.toString() !== code) {
|
||||
@ -69,3 +72,4 @@ function CodeMirror({ code, editorTheme }: Props) {
|
||||
}
|
||||
|
||||
export default CodeMirror;
|
||||
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import classNames from "classnames";
|
||||
import useThrottle from "../hooks/useThrottle";
|
||||
|
||||
@ -8,12 +9,22 @@ interface Props {
|
||||
|
||||
function Preview({ code, device }: Props) {
|
||||
const throttledCode = useThrottle(code, 200);
|
||||
const iframeRef = useRef<HTMLIFrameElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const iframe = iframeRef.current;
|
||||
if (iframe && iframe.contentDocument) {
|
||||
iframe.contentDocument.open();
|
||||
iframe.contentDocument.write(throttledCode);
|
||||
iframe.contentDocument.close();
|
||||
}
|
||||
}, [throttledCode]);
|
||||
|
||||
return (
|
||||
<div className="flex justify-center mx-2">
|
||||
<iframe
|
||||
ref={iframeRef}
|
||||
title="Preview"
|
||||
srcDoc={throttledCode}
|
||||
className={classNames(
|
||||
"border-[4px] border-black rounded-[20px] shadow-lg",
|
||||
"transform scale-[0.9] origin-top",
|
||||
@ -26,4 +37,5 @@ function Preview({ code, device }: Props) {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export default Preview;
|
||||
|
||||
export default Preview;
|
||||
@ -9,7 +9,7 @@ import {
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { FaCog } from "react-icons/fa";
|
||||
import { Settings } from "../types";
|
||||
import { EditorTheme, Settings } from "../types";
|
||||
import { Switch } from "./ui/switch";
|
||||
import { Label } from "./ui/label";
|
||||
import { Input } from "./ui/input";
|
||||
@ -21,7 +21,7 @@ interface Props {
|
||||
}
|
||||
|
||||
function SettingsDialog({ settings, setSettings }: Props) {
|
||||
const handleThemeChange = (theme: string) => {
|
||||
const handleThemeChange = (theme: EditorTheme) => {
|
||||
setSettings((s) => ({
|
||||
...s,
|
||||
editorTheme: theme,
|
||||
@ -110,7 +110,7 @@ function SettingsDialog({ settings, setSettings }: Props) {
|
||||
id="editor-theme"
|
||||
value={settings.editorTheme}
|
||||
onChange={(e: React.ChangeEvent<HTMLSelectElement>) =>
|
||||
handleThemeChange(e.target.value)
|
||||
handleThemeChange(e.target.value as EditorTheme)
|
||||
}
|
||||
>
|
||||
<option value="cobalt">Cobalt</option>
|
||||
|
||||
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,9 +1,12 @@
|
||||
import toast from "react-hot-toast";
|
||||
import { WS_BACKEND_URL } from "./config";
|
||||
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";
|
||||
|
||||
const STOP_MESSAGE = "Code generation stopped";
|
||||
|
||||
export interface CodeGenerationParams {
|
||||
generationType: "create" | "update";
|
||||
image: string;
|
||||
@ -12,6 +15,7 @@ export interface CodeGenerationParams {
|
||||
}
|
||||
|
||||
export function generateCode(
|
||||
wsRef: React.MutableRefObject<WebSocket | null>,
|
||||
params: CodeGenerationParams,
|
||||
onChange: (chunk: string) => void,
|
||||
onSetCode: (code: string) => void,
|
||||
@ -22,6 +26,7 @@ export function generateCode(
|
||||
console.log("Connecting to backend @ ", wsUrl);
|
||||
|
||||
const ws = new WebSocket(wsUrl);
|
||||
wsRef.current = ws;
|
||||
|
||||
ws.addEventListener("open", () => {
|
||||
ws.send(JSON.stringify(params));
|
||||
@ -40,13 +45,16 @@ export function generateCode(
|
||||
toast.error(response.value);
|
||||
}
|
||||
});
|
||||
|
||||
ws.addEventListener("close", (event) => {
|
||||
console.log("Connection closed", event.code, event.reason);
|
||||
if (event.code != 1000) {
|
||||
if (event.code === USER_CLOSE_WEB_SOCKET_CODE) {
|
||||
toast.success(STOP_MESSAGE);
|
||||
onComplete();
|
||||
} else if (event.code === 1000) {
|
||||
onComplete();
|
||||
} else {
|
||||
console.error("WebSocket error code", event);
|
||||
toast.error(ERROR_MESSAGE);
|
||||
} else {
|
||||
onComplete();
|
||||
}
|
||||
});
|
||||
|
||||
@ -1,6 +1,17 @@
|
||||
export enum EditorTheme {
|
||||
ESPRESSO = "espresso",
|
||||
COBALT = "cobalt",
|
||||
}
|
||||
|
||||
export interface Settings {
|
||||
openAiApiKey: string | null;
|
||||
screenshotOneApiKey: string | null;
|
||||
isImageGenerationEnabled: boolean;
|
||||
editorTheme: string;
|
||||
editorTheme: EditorTheme;
|
||||
}
|
||||
|
||||
export enum AppState {
|
||||
INITIAL = "INITIAL",
|
||||
CODING = "CODING",
|
||||
CODE_READY = "CODE_READY",
|
||||
}
|
||||
|
||||
@ -1258,6 +1258,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"
|
||||
@ -2420,6 +2427,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"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user