Spaces:
				
			
			
	
			
			
					
		Running
		
	
	
	
			
			
	
	
	
	
		
		
					
		Running
		
	| "use client" | |
| import type React from "react" | |
| import { useState, useEffect } from "react" | |
| import { Wand2, ArrowUp, Loader2, Maximize2, Minimize2 } from "lucide-react" | |
| import { Button } from "@/components/ui/button" | |
| import { Textarea } from "@/components/ui/textarea" | |
| import { ColorPanel } from "./color-panel" | |
| import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip" | |
| import { FullscreenToggle } from "./ui/fullscreen-toggle" | |
| import { AuthErrorPopup } from "./auth-error-popup" | |
| import { getInferenceToken } from "@/lib/auth" | |
| import posthog from 'posthog-js' | |
| interface PromptInputProps { | |
| onSubmit: (prompt: string, colors: string[]) => Promise<void>; | |
| isLoading?: boolean; | |
| initialPrompt?: string; | |
| onImproveError?: (error: string | null) => void; | |
| } | |
| export function PromptInput({ | |
| onSubmit, | |
| isLoading = false, | |
| initialPrompt = "", | |
| onImproveError | |
| }: PromptInputProps) { | |
| const [prompt, setPrompt] = useState(initialPrompt); | |
| const [isImprovingPrompt, setIsImprovingPrompt] = useState(false); | |
| const [isFullScreen, setIsFullScreen] = useState(false); | |
| const [selectedColors, setSelectedColors] = useState<string[]>([]); | |
| const [improveError, setImproveError] = useState<string | null>(null); | |
| const [showAuthError, setShowAuthError] = useState(false); | |
| // Update prompt when initialPrompt changes | |
| useEffect(() => { | |
| setPrompt(initialPrompt); | |
| }, [initialPrompt]); | |
| useEffect(() => { | |
| if (onImproveError) { | |
| onImproveError(improveError); | |
| } | |
| }, [improveError, onImproveError]); | |
| const checkAuth = async (): Promise<boolean> => { | |
| try { | |
| const token = await getInferenceToken(); | |
| if (token) { | |
| return true; | |
| } | |
| const canBypass = await fetch("/api/auth/check-bypass").then(res => res.json()); | |
| if (!canBypass) { | |
| throw new Error("Authentication required"); | |
| } | |
| return true; | |
| } catch (error) { | |
| setShowAuthError(true); | |
| return false; | |
| } | |
| } | |
| const handleSubmit = async (e: React.FormEvent) => { | |
| e.preventDefault(); | |
| if (prompt.trim() === '' || isLoading) return; | |
| // Check for authentication | |
| const isAuthenticated = await checkAuth(); | |
| if (!isAuthenticated) return; | |
| // Clear any previous errors | |
| setImproveError(null); | |
| await onSubmit(prompt, selectedColors); | |
| } | |
| const improvePrompt = async () => { | |
| if (prompt.trim() === '' || isImprovingPrompt || isLoading) return; | |
| // Check for authentication | |
| const isAuthenticated = await checkAuth(); | |
| if (!isAuthenticated) return; | |
| posthog.capture("Improve prompt", {}); | |
| // Clear previous errors | |
| setImproveError(null); | |
| setShowAuthError(false); | |
| setIsImprovingPrompt(true); | |
| try { | |
| const response = await fetch("/api/improve-prompt", { | |
| method: "POST", | |
| headers: { | |
| "Content-Type": "application/json", | |
| }, | |
| body: JSON.stringify({ prompt: prompt.trim() }), | |
| }) | |
| if (!response.ok) { | |
| posthog.capture("Improve prompt", {"type": "failed", "status": response.status}); | |
| // Handle auth error with openLogin flag | |
| if (response.status === 401) { | |
| const errorData = await response.json(); | |
| if (errorData.openLogin) { | |
| setShowAuthError(true); | |
| throw new Error('Authentication required'); | |
| } | |
| } | |
| const errorText = await response.text(); | |
| throw new Error(errorText || `Failed to improve prompt (${response.status})`); | |
| } | |
| if (!response.body) { | |
| throw new Error("Response body is null"); | |
| } | |
| // Handle streaming response | |
| const reader = response.body.getReader(); | |
| let improvedPrompt = ""; | |
| let textDecoder = new TextDecoder(); | |
| while (true) { | |
| const { done, value } = await reader.read(); | |
| if (done) break; | |
| const chunkText = textDecoder.decode(value, { stream: true }); | |
| let parsedChunk: any; | |
| let appended = false; | |
| try { | |
| // Parse the JSON response | |
| parsedChunk = JSON.parse(chunkText); | |
| } catch (parseError) { | |
| appended = true; | |
| // If JSON parsing fails, treat it as plain text (backwards compatibility) | |
| improvedPrompt += chunkText | |
| setPrompt(improvedPrompt) | |
| } | |
| if (parsedChunk && parsedChunk.type === "error") { | |
| throw new Error(parsedChunk.message || "An error occurred"); | |
| } else if (!appended) { | |
| improvedPrompt += chunkText | |
| setPrompt(improvedPrompt) | |
| } | |
| } | |
| } catch (error) { | |
| posthog.capture("Improve prompt", {"type": "failed", "error": error}); | |
| console.error("Error improving prompt:", error) | |
| setImproveError(error instanceof Error ? error.message : "Failed to improve prompt") | |
| } finally { | |
| setIsImprovingPrompt(false) | |
| } | |
| } | |
| const toggleFullScreen = () => { | |
| setIsFullScreen(!isFullScreen) | |
| } | |
| const handleColorsChange = (colors: string[]) => { | |
| setSelectedColors(colors); | |
| } | |
| const isPromptTooShort = prompt.length < 10 | |
| return ( | |
| <div className={`border-t border-novita-gray/20 p-4 relative transition-all duration-300 ease-in-out ${isFullScreen ? 'h-[50vh]' : ''}`}> | |
| <div className="absolute top-1 right-1 z-10"> | |
| <FullscreenToggle | |
| isFullScreen={isFullScreen} | |
| onClick={toggleFullScreen} | |
| /> | |
| </div> | |
| <form onSubmit={handleSubmit} className="flex flex-col gap-4 h-full"> | |
| <div className={`relative ${isFullScreen ? 'h-full' : ''}`}> | |
| <ColorPanel onColorsChange={handleColorsChange} /> | |
| <Textarea | |
| value={prompt} | |
| onChange={(e) => { | |
| setPrompt(e.target.value) | |
| // Clear error when user types | |
| if (improveError) setImproveError(null) | |
| }} | |
| placeholder="Describe what site you want to build. E.g., Build a snake game" | |
| className={`min-h-24 pr-20 pt-12 bg-novita-gray/20 border-novita-gray/30 text-white placeholder:text-novita-gray/70 resize-none ${isFullScreen ? 'h-full' : ''}`} | |
| disabled={isLoading || isImprovingPrompt} | |
| /> | |
| <div className="absolute bottom-3 right-3 flex gap-2"> | |
| <TooltipProvider> | |
| <Tooltip> | |
| <TooltipTrigger asChild> | |
| <span> | |
| <Button | |
| type="button" | |
| size="icon" | |
| variant="outline" | |
| className="h-8 w-8 bg-novita-gray/20 border-novita-gray/30 text-white hover:bg-novita-gray/30" | |
| disabled={isPromptTooShort || isLoading || isImprovingPrompt} | |
| onClick={improvePrompt} | |
| > | |
| {isImprovingPrompt ? ( | |
| <Loader2 className="h-4 w-4 animate-spin" /> | |
| ) : ( | |
| <Wand2 className="h-4 w-4" /> | |
| )} | |
| <span className="sr-only">Magic wand</span> | |
| </Button> | |
| </span> | |
| </TooltipTrigger> | |
| {isPromptTooShort && ( | |
| <TooltipContent className="bg-novita-gray text-white"> | |
| <p>Your prompt is too simple, we can't improve it.</p> | |
| </TooltipContent> | |
| )} | |
| </Tooltip> | |
| </TooltipProvider> | |
| <Button | |
| type="submit" | |
| size="icon" | |
| className="h-8 w-8 bg-novita-green text-black hover:bg-novita-green/90" | |
| disabled={prompt.trim() === "" || isLoading || isImprovingPrompt} | |
| > | |
| {isLoading ? ( | |
| <Loader2 className="h-4 w-4 animate-spin" /> | |
| ) : ( | |
| <ArrowUp className="h-4 w-4" /> | |
| )} | |
| <span className="sr-only">Submit</span> | |
| </Button> | |
| </div> | |
| </div> | |
| </form> | |
| <AuthErrorPopup | |
| show={showAuthError} | |
| onClose={() => setShowAuthError(false)} | |
| /> | |
| </div> | |
| ) | |
| } | |