diff --git a/backend/custom_types.py b/backend/custom_types.py index b6c9fee..795eb18 100644 --- a/backend/custom_types.py +++ b/backend/custom_types.py @@ -4,4 +4,5 @@ from typing import Literal InputMode = Literal[ "image", "video", + "text", ] diff --git a/backend/prompts/__init__.py b/backend/prompts/__init__.py index dc96ab9..de544a7 100644 --- a/backend/prompts/__init__.py +++ b/backend/prompts/__init__.py @@ -5,6 +5,7 @@ from llm import Llm from prompts.imported_code_prompts import IMPORTED_CODE_SYSTEM_PROMPTS from prompts.screenshot_system_prompts import SYSTEM_PROMPTS +from prompts.text_prompts import SYSTEM_PROMPTS as TEXT_SYSTEM_PROMPTS from prompts.types import Stack @@ -87,3 +88,22 @@ def assemble_prompt( "content": user_content, }, ] + + +def assemble_text_prompt( + text_prompt: str, + stack: Stack, +) -> List[ChatCompletionMessageParam]: + + system_content = TEXT_SYSTEM_PROMPTS[stack] + + return [ + { + "role": "system", + "content": system_content, + }, + { + "role": "user", + "content": "Generate UI for " + text_prompt, + }, + ] diff --git a/backend/prompts/test_text_prompts.py b/backend/prompts/test_text_prompts.py new file mode 100644 index 0000000..9d38fd7 --- /dev/null +++ b/backend/prompts/test_text_prompts.py @@ -0,0 +1,37 @@ +import unittest +from prompts.text_prompts import HTML_TAILWIND_SYSTEM_PROMPT + + +class TestTextPrompts(unittest.TestCase): + def test_html_tailwind_system_prompt(self): + self.maxDiff = None + + print(HTML_TAILWIND_SYSTEM_PROMPT) + + expected_prompt = """ +You are an expert Tailwind developer. + + +- Make sure to make it look modern and sleek. +- Use modern, professional fonts and colors. +- Follow UX best practices. +- Do not add comments in the code such as "" and "" in place of writing the full code. WRITE THE FULL CODE. +- 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 this script to include Tailwind: + +- You can use Google Fonts +- Font Awesome for icons: + + +Return only the full code in tags. +Do not include markdown "```" or "```html" at the start or end. +Reply with only the code, and no text/explanation before and after the code. +""" + self.assertEqual(HTML_TAILWIND_SYSTEM_PROMPT.strip(), expected_prompt.strip()) + + +if __name__ == "__main__": + unittest.main() diff --git a/backend/prompts/text_prompts.py b/backend/prompts/text_prompts.py new file mode 100644 index 0000000..d6db94b --- /dev/null +++ b/backend/prompts/text_prompts.py @@ -0,0 +1,126 @@ +from prompts.types import SystemPrompts + +GENERAL_INSTRUCTIONS = """ +- Make sure to make it look modern and sleek. +- Use modern, professional fonts and colors. +- Follow UX best practices. +- Do not add comments in the code such as "" and "" in place of writing the full code. WRITE THE FULL CODE. +- 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.""" + +LIBRARY_INSTRUCTIONS = """ +- You can use Google Fonts +- Font Awesome for icons: """ + +FORMAT_INSTRUCTIONS = """ +Return only the full code in tags. +Do not include markdown "```" or "```html" at the start or end. +Reply with only the code, and no text/explanation before and after the code. +""" + +HTML_TAILWIND_SYSTEM_PROMPT = f""" +You are an expert Tailwind developer. + +{GENERAL_INSTRUCTIONS} + +In terms of libraries, + +- Use this script to include Tailwind: +{LIBRARY_INSTRUCTIONS} + +{FORMAT_INSTRUCTIONS} +""" + +HTML_CSS_SYSTEM_PROMPT = f""" +You are an expert HTML, CSS and JS developer. + +{GENERAL_INSTRUCTIONS} + +In terms of libraries, +{LIBRARY_INSTRUCTIONS} + +{FORMAT_INSTRUCTIONS} +""" + +REACT_TAILWIND_SYSTEM_PROMPT = f""" +You are an expert React/Tailwind developer. + +{GENERAL_INSTRUCTIONS} + +In terms of libraries, +- Use these script to include React so that it can run on a standalone page: + + + +- Use this script to include Tailwind: +{LIBRARY_INSTRUCTIONS} + +{FORMAT_INSTRUCTIONS} +""" + +BOOTSTRAP_SYSTEM_PROMPT = f""" +You are an expert Bootstrap, HTML and JS developer. + +{GENERAL_INSTRUCTIONS} + +In terms of libraries, +- Use this script to include Bootstrap: +{LIBRARY_INSTRUCTIONS} + +{FORMAT_INSTRUCTIONS} +""" + +IONIC_TAILWIND_SYSTEM_PROMPT = f""" +You are an expert Ionic/Tailwind developer. + +{GENERAL_INSTRUCTIONS} + +In terms of libraries, +- Use these script to include Ionic so that it can run on a standalone page: + + + +- Use this script to include Tailwind: +- You can use Google Fonts +- ionicons for icons, add the following + + + +{FORMAT_INSTRUCTIONS} +""" + +VUE_TAILWIND_SYSTEM_PROMPT = f""" +You are an expert Vue/Tailwind developer. + +{GENERAL_INSTRUCTIONS} + +In terms of libraries, +- Use these script to include Vue so that it can run on a standalone page: + +- Use this script to include Tailwind: +{LIBRARY_INSTRUCTIONS} + +{FORMAT_INSTRUCTIONS} +""" + +SVG_SYSTEM_PROMPT = f""" +You are an expert at building SVGs. + +{GENERAL_INSTRUCTIONS} + +Return only the full code in tags. +Do not include markdown "```" or "```svg" at the start or end. +""" + + +SYSTEM_PROMPTS = SystemPrompts( + html_css=HTML_CSS_SYSTEM_PROMPT, + html_tailwind=HTML_TAILWIND_SYSTEM_PROMPT, + react_tailwind=REACT_TAILWIND_SYSTEM_PROMPT, + bootstrap=BOOTSTRAP_SYSTEM_PROMPT, + ionic_tailwind=IONIC_TAILWIND_SYSTEM_PROMPT, + vue_tailwind=VUE_TAILWIND_SYSTEM_PROMPT, + svg=SVG_SYSTEM_PROMPT, +) diff --git a/backend/routes/generate_code.py b/backend/routes/generate_code.py index 269723a..f0ccfb8 100644 --- a/backend/routes/generate_code.py +++ b/backend/routes/generate_code.py @@ -17,7 +17,7 @@ from openai.types.chat import ChatCompletionMessageParam from mock_llm import mock_completion from typing import Dict, List, Union, cast, get_args from image_generation import create_alt_url_mapping, generate_images -from prompts import assemble_imported_code_prompt, assemble_prompt +from prompts import assemble_imported_code_prompt, assemble_prompt, assemble_text_prompt from datetime import datetime import json from routes.logging_utils import PaymentMethod, send_to_saas_backend @@ -230,12 +230,18 @@ async def stream_code(websocket: WebSocket): else: # Assemble the prompt try: - if params.get("resultImage") and params["resultImage"]: - prompt_messages = assemble_prompt( - params["image"], valid_stack, params["resultImage"] - ) + if validated_input_mode == "image": + if params.get("resultImage") and params["resultImage"]: + prompt_messages = assemble_prompt( + params["image"], valid_stack, params["resultImage"] + ) + else: + prompt_messages = assemble_prompt(params["image"], valid_stack) + elif validated_input_mode == "text": + prompt_messages = assemble_text_prompt(params["image"], valid_stack) else: - prompt_messages = assemble_prompt(params["image"], valid_stack) + await throw_error("Invalid input mode") + return except: await websocket.send_json( { @@ -246,8 +252,8 @@ async def stream_code(websocket: WebSocket): await websocket.close() return + # Transform the history tree into message format for updates if params["generationType"] == "update": - # Transform the history tree into message format # TODO: Move this to frontend for index, text in enumerate(params["history"]): if index % 2 == 0: diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 7ace365..8186c31 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -49,6 +49,7 @@ import TipLink from "./components/core/TipLink"; import FeedbackCallNote from "./components/user-feedback/FeedbackCallNote"; import SelectAndEditModeToggleButton from "./components/select-and-edit/SelectAndEditModeToggleButton"; import { useAppStore } from "./store/app-store"; +import GenerateFromText from "./components/generate-from-text/GenerateFromText"; const IS_OPENAI_DOWN = false; @@ -60,7 +61,11 @@ function App({ navbarComponent }: Props) { const [appState, setAppState] = useState(AppState.INITIAL); const [generatedCode, setGeneratedCode] = useState(""); - const [inputMode, setInputMode] = useState<"image" | "video">("image"); + const [inputMode, setInputMode] = useState<"image" | "video" | "text">( + "image" + ); + + const [initialPrompt, setInitialPrompt] = useState(""); const [referenceImages, setReferenceImages] = useState([]); const [executionConsole, setExecutionConsole] = useState([]); @@ -177,6 +182,7 @@ function App({ navbarComponent }: Props) { setAppState(AppState.INITIAL); setGeneratedCode(""); setReferenceImages([]); + setInitialPrompt(""); setExecutionConsole([]); setUpdateInstruction(""); setIsImportedFromCode(false); @@ -207,7 +213,12 @@ function App({ navbarComponent }: Props) { addEvent("Regenerate"); // Re-run the create - doCreate(referenceImages, inputMode); + if (inputMode === "image" || inputMode === "video") { + doCreate(referenceImages, inputMode); + } else { + // TODO: Fix this + doCreateFromText(initialPrompt); + } }; const cancelCodeGeneration = () => { @@ -258,14 +269,25 @@ function App({ navbarComponent }: Props) { (code) => { setGeneratedCode(code); if (params.generationType === "create") { - setAppHistory([ - { - type: "ai_create", - parentIndex: null, - code, - inputs: { image_url: referenceImages[0] }, - }, - ]); + 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) => { @@ -332,6 +354,22 @@ function App({ navbarComponent }: Props) { } } + 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, @@ -391,7 +429,7 @@ function App({ navbarComponent }: Props) { { generationType: "update", inputMode, - image: referenceImages[0], + image: inputMode === "text" ? initialPrompt : referenceImages[0], history: updatedHistory, isImportedFromCode, }, @@ -509,6 +547,10 @@ function App({ navbarComponent }: Props) { )} + {appState === AppState.INITIAL && ( + + )} + {(appState === AppState.CODING || appState === AppState.CODE_READY) && ( <> diff --git a/frontend/src/components/generate-from-text/GenerateFromText.tsx b/frontend/src/components/generate-from-text/GenerateFromText.tsx new file mode 100644 index 0000000..35919f9 --- /dev/null +++ b/frontend/src/components/generate-from-text/GenerateFromText.tsx @@ -0,0 +1,57 @@ +import { useState, useRef, useEffect } from "react"; +import { Button } from "../ui/button"; +import { Textarea } from "../ui/textarea"; +import toast from "react-hot-toast"; + +interface GenerateFromTextProps { + doCreateFromText: (text: string) => void; +} + +function GenerateFromText({ doCreateFromText }: GenerateFromTextProps) { + const [isOpen, setIsOpen] = useState(false); + const [text, setText] = useState(""); + const textareaRef = useRef(null); + + useEffect(() => { + if (isOpen && textareaRef.current) { + textareaRef.current.focus(); + } + }, [isOpen]); + + const handleGenerate = () => { + if (text.trim() === "") { + // Assuming there's a toast function available in the context + toast.error("Please enter a prompt to generate from"); + return; + } + doCreateFromText(text); + }; + + return ( +
+ {!isOpen ? ( +
+ +
+ ) : ( + <> +