diff --git a/README.md b/README.md index 7a7531a..1762f91 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ We also just added experimental support for taking a video/screen recording of a ## Sponsors - + ## 🚀 Try It Out without no install diff --git a/backend/routes/generate_code.py b/backend/routes/generate_code.py index 526571d..d1f2760 100644 --- a/backend/routes/generate_code.py +++ b/backend/routes/generate_code.py @@ -23,7 +23,7 @@ from routes.saas_utils import does_user_have_subscription_credits from prompts.claude_prompts import VIDEO_PROMPT from prompts.types import Stack -# from utils import pprint_prompt +from utils import pprint_prompt from video.utils import extract_tag_content, assemble_claude_prompt_video from ws.constants import APP_ERROR_WEB_SOCKET_CODE # type: ignore @@ -243,7 +243,7 @@ async def stream_code(websocket: WebSocket): video_data_url = params["image"] prompt_messages = await assemble_claude_prompt_video(video_data_url) - # pprint_prompt(prompt_messages) # type: ignore + pprint_prompt(prompt_messages) # type: ignore if SHOULD_MOCK_AI_RESPONSE: completion = await mock_completion( diff --git a/frontend/package.json b/frontend/package.json index 3bde6c4..5f8c639 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -54,7 +54,7 @@ "thememirror": "^2.0.1", "vite-plugin-checker": "^0.6.2", "webm-duration-fix": "^1.0.4", - "zustand": "^4.4.7" + "zustand": "^4.5.2" }, "devDependencies": { "@types/jest": "^29.5.12", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 8c5a150..074667c 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -43,6 +43,7 @@ import { extractHtml } from "./components/preview/extractHtml"; import useBrowserTabIndicator from "./hooks/useBrowserTabIndicator"; import TipLink from "./components/core/TipLink"; import FeedbackCallNote from "./components/user-feedback/FeedbackCallNote"; +import SelectAndEditModeToggleButton from "./components/select-and-edit/SelectAndEditModeToggleButton"; const IS_OPENAI_DOWN = false; @@ -108,6 +109,10 @@ function App({ navbarComponent }: Props) { // const showFeedbackCallNote = subscriberTier !== "free"; const showFeedbackCallNote = false; + 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); @@ -266,7 +271,9 @@ function App({ navbarComponent }: Props) { parentIndex: parentVersion, code, inputs: { - prompt: updateInstruction, + prompt: params.history + ? params.history[params.history.length - 1] + : updateInstruction, }, }, ]; @@ -312,7 +319,10 @@ function App({ navbarComponent }: Props) { } // 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." @@ -332,7 +342,17 @@ function App({ navbarComponent }: Props) { 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(); @@ -523,8 +543,8 @@ function App({ navbarComponent }: Props) { /> @@ -536,6 +556,9 @@ function App({ navbarComponent }: Props) { > 🔄 Regenerate + {showSelectAndEditFeature && ( + + )}
@@ -668,10 +691,18 @@ function App({ navbarComponent }: Props) {
- + - + void; } -function Preview({ code, device }: Props) { +function Preview({ code, device, doUpdate }: Props) { + const { inSelectAndEditMode } = useAppStore(); + 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 +51,12 @@ 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..5bc9d42 --- /dev/null +++ b/frontend/src/components/select-and-edit/EditPopup.tsx @@ -0,0 +1,126 @@ +import React, { useEffect, useRef, useState } from "react"; +import { Textarea } from "../ui/textarea"; +import { Button } from "../ui/button"; +import { addHighlight, getAdjustedCoordinates, removeHighlight } from "./utils"; + +interface EditPopupProps { + event: MouseEvent | null; + inSelectAndEditMode: boolean; + doUpdate: (updateInstruction: string, selectedElement?: HTMLElement) => void; + iframeRef: React.RefObject; +} + +const EditPopup: React.FC = ({ + event, + inSelectAndEditMode, + doUpdate, + iframeRef, +}) => { + // Edit state + const [selectedElement, setSelectedElement] = useState< + HTMLElement | undefined + >(undefined); + const [updateText, setUpdateText] = useState(""); + + // Popup state + const [popupVisible, setPopupVisible] = useState(false); + const [popupPosition, setPopupPosition] = useState({ x: 0, y: 0 }); + + // Create a wrapper ref to store inSelectAndEditMode so the value is not stale + // in a event listener + const inSelectAndEditModeRef = useRef(inSelectAndEditMode); + + // 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); + } + + // Update the ref whenever the state changes + useEffect(() => { + inSelectAndEditModeRef.current = inSelectAndEditMode; + }, [inSelectAndEditMode]); + + // 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]); + + if (!popupVisible) return; + + return ( +
+