Merge branch 'main' into pr/43

This commit is contained in:
Abi Raja 2023-11-20 12:05:28 -05:00
commit 55060b866e
11 changed files with 228 additions and 13 deletions

View File

@ -12,6 +12,7 @@ See the [Examples](#examples) section below for more demos.
## 🌟 Recent Updates ## 🌟 Recent Updates
- Nov 20 - Paste in a URL to screenshot and replicate (requires [ScreenshotOne free API key](https://screenshotone.com?via=screenshot-to-code))
- Nov 19 - Support for dark/light code editor theme - thanks https://github.com/kachbit - Nov 19 - Support for dark/light code editor theme - thanks https://github.com/kachbit
- Nov 16 - Added a setting to disable DALL-E image generation if you don't need that - Nov 16 - Added a setting to disable DALL-E image generation if you don't need that
- Nov 16 - View code directly within the app - Nov 16 - View code directly within the app

View File

@ -1,5 +1,6 @@
# Load environment variables first # Load environment variables first
from dotenv import load_dotenv from dotenv import load_dotenv
from pydantic import BaseModel
load_dotenv() load_dotenv()
@ -9,19 +10,35 @@ import os
import traceback import traceback
from datetime import datetime from datetime import datetime
from fastapi import FastAPI, WebSocket from fastapi import FastAPI, WebSocket
from fastapi.middleware.cors import CORSMiddleware
from llm import stream_openai_response from llm import stream_openai_response
from mock import mock_completion from mock import mock_completion
from image_generation import create_alt_url_mapping, generate_images from image_generation import create_alt_url_mapping, generate_images
from prompts import assemble_prompt from prompts import assemble_prompt
from routes import screenshot
app = FastAPI(openapi_url=None, docs_url=None, redoc_url=None)
# Configure CORS
# Configure CORS settings
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
app = FastAPI()
# Useful for debugging purposes when you don't want to waste GPT4-Vision credits # Useful for debugging purposes when you don't want to waste GPT4-Vision credits
# Setting to True will stream a mock response instead of calling the OpenAI API # Setting to True will stream a mock response instead of calling the OpenAI API
SHOULD_MOCK_AI_RESPONSE = False SHOULD_MOCK_AI_RESPONSE = False
app.include_router(screenshot.router)
def write_logs(prompt_messages, completion): def write_logs(prompt_messages, completion):
# Get the logs path from environment, default to the current working directory # Get the logs path from environment, default to the current working directory
logs_path = os.environ.get("LOGS_PATH", os.getcwd()) logs_path = os.environ.get("LOGS_PATH", os.getcwd())

2
backend/poetry.lock generated
View File

@ -471,4 +471,4 @@ files = [
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = "^3.10" python-versions = "^3.10"
content-hash = "5e4aa03dda279f66a9b3d30f7327109bcfd395795470d95f8c563897ce1bff84" content-hash = "b8d248a44a5eea9638a7726096de77d7a9aa8c00673da806534da2c228ffabb4"

View File

@ -13,6 +13,7 @@ websockets = "^12.0"
openai = "^1.2.4" openai = "^1.2.4"
python-dotenv = "^1.0.0" python-dotenv = "^1.0.0"
beautifulsoup4 = "^4.12.2" beautifulsoup4 = "^4.12.2"
httpx = "^0.25.1"
[build-system] [build-system]
requires = ["poetry-core"] requires = ["poetry-core"]

View File

@ -0,0 +1,64 @@
import base64
from fastapi import APIRouter
from pydantic import BaseModel
import httpx
router = APIRouter()
def bytes_to_data_url(image_bytes: bytes, mime_type: str) -> str:
base64_image = base64.b64encode(image_bytes).decode("utf-8")
return f"data:{mime_type};base64,{base64_image}"
async def capture_screenshot(target_url, api_key, device="desktop") -> bytes:
api_base_url = "https://api.screenshotone.com/take"
params = {
"access_key": api_key,
"url": target_url,
"full_page": "true",
"device_scale_factor": "1",
"format": "png",
"block_ads": "true",
"block_cookie_banners": "true",
"block_trackers": "true",
"cache": "false",
"viewport_width": "342",
"viewport_height": "684",
}
if device == "desktop":
params["viewport_width"] = "1280"
params["viewport_height"] = "832"
async with httpx.AsyncClient(timeout=60) as client:
response = await client.get(api_base_url, params=params)
if response.status_code == 200 and response.content:
return response.content
else:
raise Exception("Error taking screenshot")
class ScreenshotRequest(BaseModel):
url: str
apiKey: str
class ScreenshotResponse(BaseModel):
url: str
@router.post("/api/screenshot")
async def app_screenshot(request: ScreenshotRequest):
# Extract the URL from the request body
url = request.url
api_key = request.apiKey
# TODO: Add error handling
image_bytes = await capture_screenshot(url, api_key=api_key)
# Convert the image bytes to a data url
data_url = bytes_to_data_url(image_bytes, "image/png")
return ScreenshotResponse(url=data_url)

View File

@ -22,6 +22,7 @@ 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";
import { usePersistedState } from "./hooks/usePersistedState"; import { usePersistedState } from "./hooks/usePersistedState";
import { UrlInputSection } from "./components/UrlInputSection";
function App() { function App() {
const [appState, setAppState] = useState<"INITIAL" | "CODING" | "CODE_READY">( const [appState, setAppState] = useState<"INITIAL" | "CODING" | "CODE_READY">(
@ -32,11 +33,15 @@ function App() {
const [executionConsole, setExecutionConsole] = useState<string[]>([]); const [executionConsole, setExecutionConsole] = useState<string[]>([]);
const [updateInstruction, setUpdateInstruction] = useState(""); const [updateInstruction, setUpdateInstruction] = useState("");
const [history, setHistory] = useState<string[]>([]); const [history, setHistory] = useState<string[]>([]);
const [settings, setSettings] = usePersistedState<Settings>({ const [settings, setSettings] = usePersistedState<Settings>(
openAiApiKey: null, {
isImageGenerationEnabled: true, openAiApiKey: null,
editorTheme: "cobalt" screenshotOneApiKey: null,
}, 'setting'); isImageGenerationEnabled: true,
editorTheme: "cobalt",
},
"setting"
);
const downloadCode = () => { const downloadCode = () => {
// Create a blob from the generated code // Create a blob from the generated code
@ -203,9 +208,13 @@ function App() {
<main className="py-2 lg:pl-96"> <main className="py-2 lg:pl-96">
{appState === "INITIAL" && ( {appState === "INITIAL" && (
<> <div className="flex flex-col justify-center items-center gap-y-10">
<ImageUpload setReferenceImages={doCreate} /> <ImageUpload setReferenceImages={doCreate} />
</> <UrlInputSection
doCreate={doCreate}
screenshotOneApiKey={settings.screenshotOneApiKey}
/>
</div>
)} )}
{(appState === "CODING" || appState === "CODE_READY") && ( {(appState === "CODING" || appState === "CODE_READY") && (
@ -232,7 +241,10 @@ function App() {
<Preview code={generatedCode} device="mobile" /> <Preview code={generatedCode} device="mobile" />
</TabsContent> </TabsContent>
<TabsContent value="code"> <TabsContent value="code">
<CodeMirror code={generatedCode} editorTheme={settings.editorTheme} /> <CodeMirror
code={generatedCode}
editorTheme={settings.editorTheme}
/>
</TabsContent> </TabsContent>
</Tabs> </Tabs>
</div> </div>

View File

@ -74,6 +74,33 @@ function SettingsDialog({ settings, setSettings }: Props) {
})) }))
} }
/> />
<Label htmlFor="screenshot-one-api-key">
<div>ScreenshotOne API key</div>
<div className="font-light mt-2">
Never stored.{" "}
<a
href="https://screenshotone.com?via=screenshot-to-code"
className="underline"
target="_blank"
>
Get 100 screenshots/mo for free.
</a>
</div>
</Label>
<Input
id="screenshot-one-api-key"
placeholder="ScreenshotOne API key"
value={settings.screenshotOneApiKey || ""}
onChange={(e) =>
setSettings((s) => ({
...s,
screenshotOneApiKey: e.target.value,
}))
}
/>
<Label htmlFor="editor-theme"> <Label htmlFor="editor-theme">
<div>Editor Theme</div> <div>Editor Theme</div>
</Label> </Label>

View File

@ -0,0 +1,87 @@
import { useState } from "react";
import { HTTP_BACKEND_URL } from "../config";
import { Button } from "./ui/button";
import { Input } from "./ui/input";
import { toast } from "react-hot-toast";
interface Props {
screenshotOneApiKey: string | null;
doCreate: (urls: string[]) => void;
}
export function UrlInputSection({ doCreate, screenshotOneApiKey }: Props) {
const [isLoading, setIsLoading] = useState(false);
const [referenceUrl, setReferenceUrl] = useState("");
const isDisabled = !screenshotOneApiKey;
async function takeScreenshot() {
if (!screenshotOneApiKey) {
toast.error("Please add a Screenshot API key in the settings dialog");
return;
}
if (!referenceUrl) {
toast.error("Please enter a URL");
return;
}
if (referenceUrl) {
try {
setIsLoading(true);
const response = await fetch(`${HTTP_BACKEND_URL}/api/screenshot`, {
method: "POST",
body: JSON.stringify({
url: referenceUrl,
apiKey: screenshotOneApiKey,
}),
headers: {
"Content-Type": "application/json",
},
});
if (!response.ok) {
throw new Error("Failed to capture screenshot");
}
const res = await response.json();
doCreate([res.url]);
} catch (error) {
console.error(error);
toast.error(
"Failed to capture screenshot. Look at the console and your backend logs for more details."
);
} finally {
setIsLoading(false);
}
}
}
return (
<div className="w-[400px] gap-y-2 flex flex-col">
<div className="text-gray-500 text-sm">Or screenshot a URL...</div>
<Input
placeholder="Enter URL"
onChange={(e) => setReferenceUrl(e.target.value)}
value={referenceUrl}
/>
<Button onClick={takeScreenshot} disabled={isDisabled || isLoading}>
{isLoading ? "Capturing..." : "Capture"}
</Button>
{isDisabled && (
<div className="flex space-y-4 bg-slate-200 p-2 rounded text-stone-800 text-sm">
<span>
To screenshot a URL, add a{" "}
<a
href="https://screenshotone.com?via=screenshot-to-code"
className="underline"
target="_blank"
>
ScreenshotOne API key
</a>{" "}
in the settings dialog.
</span>
</div>
)}
</div>
);
}

View File

@ -1,3 +1,9 @@
// Default to false if set to anything other than "true" or unset // Default to false if set to anything other than "true" or unset
export const IS_RUNNING_ON_CLOUD = export const IS_RUNNING_ON_CLOUD =
import.meta.env.VITE_IS_DEPLOYED === "true" || false; import.meta.env.VITE_IS_DEPLOYED === "true" || false;
export const WS_BACKEND_URL =
import.meta.env.VITE_WS_BACKEND_URL || "ws://127.0.0.1:7001";
export const HTTP_BACKEND_URL =
import.meta.env.VITE_HTTP_BACKEND_URL || "http://127.0.0.1:7001";

View File

@ -1,7 +1,6 @@
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import { WS_BACKEND_URL } from "./config";
const WS_BACKEND_URL =
import.meta.env.VITE_WS_BACKEND_URL || "ws://127.0.0.1:7001";
const ERROR_MESSAGE = 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";

View File

@ -1,5 +1,6 @@
export interface Settings { export interface Settings {
openAiApiKey: string | null; openAiApiKey: string | null;
screenshotOneApiKey: string | null;
isImageGenerationEnabled: boolean; isImageGenerationEnabled: boolean;
editorTheme: string; editorTheme: string;
} }