diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index f0d86fc..5156a0f 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -40,6 +40,8 @@ import ModelSettingsSection from "./components/ModelSettingsSection"; import { extractHtml } from "./components/preview/extractHtml"; import useBrowserTabIndicator from "./hooks/useBrowserTabIndicator"; import TipLink from "./components/core/TipLink"; +import SelectAndEditModeToggleButton from "./components/select-and-edit/SelectAndEditModeToggleButton"; +import { useAppStore } from "./store/app-store"; const IS_OPENAI_DOWN = false; @@ -54,6 +56,8 @@ function App() { const [updateInstruction, setUpdateInstruction] = useState(""); const [isImportedFromCode, setIsImportedFromCode] = useState(false); + const { disableInSelectAndEditMode } = useAppStore(); + // Settings const [settings, setSettings] = usePersistedState( { @@ -94,6 +98,10 @@ function App() { selectedCodeGenerationModel !== CodeGenerationModel.GPT_4O_2024_05_13 && appState === AppState.INITIAL; + const showSelectAndEditFeature = + selectedCodeGenerationModel === CodeGenerationModel.GPT_4O_2024_05_13 && + settings.generatedCodeConfig === Stack.HTML_TAILWIND; + // Indicate coding state using the browser tab's favicon and title useBrowserTabIndicator(appState === AppState.CODING); @@ -149,6 +157,7 @@ function App() { setAppHistory([]); setCurrentVersion(null); setShouldIncludeResultImage(false); + disableInSelectAndEditMode(); }; const regenerate = () => { @@ -237,7 +246,9 @@ function App() { parentIndex: parentVersion, code, inputs: { - prompt: updateInstruction, + prompt: params.history + ? params.history[params.history.length - 1] + : updateInstruction, }, }, ]; @@ -279,7 +290,10 @@ function App() { } // Subsequent updates - async function doUpdate() { + async function doUpdate( + updateInstruction: string, + selectedElement?: HTMLElement + ) { if (currentVersion === null) { toast.error( "No current version set. Contact support or open a Github issue." @@ -297,7 +311,17 @@ function App() { return; } - const updatedHistory = [...historyTree, updateInstruction]; + 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(); @@ -379,10 +403,7 @@ function App() { /> )}
-
+

Screenshot to Code

@@ -485,7 +506,7 @@ function App() { />
+ {showSelectAndEditFeature && ( + + )}
@@ -626,10 +650,18 @@ function App() {
- + - + void; } -function Preview({ code, device }: Props) { +function Preview({ code, device, doUpdate }: Props) { const iframeRef = useRef(null); // Don't update code more often than every 200ms. const throttledCode = useThrottle(code, 200); + // Select and edit functionality + const [clickEvent, setClickEvent] = useState(null); + useEffect(() => { - if (iframeRef.current) { - iframeRef.current.srcdoc = throttledCode; + const iframe = iframeRef.current; + if (iframe) { + iframe.srcdoc = throttledCode; + + // Set up click handler for select and edit funtionality + iframe.addEventListener("load", function () { + iframe.contentWindow?.document.body.addEventListener( + "click", + setClickEvent + ); + }); } }, [throttledCode]); @@ -34,6 +48,7 @@ function Preview({ code, device }: Props) { } )} > +
); } diff --git a/frontend/src/components/select-and-edit/EditPopup.tsx b/frontend/src/components/select-and-edit/EditPopup.tsx new file mode 100644 index 0000000..09192b3 --- /dev/null +++ b/frontend/src/components/select-and-edit/EditPopup.tsx @@ -0,0 +1,143 @@ +import React, { useEffect, useRef, useState } from "react"; +import { Textarea } from "../ui/textarea"; +import { Button } from "../ui/button"; +import { addHighlight, getAdjustedCoordinates, removeHighlight } from "./utils"; +import { useAppStore } from "../../store/app-store"; + +interface EditPopupProps { + event: MouseEvent | null; + iframeRef: React.RefObject; + doUpdate: (updateInstruction: string, selectedElement?: HTMLElement) => void; +} + +const EditPopup: React.FC = ({ + event, + iframeRef, + doUpdate, +}) => { + // App state + const { inSelectAndEditMode } = useAppStore(); + + // Create a wrapper ref to store inSelectAndEditMode so the value is not stale + // in a event listener + const inSelectAndEditModeRef = useRef(inSelectAndEditMode); + + // Update the ref whenever the state changes + useEffect(() => { + inSelectAndEditModeRef.current = inSelectAndEditMode; + }, [inSelectAndEditMode]); + + // Popup state + const [popupVisible, setPopupVisible] = useState(false); + const [popupPosition, setPopupPosition] = useState({ x: 0, y: 0 }); + + // Edit state + const [selectedElement, setSelectedElement] = useState< + HTMLElement | undefined + >(undefined); + const [updateText, setUpdateText] = useState(""); + + // Textarea ref for focusing + const textareaRef = useRef(null); + + function onUpdate(updateText: string) { + // Perform the update + doUpdate( + updateText, + selectedElement ? removeHighlight(selectedElement) : selectedElement + ); + + // Unselect the element + setSelectedElement(undefined); + + // Hide the popup + setPopupVisible(false); + } + + // Remove highlight and reset state when not in select and edit mode + useEffect(() => { + if (!inSelectAndEditMode) { + if (selectedElement) removeHighlight(selectedElement); + setSelectedElement(undefined); + setPopupVisible(false); + } + }, [inSelectAndEditMode, selectedElement]); + + // Handle the click event + useEffect(() => { + // Return if not in select and edit mode + if (!inSelectAndEditModeRef.current || !event) { + return; + } + + // Prevent default to avoid issues like label clicks triggering textareas, etc. + event.preventDefault(); + + const targetElement = event.target as HTMLElement; + + // Return if no target element + if (!targetElement) return; + + // Highlight and set the selected element + setSelectedElement((prev) => { + // Remove style from previous element + if (prev) { + removeHighlight(prev); + } + return addHighlight(targetElement); + }); + + // Calculate adjusted coordinates + const adjustedCoordinates = getAdjustedCoordinates( + event.clientX, + event.clientY, + iframeRef.current?.getBoundingClientRect() + ); + + // Show the popup at the click position + setPopupVisible(true); + setPopupPosition({ x: adjustedCoordinates.x, y: adjustedCoordinates.y }); + + // Reset the update text + setUpdateText(""); + + // Focus the textarea + textareaRef.current?.focus(); + }, [event, iframeRef]); + + // Focus the textarea when the popup is visible (we can't do this only when handling the click event + // because the textarea is not rendered yet) + // We need to also do it in the click event because popupVisible doesn't change values in that event + useEffect(() => { + if (popupVisible) { + textareaRef.current?.focus(); + } + }, [popupVisible]); + + if (!popupVisible) return; + + return ( +
+