Merge main into sweep/add-sweep-config
This commit is contained in:
commit
bbd3b8b1fb
@ -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
2
backend/poetry.lock
generated
@ -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"
|
||||||
|
|||||||
@ -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"]
|
||||||
|
|||||||
64
backend/routes/screenshot.py
Normal file
64
backend/routes/screenshot.py
Normal 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)
|
||||||
@ -21,6 +21,7 @@ import { 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";
|
||||||
|
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">(
|
||||||
@ -33,8 +34,9 @@ function App() {
|
|||||||
const [history, setHistory] = useState<string[]>([]);
|
const [history, setHistory] = useState<string[]>([]);
|
||||||
const [settings, setSettings] = useState<Settings>({
|
const [settings, setSettings] = useState<Settings>({
|
||||||
openAiApiKey: null,
|
openAiApiKey: null,
|
||||||
|
screenshotOneApiKey: null,
|
||||||
isImageGenerationEnabled: true,
|
isImageGenerationEnabled: true,
|
||||||
editorTheme: "cobalt"
|
editorTheme: "cobalt",
|
||||||
});
|
});
|
||||||
|
|
||||||
const downloadCode = () => {
|
const downloadCode = () => {
|
||||||
@ -202,9 +204,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") && (
|
||||||
@ -231,7 +237,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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
87
frontend/src/components/UrlInputSection.tsx
Normal file
87
frontend/src/components/UrlInputSection.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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";
|
||||||
|
|||||||
@ -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";
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user