import { useEffect, useRef, useState } from "react"; import { generateCode } from "./generateCode"; import { IS_FREE_TRIAL_ENABLED, IS_RUNNING_ON_CLOUD } from "./config"; import SettingsDialog from "./components/settings/SettingsDialog"; import { AppState, CodeGenerationParams, EditorTheme, Settings } from "./types"; import { PicoBadge } from "./components/messages/PicoBadge"; import { OnboardingNote } from "./components/messages/OnboardingNote"; import { usePersistedState } from "./hooks/usePersistedState"; import TermsOfServiceDialog from "./components/TermsOfServiceDialog"; import { USER_CLOSE_WEB_SOCKET_CODE } from "./constants"; import { addEvent } from "./lib/analytics"; import { History } from "./components/history/history_types"; import { extractHistoryTree } from "./components/history/utils"; import toast from "react-hot-toast"; import { useAuth } from "@clerk/clerk-react"; import { useStore } from "./store/store"; import { Stack } from "./lib/stacks"; import { CodeGenerationModel } from "./lib/models"; import useBrowserTabIndicator from "./hooks/useBrowserTabIndicator"; import TipLink from "./components/messages/TipLink"; import { useAppStore } from "./store/app-store"; import GenerateFromText from "./components/generate-from-text/GenerateFromText"; import { useProjectStore } from "./store/project-store"; import PreviewPane from "./components/preview/PreviewPane"; import DeprecationMessage from "./components/messages/DeprecationMessage"; import { GenerationSettings } from "./components/settings/GenerationSettings"; import StartPane from "./components/start-pane/StartPane"; import { takeScreenshot } from "./lib/takeScreenshot"; import Sidebar from "./components/sidebar/Sidebar"; interface Props { navbarComponent?: JSX.Element; } function App({ navbarComponent }: Props) { const [initialPrompt, setInitialPrompt] = useState(""); // Relevant for hosted version only // TODO: Move to AppContainer const { getToken } = useAuth(); const subscriberTier = useStore((state) => state.subscriberTier); const { // Inputs inputMode, setInputMode, isImportedFromCode, setIsImportedFromCode, referenceImages, setReferenceImages, // Outputs setGeneratedCode, setExecutionConsole, currentVersion, setCurrentVersion, appHistory, setAppHistory, } = useProjectStore(); const { disableInSelectAndEditMode, setUpdateInstruction, appState, setAppState, shouldIncludeResultImage, setShouldIncludeResultImage, } = useAppStore(); // Settings const [settings, setSettings] = usePersistedState( { openAiApiKey: null, openAiBaseURL: null, anthropicApiKey: null, screenshotOneApiKey: null, isImageGenerationEnabled: true, editorTheme: EditorTheme.COBALT, generatedCodeConfig: Stack.HTML_TAILWIND, codeGenerationModel: CodeGenerationModel.CLAUDE_3_5_SONNET_2024_06_20, // Only relevant for hosted version isTermOfServiceAccepted: false, }, "setting" ); const wsRef = useRef(null); // Code generation model from local storage or the default value const model = settings.codeGenerationModel || CodeGenerationModel.GPT_4_VISION; const showBetterModelMessage = model !== CodeGenerationModel.GPT_4O_2024_05_13 && model !== CodeGenerationModel.CLAUDE_3_5_SONNET_2024_06_20 && appState === AppState.INITIAL; const showSelectAndEditFeature = (model === CodeGenerationModel.GPT_4O_2024_05_13 || model === CodeGenerationModel.CLAUDE_3_5_SONNET_2024_06_20) && (settings.generatedCodeConfig === Stack.HTML_TAILWIND || settings.generatedCodeConfig === Stack.HTML_CSS); // Indicate coding state using the browser tab's favicon and title useBrowserTabIndicator(appState === AppState.CODING); // 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: Stack.HTML_TAILWIND, })); } }, [settings.generatedCodeConfig, setSettings]); // Functions const reset = () => { setAppState(AppState.INITIAL); setGeneratedCode(""); setReferenceImages([]); setInitialPrompt(""); setExecutionConsole([]); setUpdateInstruction(""); setIsImportedFromCode(false); setAppHistory([]); setCurrentVersion(null); setShouldIncludeResultImage(false); disableInSelectAndEditMode(); }; const regenerate = () => { if (currentVersion === null) { // This would be a error that I log to Sentry addEvent("RegenerateCurrentVersionNull"); toast.error( "No current version set. Please open a Github issue as this shouldn't happen." ); return; } // Retrieve the previous command const previousCommand = appHistory[currentVersion]; if (previousCommand.type !== "ai_create") { addEvent("RegenerateNotFirstVersion"); toast.error("Only the first version can be regenerated."); return; } addEvent("Regenerate"); // Re-run the create if (inputMode === "image" || inputMode === "video") { doCreate(referenceImages, inputMode); } else { // TODO: Fix this doCreateFromText(initialPrompt); } }; // Used when the user cancels the code generation const cancelCodeGeneration = () => { addEvent("Cancel"); wsRef.current?.close?.(USER_CLOSE_WEB_SOCKET_CODE); // make sure stop can correct the state even if the websocket is already closed cancelCodeGenerationAndReset(); }; // Used for code generation failure as well const cancelCodeGenerationAndReset = () => { // When this is the first version, reset the entire app state if (currentVersion === null) { reset(); } else { // Otherwise, revert to the last version setGeneratedCode(appHistory[currentVersion].code); setAppState(AppState.CODE_READY); } }; async function doGenerateCode( params: CodeGenerationParams, parentVersion: number | null ) { // Reset the execution console setExecutionConsole([]); // Set the app state setAppState(AppState.CODING); // Merge settings with params const authToken = await getToken(); const updatedParams = { ...params, ...settings, authToken: authToken || undefined, }; generateCode( wsRef, updatedParams, // On change (token) => setGeneratedCode((prev) => prev + token), // On set code (code) => { setGeneratedCode(code); if (params.generationType === "create") { if (inputMode === "image" || inputMode === "video") { setAppHistory([ { type: "ai_create", parentIndex: null, code, inputs: { image_url: referenceImages[0] }, }, ]); } else { setAppHistory([ { type: "ai_create", parentIndex: null, code, inputs: { text: params.image }, }, ]); } setCurrentVersion(0); } else { setAppHistory((prev) => { // Validate parent version if (parentVersion === null) { toast.error( "No parent version set. Contact support or open a Github issue." ); addEvent("ParentVersionNull"); return prev; } const newHistory: History = [ ...prev, { type: "ai_edit", parentIndex: parentVersion, code, inputs: { prompt: params.history ? params.history[params.history.length - 1] : "", // History should never be empty when performing an edit }, }, ]; setCurrentVersion(newHistory.length - 1); return newHistory; }); } }, // On status update (line) => setExecutionConsole((prev) => [...prev, line]), // On cancel () => { cancelCodeGenerationAndReset(); }, // On complete () => { setAppState(AppState.CODE_READY); } ); } // Initial version creation async function doCreate( referenceImages: string[], inputMode: "image" | "video" ) { // Reset any existing state reset(); // Set the input states setReferenceImages(referenceImages); setInputMode(inputMode); // Kick off the code generation if (referenceImages.length > 0) { addEvent("Create"); await doGenerateCode( { generationType: "create", image: referenceImages[0], inputMode, }, currentVersion ); } } function doCreateFromText(text: string) { // Reset any existing state reset(); setInputMode("text"); setInitialPrompt(text); doGenerateCode( { generationType: "create", inputMode: "text", image: text, }, currentVersion ); } // Subsequent updates async function doUpdate( updateInstruction: string, selectedElement?: HTMLElement ) { if (updateInstruction.trim() === "") { toast.error("Please include some instructions for AI on what to update."); return; } if (currentVersion === null) { toast.error( "No current version set. Contact support or open a Github issue." ); addEvent("CurrentVersionNull"); return; } let historyTree; try { historyTree = extractHistoryTree(appHistory, currentVersion); } catch { addEvent("HistoryTreeFailed"); toast.error( "Version history is invalid. This shouldn't happen. Please contact support or open a Github issue." ); return; } let modifiedUpdateInstruction = updateInstruction; // Send in a reference to the selected element if it exists if (selectedElement) { modifiedUpdateInstruction = updateInstruction + " referring to this element specifically: " + selectedElement.outerHTML; } const updatedHistory = [...historyTree, modifiedUpdateInstruction]; if (shouldIncludeResultImage) { const resultImage = await takeScreenshot(); await doGenerateCode( { generationType: "update", inputMode, image: referenceImages[0], resultImage: resultImage, history: updatedHistory, isImportedFromCode, }, currentVersion ); } else { await doGenerateCode( { generationType: "update", inputMode, image: inputMode === "text" ? initialPrompt : referenceImages[0], history: updatedHistory, isImportedFromCode, }, currentVersion ); } setGeneratedCode(""); setUpdateInstruction(""); } const handleTermDialogOpenChange = (open: boolean) => { setSettings((s) => ({ ...s, isTermOfServiceAccepted: !open, })); }; function setStack(stack: Stack) { setSettings((prev) => ({ ...prev, generatedCodeConfig: stack, })); } function importFromCode(code: string, stack: Stack) { // Set input state setIsImportedFromCode(true); // Set up this project setGeneratedCode(code); setStack(stack); setAppHistory([ { type: "code_create", parentIndex: null, code, inputs: { code }, }, ]); setCurrentVersion(0); // Set the app state setAppState(AppState.CODE_READY); } return (
{IS_RUNNING_ON_CLOUD && } {IS_RUNNING_ON_CLOUD && ( )}
{/* Header with access to settings */}

Screenshot to Code

{/* Generation settings like stack and model */} {/* Show auto updated message when older models are choosen */} {showBetterModelMessage && } {/* Show tip link until coding is complete */} {appState !== AppState.CODE_READY && } {IS_RUNNING_ON_CLOUD && !settings.openAiApiKey && !IS_FREE_TRIAL_ENABLED && subscriberTier === "free" && } {appState === AppState.INITIAL && ( )} {/* Rest of the sidebar when we're not in the initial state */} {(appState === AppState.CODING || appState === AppState.CODE_READY) && ( )}
{!!navbarComponent && navbarComponent} {appState === AppState.INITIAL && ( )} {(appState === AppState.CODING || appState === AppState.CODE_READY) && ( )}
); } export default App;