diff --git a/apps/landing/package.json b/apps/landing/package.json index f1b3d60..3188c77 100644 --- a/apps/landing/package.json +++ b/apps/landing/package.json @@ -1,5 +1,5 @@ { - "name": "landing", + "name": "@banatie/landing", "version": "0.1.0", "private": true, "scripts": { diff --git a/apps/landing/src/app/demo/tti/page.tsx b/apps/landing/src/app/demo/tti/page.tsx new file mode 100644 index 0000000..cd42929 --- /dev/null +++ b/apps/landing/src/app/demo/tti/page.tsx @@ -0,0 +1,509 @@ +'use client'; + +import { useState, useRef, KeyboardEvent } from 'react'; + +const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000'; + +interface GenerationResult { + id: string; + timestamp: Date; + originalPrompt: string; + enhancedPrompt?: string; + leftImage: { + url: string; + width: number; + height: number; + error?: string; + } | null; + rightImage: { + url: string; + width: number; + height: number; + error?: string; + } | null; +} + +interface ApiKeyInfo { + organizationName?: string; + projectName?: string; +} + +export default function DemoTTIPage() { + // API Key State + const [apiKey, setApiKey] = useState(''); + const [apiKeyVisible, setApiKeyVisible] = useState(false); + const [apiKeyValidated, setApiKeyValidated] = useState(false); + const [apiKeyInfo, setApiKeyInfo] = useState(null); + const [apiKeyError, setApiKeyError] = useState(''); + const [validatingKey, setValidatingKey] = useState(false); + + // Prompt State + const [prompt, setPrompt] = useState(''); + const [generating, setGenerating] = useState(false); + const [generationError, setGenerationError] = useState(''); + + // Results State + const [results, setResults] = useState([]); + + // Modal State + const [zoomedImage, setZoomedImage] = useState(null); + + const textareaRef = useRef(null); + + // Validate API Key + const validateApiKey = async () => { + if (!apiKey.trim()) { + setApiKeyError('Please enter an API key'); + return; + } + + setValidatingKey(true); + setApiKeyError(''); + + try { + // Test API key with a minimal request to /api/info or similar + const response = await fetch(`${API_BASE_URL}/api/info`, { + headers: { + 'X-API-Key': apiKey, + }, + }); + + if (response.ok) { + setApiKeyValidated(true); + // Try to extract org/project info if available + // For now, we'll set placeholder - this would come from API response + setApiKeyInfo({ + organizationName: 'Your Organization', + projectName: 'Your Project', + }); + } else { + const error = await response.json(); + setApiKeyError(error.message || 'Invalid API key'); + setApiKeyValidated(false); + } + } catch (error) { + setApiKeyError('Failed to validate API key. Please check your connection.'); + setApiKeyValidated(false); + } finally { + setValidatingKey(false); + } + }; + + // Generate Images + const generateImages = async () => { + if (!prompt.trim()) { + setGenerationError('Please enter a prompt'); + return; + } + + setGenerating(true); + setGenerationError(''); + + const resultId = Date.now().toString(); + const timestamp = new Date(); + + try { + // Call API twice in parallel (both identical for now) + const [leftResult, rightResult] = await Promise.all([ + fetch(`${API_BASE_URL}/api/text-to-image`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-API-Key': apiKey, + }, + body: JSON.stringify({ + prompt: prompt.trim(), + filename: `demo_${resultId}_left`, + }), + }), + fetch(`${API_BASE_URL}/api/text-to-image`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-API-Key': apiKey, + }, + body: JSON.stringify({ + prompt: prompt.trim(), + filename: `demo_${resultId}_right`, + }), + }), + ]); + + const leftData = await leftResult.json(); + const rightData = await rightResult.json(); + + // Create result object + const newResult: GenerationResult = { + id: resultId, + timestamp, + originalPrompt: prompt.trim(), + enhancedPrompt: rightData.data?.promptEnhancement?.enhancedPrompt, + leftImage: leftData.success + ? { + url: leftData.data.url || leftData.data.filepath, + width: 1024, // Default, would come from API + height: 1024, + } + : null, + rightImage: rightData.success + ? { + url: rightData.data.url || rightData.data.filepath, + width: 1024, + height: 1024, + } + : null, + }; + + if (!leftData.success) { + newResult.leftImage = { url: '', width: 0, height: 0, error: leftData.error }; + } + + if (!rightData.success) { + newResult.rightImage = { url: '', width: 0, height: 0, error: rightData.error }; + } + + // Add to results at the top + setResults((prev) => [newResult, ...prev]); + + // Clear prompt + setPrompt(''); + } catch (error) { + setGenerationError( + error instanceof Error ? error.message : 'Failed to generate images' + ); + } finally { + setGenerating(false); + } + }; + + // Handle Ctrl+Enter + const handleKeyDown = (e: KeyboardEvent) => { + if (e.ctrlKey && e.key === 'Enter') { + e.preventDefault(); + generateImages(); + } + }; + + // Copy to clipboard + const copyToClipboard = (text: string) => { + navigator.clipboard.writeText(text); + }; + + // Download image + const downloadImage = async (url: string, filename: string) => { + try { + const response = await fetch(url); + const blob = await response.blob(); + const blobUrl = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = blobUrl; + link.download = filename; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + URL.revokeObjectURL(blobUrl); + } catch (error) { + console.error('Download failed:', error); + } + }; + + return ( +
+ {/* Page Header */} +
+

+ Text-to-Image Demo +

+

+ Generate AI images with automatic prompt enhancement +

+
+ + {/* API Key Section */} +
+

API Key

+
+
+ setApiKey(e.target.value)} + placeholder="Enter your API key" + disabled={apiKeyValidated} + className="w-full px-4 py-3 bg-slate-800 border border-slate-700 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent disabled:opacity-50 disabled:cursor-not-allowed pr-12" + /> + +
+ {!apiKeyValidated && ( + + )} +
+ + {apiKeyError && ( +

{apiKeyError}

+ )} + + {apiKeyValidated && apiKeyInfo && ( +
+ ✓ Validated • {apiKeyInfo.organizationName} / {apiKeyInfo.projectName} +
+ )} +
+ + {/* Prompt Input Section */} +
+

Your Prompt

+