Merge branch 'main' into hosted
This commit is contained in:
commit
b7d808b227
@ -12,7 +12,8 @@ See the [Examples](#examples) section below for more demos.
|
||||
|
||||
## 🌟 Recent Updates
|
||||
|
||||
- Nov 28 - 🔥 🔥 🔥 Get output code in React or Bootstrap or TailwindCSS
|
||||
- Nov 30 - Dark mode, output code in Ionic (thanks [@dialmedu](https://github.com/dialmedu)), set OpenAI base URL
|
||||
- Nov 28 - 🔥 🔥 🔥 Customize your stack: React or Bootstrap or TailwindCSS
|
||||
- Nov 23 - Send in a screenshot of the current replicated version (sometimes improves quality of subsequent generations)
|
||||
- Nov 21 - Edit code in the code editor and preview changes live thanks to [@clean99](https://github.com/clean99)
|
||||
- Nov 20 - Paste in a URL to screenshot and clone (requires [ScreenshotOne free API key](https://screenshotone.com?via=screenshot-to-code))
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
### Getting an OpenAI API key
|
||||
### Getting an OpenAI API key with GPT4-Vision model access
|
||||
|
||||
You don't need a ChatGPT Pro account. Screenshot to code uses API keys from your OpenAI developer account. In order to get access to the GPT4 Vision model, log into your OpenAI account and then, follow these instructions:
|
||||
|
||||
@ -10,6 +10,7 @@ You don't need a ChatGPT Pro account. Screenshot to code uses API keys from your
|
||||
|
||||
5. Go to Settings > Limits and check at the bottom of the page, your current tier has to be "Tier 1" to have GPT4 access
|
||||
<img width="785" alt="285636973-da38bd4d-8a78-4904-8027-ca67d729b933" src="https://github.com/abi/screenshot-to-code/assets/23818/8d07cd84-0cf9-4f88-bc00-80eba492eadf">
|
||||
6. Go to Screenshot to code and paste it in the Settings dialog under OpenAI key (gear icon). Your key is only stored in your browser. Never stored on our servers.
|
||||
|
||||
Some users have also reported that it can take upto 30 minutes after your credit purchase for the GPT4 vision model to be activated.
|
||||
|
||||
|
||||
@ -5,8 +5,8 @@ from openai import AsyncOpenAI
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
|
||||
async def process_tasks(prompts, api_key):
|
||||
tasks = [generate_image(prompt, api_key) for prompt in prompts]
|
||||
async def process_tasks(prompts, api_key, base_url):
|
||||
tasks = [generate_image(prompt, api_key, base_url) for prompt in prompts]
|
||||
results = await asyncio.gather(*tasks, return_exceptions=True)
|
||||
|
||||
processed_results = []
|
||||
@ -20,8 +20,8 @@ async def process_tasks(prompts, api_key):
|
||||
return processed_results
|
||||
|
||||
|
||||
async def generate_image(prompt, api_key):
|
||||
client = AsyncOpenAI(api_key=api_key)
|
||||
async def generate_image(prompt, api_key, base_url):
|
||||
client = AsyncOpenAI(api_key=api_key, base_url=base_url)
|
||||
image_params = {
|
||||
"model": "dall-e-3",
|
||||
"quality": "standard",
|
||||
@ -60,7 +60,7 @@ def create_alt_url_mapping(code):
|
||||
return mapping
|
||||
|
||||
|
||||
async def generate_images(code, api_key, image_cache):
|
||||
async def generate_images(code, api_key, base_url, image_cache):
|
||||
# Find all images
|
||||
soup = BeautifulSoup(code, "html.parser")
|
||||
images = soup.find_all("img")
|
||||
@ -87,7 +87,7 @@ async def generate_images(code, api_key, image_cache):
|
||||
return code
|
||||
|
||||
# Generate images
|
||||
results = await process_tasks(prompts, api_key)
|
||||
results = await process_tasks(prompts, api_key, base_url)
|
||||
|
||||
# Create a dict mapping alt text to image URL
|
||||
mapped_image_urls = dict(zip(prompts, results))
|
||||
|
||||
@ -6,9 +6,12 @@ MODEL_GPT_4_VISION = "gpt-4-vision-preview"
|
||||
|
||||
|
||||
async def stream_openai_response(
|
||||
messages, api_key: str, callback: Callable[[str], Awaitable[None]]
|
||||
messages,
|
||||
api_key: str,
|
||||
base_url: str | None,
|
||||
callback: Callable[[str], Awaitable[None]],
|
||||
):
|
||||
client = AsyncOpenAI(api_key=api_key)
|
||||
client = AsyncOpenAI(api_key=api_key, base_url=base_url)
|
||||
|
||||
model = MODEL_GPT_4_VISION
|
||||
|
||||
|
||||
@ -69,13 +69,11 @@ async def stream_code(websocket: WebSocket):
|
||||
|
||||
print("Received params")
|
||||
|
||||
# Read the output settings from the request. Fall back to default if not provided.
|
||||
output_settings = {"css": "tailwind", "js": "vanilla"}
|
||||
if params["outputSettings"] and params["outputSettings"]["css"]:
|
||||
output_settings["css"] = params["outputSettings"]["css"]
|
||||
if params["outputSettings"] and params["outputSettings"]["js"]:
|
||||
output_settings["js"] = params["outputSettings"]["js"]
|
||||
print("Using output settings:", output_settings)
|
||||
# Read the code config settings from the request. Fall back to default if not provided.
|
||||
generated_code_config = ""
|
||||
if "generatedCodeConfig" in params and params["generatedCodeConfig"]:
|
||||
generated_code_config = params["generatedCodeConfig"]
|
||||
print(f"Generating {generated_code_config} code")
|
||||
|
||||
# Get the OpenAI API key from the request. Fall back to environment variable if not provided.
|
||||
# If neither is provided, we throw an error.
|
||||
@ -111,6 +109,22 @@ async def stream_code(websocket: WebSocket):
|
||||
)
|
||||
return
|
||||
|
||||
# Get the OpenAI Base URL from the request. Fall back to environment variable if not provided.
|
||||
openai_base_url = None
|
||||
# Disable user-specified OpenAI Base URL in prod
|
||||
if not os.environ.get("IS_PROD"):
|
||||
if "openAiBaseURL" in params and params["openAiBaseURL"]:
|
||||
openai_base_url = params["openAiBaseURL"]
|
||||
print("Using OpenAI Base URL from client-side settings dialog")
|
||||
else:
|
||||
openai_base_url = os.environ.get("OPENAI_BASE_URL")
|
||||
if openai_base_url:
|
||||
print("Using OpenAI Base URL from environment variable")
|
||||
|
||||
if not openai_base_url:
|
||||
print("Using official OpenAI URL")
|
||||
|
||||
# Get the image generation flag from the request. Fall back to True if not provided.
|
||||
should_generate_images = (
|
||||
params["isImageGenerationEnabled"]
|
||||
if "isImageGenerationEnabled" in params
|
||||
@ -123,12 +137,23 @@ async def stream_code(websocket: WebSocket):
|
||||
async def process_chunk(content):
|
||||
await websocket.send_json({"type": "chunk", "value": content})
|
||||
|
||||
if params.get("resultImage") and params["resultImage"]:
|
||||
prompt_messages = assemble_prompt(
|
||||
params["image"], output_settings, params["resultImage"]
|
||||
# Assemble the prompt
|
||||
try:
|
||||
if params.get("resultImage") and params["resultImage"]:
|
||||
prompt_messages = assemble_prompt(
|
||||
params["image"], generated_code_config, params["resultImage"]
|
||||
)
|
||||
else:
|
||||
prompt_messages = assemble_prompt(params["image"], generated_code_config)
|
||||
except:
|
||||
await websocket.send_json(
|
||||
{
|
||||
"type": "error",
|
||||
"value": "Error assembling prompt. Contact support at support@picoapps.xyz",
|
||||
}
|
||||
)
|
||||
else:
|
||||
prompt_messages = assemble_prompt(params["image"], output_settings)
|
||||
await websocket.close()
|
||||
return
|
||||
|
||||
# Image cache for updates so that we don't have to regenerate images
|
||||
image_cache = {}
|
||||
@ -149,6 +174,7 @@ async def stream_code(websocket: WebSocket):
|
||||
completion = await stream_openai_response(
|
||||
prompt_messages,
|
||||
api_key=openai_api_key,
|
||||
base_url=openai_base_url,
|
||||
callback=lambda x: process_chunk(x),
|
||||
)
|
||||
|
||||
@ -161,7 +187,10 @@ async def stream_code(websocket: WebSocket):
|
||||
{"type": "status", "value": "Generating images..."}
|
||||
)
|
||||
updated_html = await generate_images(
|
||||
completion, api_key=openai_api_key, image_cache=image_cache
|
||||
completion,
|
||||
api_key=openai_api_key,
|
||||
base_url=openai_base_url,
|
||||
image_cache=image_cache,
|
||||
)
|
||||
else:
|
||||
updated_html = completion
|
||||
|
||||
@ -77,23 +77,60 @@ Return only the full code in <html></html> tags.
|
||||
Do not include markdown "```" or "```html" at the start or end.
|
||||
"""
|
||||
|
||||
IONIC_TAILWIND_SYSTEM_PROMPT = """
|
||||
You are an expert Ionic/Tailwind developer
|
||||
You take screenshots of a reference web page from the user, and then build single page apps
|
||||
using Ionic and Tailwind CSS.
|
||||
You might also be given a screenshot(The second image) of a web page that you have already built, and asked to
|
||||
update it to look more like the reference image(The first image).
|
||||
|
||||
- Make sure the app looks exactly like the screenshot.
|
||||
- Pay close attention to background color, text color, font size, font family,
|
||||
padding, margin, border, etc. Match the colors and sizes exactly.
|
||||
- Use the exact text from the screenshot.
|
||||
- Do not add comments in the code such as "<!-- Add other navigation links as needed -->" and "<!-- ... other news items ... -->" in place of writing the full code. WRITE THE FULL CODE.
|
||||
- Repeat elements as needed to match the screenshot. For example, if there are 15 items, the code should have 15 items. DO NOT LEAVE comments like "<!-- Repeat for each news item -->" or bad things will happen.
|
||||
- For images, use placeholder images from https://placehold.co and include a detailed description of the image in the alt text so that an image generation AI can generate the image later.
|
||||
|
||||
In terms of libraries,
|
||||
|
||||
- Use these script to include Ionic so that it can run on a standalone page:
|
||||
<script type="module" src="https://cdn.jsdelivr.net/npm/@ionic/core/dist/ionic/ionic.esm.js"></script>
|
||||
<script nomodule src="https://cdn.jsdelivr.net/npm/@ionic/core/dist/ionic/ionic.js"></script>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@ionic/core/css/ionic.bundle.css" />
|
||||
- Use this script to include Tailwind: <script src="https://cdn.tailwindcss.com"></script>
|
||||
- You can use Google Fonts
|
||||
- ionicons for icons, add the following <script > tags near the end of the page, right before the closing </body> tag:
|
||||
<script type="module">
|
||||
import ionicons from 'https://cdn.jsdelivr.net/npm/ionicons/+esm'
|
||||
</script>
|
||||
<script nomodule src="https://cdn.jsdelivr.net/npm/ionicons/dist/esm/ionicons.min.js"></script>
|
||||
<link href="https://cdn.jsdelivr.net/npm/ionicons/dist/collection/components/icon/icon.min.css" rel="stylesheet">
|
||||
|
||||
Return only the full code in <html></html> tags.
|
||||
Do not include markdown "```" or "```html" at the start or end.
|
||||
"""
|
||||
|
||||
USER_PROMPT = """
|
||||
Generate code for a web page that looks exactly like this.
|
||||
"""
|
||||
|
||||
|
||||
def assemble_prompt(image_data_url, output_settings: dict, result_image_data_url=None):
|
||||
def assemble_prompt(
|
||||
image_data_url, generated_code_config: str, result_image_data_url=None
|
||||
):
|
||||
# Set the system prompt based on the output settings
|
||||
chosen_prompt_name = "tailwind"
|
||||
system_content = TAILWIND_SYSTEM_PROMPT
|
||||
if output_settings["css"] == "bootstrap":
|
||||
chosen_prompt_name = "bootstrap"
|
||||
system_content = BOOTSTRAP_SYSTEM_PROMPT
|
||||
if output_settings["js"] == "react":
|
||||
chosen_prompt_name = "react-tailwind"
|
||||
if generated_code_config == "html_tailwind":
|
||||
system_content = TAILWIND_SYSTEM_PROMPT
|
||||
elif generated_code_config == "react_tailwind":
|
||||
system_content = REACT_TAILWIND_SYSTEM_PROMPT
|
||||
|
||||
print("Using system prompt:", chosen_prompt_name)
|
||||
elif generated_code_config == "bootstrap":
|
||||
system_content = BOOTSTRAP_SYSTEM_PROMPT
|
||||
elif generated_code_config == "ionic_tailwind":
|
||||
system_content = IONIC_TAILWIND_SYSTEM_PROMPT
|
||||
else:
|
||||
raise Exception("Code config is not one of available options")
|
||||
|
||||
user_content = [
|
||||
{
|
||||
|
||||
@ -79,19 +79,58 @@ Return only the full code in <html></html> tags.
|
||||
Do not include markdown "```" or "```html" at the start or end.
|
||||
"""
|
||||
|
||||
IONIC_TAILWIND_SYSTEM_PROMPT = """
|
||||
You are an expert Ionic/Tailwind developer
|
||||
You take screenshots of a reference web page from the user, and then build single page apps
|
||||
using Ionic and Tailwind CSS.
|
||||
You might also be given a screenshot(The second image) of a web page that you have already built, and asked to
|
||||
update it to look more like the reference image(The first image).
|
||||
|
||||
- Make sure the app looks exactly like the screenshot.
|
||||
- Pay close attention to background color, text color, font size, font family,
|
||||
padding, margin, border, etc. Match the colors and sizes exactly.
|
||||
- Use the exact text from the screenshot.
|
||||
- Do not add comments in the code such as "<!-- Add other navigation links as needed -->" and "<!-- ... other news items ... -->" in place of writing the full code. WRITE THE FULL CODE.
|
||||
- Repeat elements as needed to match the screenshot. For example, if there are 15 items, the code should have 15 items. DO NOT LEAVE comments like "<!-- Repeat for each news item -->" or bad things will happen.
|
||||
- For images, use placeholder images from https://placehold.co and include a detailed description of the image in the alt text so that an image generation AI can generate the image later.
|
||||
|
||||
In terms of libraries,
|
||||
|
||||
- Use these script to include Ionic so that it can run on a standalone page:
|
||||
<script type="module" src="https://cdn.jsdelivr.net/npm/@ionic/core/dist/ionic/ionic.esm.js"></script>
|
||||
<script nomodule src="https://cdn.jsdelivr.net/npm/@ionic/core/dist/ionic/ionic.js"></script>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@ionic/core/css/ionic.bundle.css" />
|
||||
- Use this script to include Tailwind: <script src="https://cdn.tailwindcss.com"></script>
|
||||
- You can use Google Fonts
|
||||
- ionicons for icons, add the following <script > tags near the end of the page, right before the closing </body> tag:
|
||||
<script type="module">
|
||||
import ionicons from 'https://cdn.jsdelivr.net/npm/ionicons/+esm'
|
||||
</script>
|
||||
<script nomodule src="https://cdn.jsdelivr.net/npm/ionicons/dist/esm/ionicons.min.js"></script>
|
||||
<link href="https://cdn.jsdelivr.net/npm/ionicons/dist/collection/components/icon/icon.min.css" rel="stylesheet">
|
||||
|
||||
Return only the full code in <html></html> tags.
|
||||
Do not include markdown "```" or "```html" at the start or end.
|
||||
"""
|
||||
|
||||
|
||||
def test_prompts():
|
||||
tailwind_prompt = assemble_prompt(
|
||||
"image_data_url", {"css": "tailwind", "js": "vanilla"}, "result_image_data_url"
|
||||
"image_data_url", "html_tailwind", "result_image_data_url"
|
||||
)
|
||||
assert tailwind_prompt[0]["content"] == TAILWIND_SYSTEM_PROMPT
|
||||
|
||||
react_tailwind_prompt = assemble_prompt(
|
||||
"image_data_url", "react_tailwind", "result_image_data_url"
|
||||
)
|
||||
assert react_tailwind_prompt[0]["content"] == REACT_TAILWIND_SYSTEM_PROMPT
|
||||
|
||||
bootstrap_prompt = assemble_prompt(
|
||||
"image_data_url", {"css": "bootstrap", "js": "vanilla"}, "result_image_data_url"
|
||||
"image_data_url", "bootstrap", "result_image_data_url"
|
||||
)
|
||||
assert bootstrap_prompt[0]["content"] == BOOTSTRAP_SYSTEM_PROMPT
|
||||
|
||||
react_tailwind_prompt = assemble_prompt(
|
||||
"image_data_url", {"css": "tailwind", "js": "react"}, "result_image_data_url"
|
||||
ionic_tailwind = assemble_prompt(
|
||||
"image_data_url", "ionic_tailwind", "result_image_data_url"
|
||||
)
|
||||
assert react_tailwind_prompt[0]["content"] == REACT_TAILWIND_SYSTEM_PROMPT
|
||||
assert ionic_tailwind[0]["content"] == IONIC_TAILWIND_SYSTEM_PROMPT
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { useRef, useState } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import ImageUpload from "./components/ImageUpload";
|
||||
import CodePreview from "./components/CodePreview";
|
||||
import Preview from "./components/Preview";
|
||||
@ -18,14 +18,7 @@ import { Button } from "@/components/ui/button";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "./components/ui/tabs";
|
||||
import SettingsDialog from "./components/SettingsDialog";
|
||||
import {
|
||||
Settings,
|
||||
EditorTheme,
|
||||
AppState,
|
||||
CSSOption,
|
||||
OutputSettings,
|
||||
JSFrameworkOption,
|
||||
} from "./types";
|
||||
import { Settings, EditorTheme, AppState, GeneratedCodeConfig } from "./types";
|
||||
import { IS_RUNNING_ON_CLOUD } from "./config";
|
||||
import { PicoBadge } from "./components/PicoBadge";
|
||||
import { OnboardingNote } from "./components/OnboardingNote";
|
||||
@ -47,23 +40,35 @@ function App() {
|
||||
const [settings, setSettings] = usePersistedState<Settings>(
|
||||
{
|
||||
openAiApiKey: null,
|
||||
openAiBaseURL: null,
|
||||
screenshotOneApiKey: null,
|
||||
isImageGenerationEnabled: true,
|
||||
editorTheme: EditorTheme.COBALT,
|
||||
generatedCodeConfig: GeneratedCodeConfig.HTML_TAILWIND,
|
||||
// Only relevant for hosted version
|
||||
isTermOfServiceAccepted: false,
|
||||
accessCode: null,
|
||||
},
|
||||
"setting"
|
||||
);
|
||||
const [outputSettings, setOutputSettings] = useState<OutputSettings>({
|
||||
css: CSSOption.TAILWIND,
|
||||
js: JSFrameworkOption.NO_FRAMEWORK,
|
||||
});
|
||||
|
||||
const [shouldIncludeResultImage, setShouldIncludeResultImage] =
|
||||
useState<boolean>(false);
|
||||
|
||||
const wsRef = useRef<WebSocket>(null);
|
||||
|
||||
// When the user already has the settings in local storage, newly added keys
|
||||
// do not get added to the settings so if it's falsy, we populate it with the default
|
||||
// value
|
||||
useEffect(() => {
|
||||
if (!settings.generatedCodeConfig) {
|
||||
setSettings((prev) => ({
|
||||
...prev,
|
||||
generatedCodeConfig: GeneratedCodeConfig.HTML_TAILWIND,
|
||||
}));
|
||||
}
|
||||
}, [settings.generatedCodeConfig, setSettings]);
|
||||
|
||||
const takeScreenshot = async (): Promise<string> => {
|
||||
const iframeElement = document.querySelector(
|
||||
"#preview-desktop"
|
||||
@ -119,7 +124,7 @@ function App() {
|
||||
setAppState(AppState.CODING);
|
||||
|
||||
// Merge settings with params
|
||||
const updatedParams = { ...params, ...settings, outputSettings };
|
||||
const updatedParams = { ...params, ...settings };
|
||||
|
||||
generateCode(
|
||||
wsRef,
|
||||
@ -177,7 +182,7 @@ function App() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mt-2">
|
||||
<div className="mt-2 dark:bg-black dark:text-white">
|
||||
{IS_RUNNING_ON_CLOUD && <PicoBadge settings={settings} />}
|
||||
{IS_RUNNING_ON_CLOUD && (
|
||||
<TermsOfServiceDialog
|
||||
@ -185,17 +190,21 @@ function App() {
|
||||
onOpenChange={handleTermDialogOpenChange}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="lg:fixed lg:inset-y-0 lg:z-40 lg:flex lg:w-96 lg:flex-col">
|
||||
<div className="flex grow flex-col gap-y-2 overflow-y-auto border-r border-gray-200 bg-white px-6">
|
||||
<div className="flex grow flex-col gap-y-2 overflow-y-auto border-r border-gray-200 bg-white px-6 dark:bg-zinc-950 dark:text-white">
|
||||
<div className="flex items-center justify-between mt-10 mb-2">
|
||||
<h1 className="text-2xl ">Screenshot to Code</h1>
|
||||
<SettingsDialog settings={settings} setSettings={setSettings} />
|
||||
</div>
|
||||
|
||||
<OutputSettingsSection
|
||||
outputSettings={outputSettings}
|
||||
setOutputSettings={setOutputSettings}
|
||||
generatedCodeConfig={settings.generatedCodeConfig}
|
||||
setGeneratedCodeConfig={(config: GeneratedCodeConfig) =>
|
||||
setSettings((prev) => ({
|
||||
...prev,
|
||||
generatedCodeConfig: config,
|
||||
}))
|
||||
}
|
||||
shouldDisableUpdates={
|
||||
appState === AppState.CODING || appState === AppState.CODE_READY
|
||||
}
|
||||
@ -217,7 +226,10 @@ function App() {
|
||||
{executionConsole.slice(-1)[0]}
|
||||
</div>
|
||||
<div className="flex mt-4 w-full">
|
||||
<Button onClick={stop} className="w-full">
|
||||
<Button
|
||||
onClick={stop}
|
||||
className="w-full dark:text-white dark:bg-gray-700"
|
||||
>
|
||||
Stop
|
||||
</Button>
|
||||
</div>
|
||||
@ -234,26 +246,32 @@ function App() {
|
||||
value={updateInstruction}
|
||||
/>
|
||||
<div className="flex justify-between items-center gap-x-2">
|
||||
<div className="font-500 text-xs text-slate-700">
|
||||
<div className="font-500 text-xs text-slate-700 dark:text-white">
|
||||
Include screenshot of current version?
|
||||
</div>
|
||||
<Switch
|
||||
checked={shouldIncludeResultImage}
|
||||
onCheckedChange={setShouldIncludeResultImage}
|
||||
className="dark:bg-gray-700"
|
||||
/>
|
||||
</div>
|
||||
<Button onClick={doUpdate}>Update</Button>
|
||||
<Button
|
||||
onClick={doUpdate}
|
||||
className="dark:text-white dark:bg-gray-700"
|
||||
>
|
||||
Update
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex items-center gap-x-2 mt-2">
|
||||
<Button
|
||||
onClick={downloadCode}
|
||||
className="flex items-center gap-x-2"
|
||||
className="flex items-center gap-x-2 dark:text-white dark:bg-gray-700"
|
||||
>
|
||||
<FaDownload /> Download
|
||||
</Button>
|
||||
<Button
|
||||
onClick={reset}
|
||||
className="flex items-center gap-x-2"
|
||||
className="flex items-center gap-x-2 dark:text-white dark:bg-gray-700"
|
||||
>
|
||||
<FaUndo />
|
||||
Reset
|
||||
|
||||
@ -5,201 +5,86 @@ import {
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
} from "./ui/select";
|
||||
import { CSSOption, JSFrameworkOption, OutputSettings } from "../types";
|
||||
import toast from "react-hot-toast";
|
||||
import { Label } from "@radix-ui/react-label";
|
||||
import { Button } from "./ui/button";
|
||||
import { Popover, PopoverTrigger, PopoverContent } from "./ui/popover";
|
||||
import { GeneratedCodeConfig } from "../types";
|
||||
|
||||
function displayCSSOption(option: CSSOption) {
|
||||
switch (option) {
|
||||
case CSSOption.TAILWIND:
|
||||
return "Tailwind";
|
||||
case CSSOption.BOOTSTRAP:
|
||||
return "Bootstrap";
|
||||
function generateDisplayComponent(config: GeneratedCodeConfig) {
|
||||
switch (config) {
|
||||
case GeneratedCodeConfig.HTML_TAILWIND:
|
||||
return (
|
||||
<div>
|
||||
<span className="font-semibold">HTML</span> +{" "}
|
||||
<span className="font-semibold">Tailwind</span>
|
||||
</div>
|
||||
);
|
||||
case GeneratedCodeConfig.REACT_TAILWIND:
|
||||
return (
|
||||
<div>
|
||||
<span className="font-semibold">React</span> +{" "}
|
||||
<span className="font-semibold">Tailwind</span>
|
||||
</div>
|
||||
);
|
||||
case GeneratedCodeConfig.BOOTSTRAP:
|
||||
return (
|
||||
<div>
|
||||
<span className="font-semibold">Bootstrap</span>
|
||||
</div>
|
||||
);
|
||||
case GeneratedCodeConfig.IONIC_TAILWIND:
|
||||
return (
|
||||
<div>
|
||||
<span className="font-semibold">Ionic</span> +{" "}
|
||||
<span className="font-semibold">Tailwind</span>
|
||||
</div>
|
||||
);
|
||||
default:
|
||||
return option;
|
||||
}
|
||||
}
|
||||
|
||||
function displayJSOption(option: JSFrameworkOption) {
|
||||
switch (option) {
|
||||
case JSFrameworkOption.REACT:
|
||||
return "React";
|
||||
case JSFrameworkOption.NO_FRAMEWORK:
|
||||
return "No Framework";
|
||||
default:
|
||||
return option;
|
||||
}
|
||||
}
|
||||
|
||||
function convertStringToCSSOption(option: string) {
|
||||
switch (option) {
|
||||
case "tailwind":
|
||||
return CSSOption.TAILWIND;
|
||||
case "bootstrap":
|
||||
return CSSOption.BOOTSTRAP;
|
||||
default:
|
||||
throw new Error(`Unknown CSS option: ${option}`);
|
||||
}
|
||||
}
|
||||
|
||||
function generateDisplayString(settings: OutputSettings) {
|
||||
if (
|
||||
settings.js === JSFrameworkOption.REACT &&
|
||||
settings.css === CSSOption.TAILWIND
|
||||
) {
|
||||
return (
|
||||
<div>
|
||||
Generating <span className="font-bold">React</span> +{" "}
|
||||
<span className="font-bold">Tailwind</span> code
|
||||
</div>
|
||||
);
|
||||
} else if (
|
||||
settings.js === JSFrameworkOption.NO_FRAMEWORK &&
|
||||
settings.css === CSSOption.TAILWIND
|
||||
) {
|
||||
return (
|
||||
<div className="text-gray-800">
|
||||
Generating <span className="font-bold">HTML</span> +{" "}
|
||||
<span className="font-bold">Tailwind</span> code
|
||||
</div>
|
||||
);
|
||||
} else if (
|
||||
settings.js === JSFrameworkOption.NO_FRAMEWORK &&
|
||||
settings.css === CSSOption.BOOTSTRAP
|
||||
) {
|
||||
return (
|
||||
<div>
|
||||
Generating <span className="font-bold">HTML</span> +{" "}
|
||||
<span className="font-bold">Bootstrap</span> code
|
||||
</div>
|
||||
);
|
||||
// TODO: Should never reach this out. Error out
|
||||
return config;
|
||||
}
|
||||
}
|
||||
|
||||
interface Props {
|
||||
outputSettings: OutputSettings;
|
||||
setOutputSettings: React.Dispatch<React.SetStateAction<OutputSettings>>;
|
||||
generatedCodeConfig: GeneratedCodeConfig;
|
||||
setGeneratedCodeConfig: (config: GeneratedCodeConfig) => void;
|
||||
shouldDisableUpdates?: boolean;
|
||||
}
|
||||
|
||||
function OutputSettingsSection({
|
||||
outputSettings,
|
||||
setOutputSettings,
|
||||
generatedCodeConfig,
|
||||
setGeneratedCodeConfig,
|
||||
shouldDisableUpdates = false,
|
||||
}: Props) {
|
||||
const onCSSValueChange = (value: string) => {
|
||||
window.plausible("OutputSettings", {
|
||||
props: { framework: "CSS", value: value },
|
||||
});
|
||||
setOutputSettings((prev) => {
|
||||
if (prev.js === JSFrameworkOption.REACT) {
|
||||
if (value !== CSSOption.TAILWIND) {
|
||||
toast.error(
|
||||
'React only supports Tailwind CSS. Change JS framework to "No Framework" to use Bootstrap.'
|
||||
);
|
||||
}
|
||||
return {
|
||||
css: CSSOption.TAILWIND,
|
||||
js: JSFrameworkOption.REACT,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
...prev,
|
||||
css: convertStringToCSSOption(value),
|
||||
};
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const onJsFrameworkChange = (value: string) => {
|
||||
window.plausible("OutputSettings", {
|
||||
props: { framework: "JS", value: value },
|
||||
});
|
||||
if (value === JSFrameworkOption.REACT) {
|
||||
setOutputSettings(() => ({
|
||||
css: CSSOption.TAILWIND,
|
||||
js: value as JSFrameworkOption,
|
||||
}));
|
||||
} else {
|
||||
setOutputSettings((prev) => ({
|
||||
...prev,
|
||||
js: value as JSFrameworkOption,
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-2 justify-between text-sm">
|
||||
{generateDisplayString(outputSettings)}{" "}
|
||||
{!shouldDisableUpdates && (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="outline">Customize</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-80 text-sm">
|
||||
<div className="grid gap-4">
|
||||
<div className="space-y-2">
|
||||
<h4 className="font-medium leading-none">Code Settings</h4>
|
||||
<p className="text-muted-foreground">
|
||||
Customize your code output
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<div className="grid grid-cols-3 items-center gap-4">
|
||||
<Label htmlFor="output-settings-js">JS</Label>
|
||||
<Select
|
||||
value={outputSettings.js}
|
||||
onValueChange={onJsFrameworkChange}
|
||||
>
|
||||
<SelectTrigger
|
||||
className="col-span-2 h-8"
|
||||
id="output-settings-js"
|
||||
>
|
||||
{displayJSOption(outputSettings.js)}
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem value={JSFrameworkOption.NO_FRAMEWORK}>
|
||||
No Framework
|
||||
</SelectItem>
|
||||
<SelectItem value={JSFrameworkOption.REACT}>
|
||||
React
|
||||
</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 items-center gap-4">
|
||||
<Label htmlFor="output-settings-css">CSS</Label>
|
||||
<Select
|
||||
value={outputSettings.css}
|
||||
onValueChange={onCSSValueChange}
|
||||
>
|
||||
<SelectTrigger
|
||||
className="col-span-2 h-8"
|
||||
id="output-settings-css"
|
||||
>
|
||||
{displayCSSOption(outputSettings.css)}
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem value={CSSOption.TAILWIND}>
|
||||
Tailwind
|
||||
</SelectItem>
|
||||
<SelectItem value={CSSOption.BOOTSTRAP}>
|
||||
Bootstrap
|
||||
</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)}
|
||||
<div className="grid grid-cols-3 items-center gap-4">
|
||||
<span>Generating:</span>
|
||||
<Select
|
||||
value={generatedCodeConfig}
|
||||
onValueChange={(value: string) =>
|
||||
setGeneratedCodeConfig(value as GeneratedCodeConfig)
|
||||
}
|
||||
disabled={shouldDisableUpdates}
|
||||
>
|
||||
<SelectTrigger className="col-span-2" id="output-settings-js">
|
||||
{generateDisplayComponent(generatedCodeConfig)}
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem value={GeneratedCodeConfig.HTML_TAILWIND}>
|
||||
{generateDisplayComponent(GeneratedCodeConfig.HTML_TAILWIND)}
|
||||
</SelectItem>
|
||||
<SelectItem value={GeneratedCodeConfig.REACT_TAILWIND}>
|
||||
{generateDisplayComponent(GeneratedCodeConfig.REACT_TAILWIND)}
|
||||
</SelectItem>
|
||||
<SelectItem value={GeneratedCodeConfig.BOOTSTRAP}>
|
||||
{generateDisplayComponent(GeneratedCodeConfig.BOOTSTRAP)}
|
||||
</SelectItem>
|
||||
<SelectItem value={GeneratedCodeConfig.IONIC_TAILWIND}>
|
||||
{generateDisplayComponent(GeneratedCodeConfig.IONIC_TAILWIND)}
|
||||
</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -16,6 +16,12 @@ import { Input } from "./ui/input";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger } from "./ui/select";
|
||||
import { capitalize } from "../lib/utils";
|
||||
import { IS_RUNNING_ON_CLOUD } from "../config";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "./ui/accordion";
|
||||
|
||||
interface Props {
|
||||
settings: Settings;
|
||||
@ -42,7 +48,7 @@ function SettingsDialog({ settings, setSettings }: Props) {
|
||||
|
||||
{/* Access code */}
|
||||
{IS_RUNNING_ON_CLOUD && (
|
||||
<div className="flex flex-col space-y-4 bg-slate-300 p-4 rounded">
|
||||
<div className="flex flex-col space-y-4 bg-slate-300 p-4 rounded dark:text-white dark:bg-slate-800">
|
||||
<Label htmlFor="access-code">
|
||||
<div>Access Code</div>
|
||||
<div className="font-light mt-1 leading-relaxed">
|
||||
@ -51,7 +57,7 @@ function SettingsDialog({ settings, setSettings }: Props) {
|
||||
</Label>
|
||||
|
||||
<Input
|
||||
id="access-code"
|
||||
id="access-code dark:border-gray-700 dark:bg-gray-800 dark:text-white"
|
||||
placeholder="Enter your Screenshot to Code access code"
|
||||
value={settings.accessCode || ""}
|
||||
onChange={(e) =>
|
||||
@ -103,53 +109,115 @@ function SettingsDialog({ settings, setSettings }: Props) {
|
||||
}
|
||||
/>
|
||||
|
||||
<Label htmlFor="screenshot-one-api-key">
|
||||
<div>
|
||||
ScreenshotOne API key (optional - only needed if you want to use
|
||||
URLs directly instead of taking the screenshot yourself)
|
||||
</div>
|
||||
<div className="font-light mt-2 leading-relaxed">
|
||||
Only stored in your browser. Never stored on servers.{" "}
|
||||
<a
|
||||
href="https://screenshotone.com?via=screenshot-to-code"
|
||||
className="underline"
|
||||
target="_blank"
|
||||
>
|
||||
Get 100 screenshots/mo for free.
|
||||
</a>
|
||||
</div>
|
||||
</Label>
|
||||
{!IS_RUNNING_ON_CLOUD && (
|
||||
<>
|
||||
<Label htmlFor="openai-api-key">
|
||||
<div>OpenAI Base URL (optional)</div>
|
||||
<div className="font-light mt-2 leading-relaxed">
|
||||
Replace with a proxy URL if you don't want to use the default.
|
||||
</div>
|
||||
</Label>
|
||||
|
||||
<Input
|
||||
id="screenshot-one-api-key"
|
||||
placeholder="ScreenshotOne API key"
|
||||
value={settings.screenshotOneApiKey || ""}
|
||||
onChange={(e) =>
|
||||
setSettings((s) => ({
|
||||
...s,
|
||||
screenshotOneApiKey: e.target.value,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
<Input
|
||||
id="openai-base-url"
|
||||
placeholder="OpenAI Base URL"
|
||||
value={settings.openAiBaseURL || ""}
|
||||
onChange={(e) =>
|
||||
setSettings((s) => ({
|
||||
...s,
|
||||
openAiBaseURL: e.target.value,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Label htmlFor="editor-theme">
|
||||
<div>Editor Theme</div>
|
||||
</Label>
|
||||
<div>
|
||||
<Select // Use the custom Select component here
|
||||
name="editor-theme"
|
||||
value={settings.editorTheme}
|
||||
onValueChange={(value) => handleThemeChange(value as EditorTheme)}
|
||||
>
|
||||
<SelectTrigger className="w-[180px]">
|
||||
{capitalize(settings.editorTheme)}
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="cobalt">Cobalt</SelectItem>
|
||||
<SelectItem value="espresso">Espresso</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<Accordion type="single" collapsible className="w-full">
|
||||
<AccordionItem value="item-1">
|
||||
<AccordionTrigger>Screenshot by URL Config</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<Label htmlFor="screenshot-one-api-key">
|
||||
<div className="leading-normal font-normal text-xs">
|
||||
If you want to use URLs directly instead of taking the
|
||||
screenshot yourself, add a ScreenshotOne API key.{" "}
|
||||
<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"
|
||||
className="mt-2"
|
||||
placeholder="ScreenshotOne API key"
|
||||
value={settings.screenshotOneApiKey || ""}
|
||||
onChange={(e) =>
|
||||
setSettings((s) => ({
|
||||
...s,
|
||||
screenshotOneApiKey: e.target.value,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
|
||||
<Accordion type="single" collapsible className="w-full">
|
||||
<AccordionItem value="item-1">
|
||||
<AccordionTrigger>Theme Settings</AccordionTrigger>
|
||||
<AccordionContent className="space-y-4 flex flex-col">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="app-theme">
|
||||
<div>App Theme</div>
|
||||
</Label>
|
||||
<div>
|
||||
<button
|
||||
className="flex rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50t"
|
||||
onClick={() => {
|
||||
document
|
||||
.querySelector("div.mt-2")
|
||||
?.classList.toggle("dark"); // enable dark mode for sidebar
|
||||
document.body.classList.toggle("dark");
|
||||
document
|
||||
.querySelector('div[role="presentation"]')
|
||||
?.classList.toggle("dark"); // enable dark mode for upload container
|
||||
}}
|
||||
>
|
||||
Toggle dark mode
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="editor-theme">
|
||||
<div>
|
||||
Code Editor Theme - requires page refresh to update
|
||||
</div>
|
||||
</Label>
|
||||
<div>
|
||||
<Select // Use the custom Select component here
|
||||
name="editor-theme"
|
||||
value={settings.editorTheme}
|
||||
onValueChange={(value) =>
|
||||
handleThemeChange(value as EditorTheme)
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="w-[180px]">
|
||||
{capitalize(settings.editorTheme)}
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="cobalt">Cobalt</SelectItem>
|
||||
<SelectItem value="espresso">Espresso</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
|
||||
@ -159,4 +159,4 @@ export {
|
||||
SelectSeparator,
|
||||
SelectScrollUpButton,
|
||||
SelectScrollDownButton,
|
||||
}
|
||||
}
|
||||
@ -61,9 +61,21 @@
|
||||
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
body.dark {
|
||||
background-color: black;
|
||||
}
|
||||
|
||||
div[role="presentation"].dark {
|
||||
background-color: #09090b !important;
|
||||
}
|
||||
|
||||
iframe {
|
||||
background-color: white !important;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 222.2 84% 4.9%;
|
||||
--background: 222.2 0% 0%;
|
||||
--foreground: 210 40% 98%;
|
||||
|
||||
--card: 222.2 84% 4.9%;
|
||||
|
||||
@ -7,6 +7,6 @@ import { Toaster } from "react-hot-toast";
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
<Toaster />
|
||||
<Toaster toastOptions={{ className:"dark:bg-zinc-950 dark:text-white" }}/>
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
||||
@ -3,29 +3,24 @@ export enum EditorTheme {
|
||||
COBALT = "cobalt",
|
||||
}
|
||||
|
||||
export enum CSSOption {
|
||||
TAILWIND = "tailwind",
|
||||
// Keep in sync with backend (prompts.py)
|
||||
export enum GeneratedCodeConfig {
|
||||
HTML_TAILWIND = "html_tailwind",
|
||||
REACT_TAILWIND = "react_tailwind",
|
||||
BOOTSTRAP = "bootstrap",
|
||||
}
|
||||
|
||||
export enum JSFrameworkOption {
|
||||
NO_FRAMEWORK = "vanilla",
|
||||
REACT = "react",
|
||||
VUE = "vue",
|
||||
}
|
||||
|
||||
export interface OutputSettings {
|
||||
css: CSSOption;
|
||||
js: JSFrameworkOption;
|
||||
IONIC_TAILWIND = "ionic_tailwind",
|
||||
}
|
||||
|
||||
export interface Settings {
|
||||
openAiApiKey: string | null;
|
||||
openAiBaseURL: string | null;
|
||||
screenshotOneApiKey: string | null;
|
||||
isImageGenerationEnabled: boolean;
|
||||
editorTheme: EditorTheme;
|
||||
isTermOfServiceAccepted: boolean; // Only relevant for hosted version
|
||||
accessCode: string | null; // Only relevant for hosted version
|
||||
generatedCodeConfig: GeneratedCodeConfig;
|
||||
// Only relevant for hosted version
|
||||
isTermOfServiceAccepted: boolean;
|
||||
accessCode: string | null;
|
||||
}
|
||||
|
||||
export enum AppState {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user