Compare commits
4 Commits
c6f359c126
...
91ba71cc23
| Author | SHA1 | Date |
|---|---|---|
|
|
91ba71cc23 | |
|
|
960183c9c4 | |
|
|
ea680f4c5e | |
|
|
f0e2fcdaa6 |
|
|
@ -27,17 +27,13 @@ export const appConfig: Config = {
|
||||||
export const createApp = (): Application => {
|
export const createApp = (): Application => {
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
// Middleware - CORS configuration
|
// Middleware - CORS configuration (allow all origins)
|
||||||
const corsOrigin = process.env['CORS_ORIGIN']?.split(',') || [
|
|
||||||
'http://localhost:3001', // Landing
|
|
||||||
'http://localhost:3002', // Studio
|
|
||||||
'http://localhost:3003', // Admin
|
|
||||||
'*' // Allow all for development
|
|
||||||
];
|
|
||||||
|
|
||||||
app.use(cors({
|
app.use(cors({
|
||||||
origin: corsOrigin,
|
origin: true, // Allow all origins
|
||||||
credentials: true
|
credentials: true,
|
||||||
|
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
||||||
|
allowedHeaders: ['Content-Type', 'Authorization', 'X-API-Key'],
|
||||||
|
exposedHeaders: ['X-Request-ID']
|
||||||
}));
|
}));
|
||||||
|
|
||||||
app.use(express.json({ limit: '10mb' }));
|
app.use(express.json({ limit: '10mb' }));
|
||||||
|
|
@ -65,8 +61,8 @@ export const createApp = (): Application => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// API info endpoint
|
// API info endpoint
|
||||||
app.get('/api/info', (_req, res) => {
|
app.get('/api/info', async (req: any, res) => {
|
||||||
const info = {
|
const info: any = {
|
||||||
name: 'Banatie - Nano Banana Image Generation API',
|
name: 'Banatie - Nano Banana Image Generation API',
|
||||||
version: '1.0.0',
|
version: '1.0.0',
|
||||||
description: 'REST API service for AI-powered image generation using Gemini Flash Image model',
|
description: 'REST API service for AI-powered image generation using Gemini Flash Image model',
|
||||||
|
|
@ -84,6 +80,52 @@ export const createApp = (): Application => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// If API key is provided, validate and return key info
|
||||||
|
const providedKey = req.headers['x-api-key'] as string;
|
||||||
|
if (providedKey) {
|
||||||
|
try {
|
||||||
|
const { ApiKeyService } = await import('./services/ApiKeyService');
|
||||||
|
const apiKeyService = new ApiKeyService();
|
||||||
|
const apiKey = await apiKeyService.validateKey(providedKey);
|
||||||
|
|
||||||
|
if (apiKey) {
|
||||||
|
// Query org and project names
|
||||||
|
let organizationName = apiKey.organizationId;
|
||||||
|
let projectName = apiKey.projectId;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { db } = await import('./db');
|
||||||
|
const { organizations, projects } = await import('@banatie/database');
|
||||||
|
const { eq } = await import('drizzle-orm');
|
||||||
|
|
||||||
|
if (apiKey.organizationId) {
|
||||||
|
const org = await db.select().from(organizations).where(eq(organizations.id, apiKey.organizationId)).limit(1);
|
||||||
|
if (org.length > 0) organizationName = org[0].name;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (apiKey.projectId) {
|
||||||
|
const proj = await db.select().from(projects).where(eq(projects.id, apiKey.projectId)).limit(1);
|
||||||
|
if (proj.length > 0) projectName = proj[0].name;
|
||||||
|
}
|
||||||
|
} catch (dbError) {
|
||||||
|
// Fallback to IDs if DB query fails
|
||||||
|
}
|
||||||
|
|
||||||
|
info.authenticated = true;
|
||||||
|
info.keyInfo = {
|
||||||
|
type: apiKey.keyType,
|
||||||
|
organizationId: apiKey.organizationId,
|
||||||
|
organizationName,
|
||||||
|
projectId: apiKey.projectId,
|
||||||
|
projectName,
|
||||||
|
expiresAt: apiKey.expiresAt
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Ignore errors, just don't add key info
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
console.log(`[${new Date().toISOString()}] API info requested`);
|
console.log(`[${new Date().toISOString()}] API info requested`);
|
||||||
res.json(info);
|
res.json(info);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -40,11 +40,8 @@ export class MinioStorageService implements StorageService {
|
||||||
category: 'uploads' | 'generated' | 'references',
|
category: 'uploads' | 'generated' | 'references',
|
||||||
filename: string
|
filename: string
|
||||||
): string {
|
): string {
|
||||||
const now = new Date();
|
// Simplified path without date folder for now
|
||||||
const year = now.getFullYear();
|
return `${orgId}/${projectId}/${category}/${filename}`;
|
||||||
const month = String(now.getMonth() + 1).padStart(2, '0');
|
|
||||||
|
|
||||||
return `${orgId}/${projectId}/${category}/${year}-${month}/${filename}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private generateUniqueFilename(originalFilename: string): string {
|
private generateUniqueFilename(originalFilename: string): string {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"name": "landing",
|
"name": "@banatie/landing",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,516 @@
|
||||||
|
'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<ApiKeyInfo | null>(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<GenerationResult[]>([]);
|
||||||
|
|
||||||
|
// Modal State
|
||||||
|
const [zoomedImage, setZoomedImage] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const textareaRef = useRef<HTMLTextAreaElement>(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) {
|
||||||
|
const data = await response.json();
|
||||||
|
setApiKeyValidated(true);
|
||||||
|
// Extract org/project info from API response
|
||||||
|
if (data.keyInfo) {
|
||||||
|
setApiKeyInfo({
|
||||||
|
organizationName: data.keyInfo.organizationName || data.keyInfo.organizationId,
|
||||||
|
projectName: data.keyInfo.projectName || data.keyInfo.projectId,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setApiKeyInfo({
|
||||||
|
organizationName: 'Unknown',
|
||||||
|
projectName: 'Unknown',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} 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<HTMLTextAreaElement>) => {
|
||||||
|
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 (
|
||||||
|
<div className="relative z-10 max-w-7xl mx-auto px-6 py-16 min-h-screen">
|
||||||
|
{/* Page Header */}
|
||||||
|
<div className="mb-12">
|
||||||
|
<h1 className="text-4xl md:text-5xl font-bold text-white mb-4">
|
||||||
|
Text-to-Image Demo
|
||||||
|
</h1>
|
||||||
|
<p className="text-gray-400 text-lg">
|
||||||
|
Generate AI images with automatic prompt enhancement
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* API Key Section */}
|
||||||
|
<div className="mb-8 p-6 bg-slate-900/80 backdrop-blur-sm border border-slate-700 rounded-2xl">
|
||||||
|
<h2 className="text-xl font-semibold text-white mb-4">API Key</h2>
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<div className="flex-1 relative">
|
||||||
|
<input
|
||||||
|
type={apiKeyVisible ? 'text' : 'password'}
|
||||||
|
value={apiKey}
|
||||||
|
onChange={(e) => 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"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setApiKeyVisible(!apiKeyVisible)}
|
||||||
|
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-white transition-colors"
|
||||||
|
>
|
||||||
|
{apiKeyVisible ? '👁️' : '👁️🗨️'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{!apiKeyValidated && (
|
||||||
|
<button
|
||||||
|
onClick={validateApiKey}
|
||||||
|
disabled={validatingKey}
|
||||||
|
className="px-6 py-3 rounded-lg bg-amber-600 text-white font-semibold hover:bg-amber-700 transition-all disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
{validatingKey ? 'Validating...' : 'Validate'}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{apiKeyError && (
|
||||||
|
<p className="mt-3 text-sm text-red-400">{apiKeyError}</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{apiKeyValidated && apiKeyInfo && (
|
||||||
|
<div className="mt-3 text-sm text-green-400">
|
||||||
|
✓ Validated • {apiKeyInfo.organizationName} / {apiKeyInfo.projectName}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Prompt Input Section */}
|
||||||
|
<div className="mb-12 p-6 bg-slate-900/80 backdrop-blur-sm border border-slate-700 rounded-2xl">
|
||||||
|
<h2 className="text-xl font-semibold text-white mb-4">Your Prompt</h2>
|
||||||
|
<textarea
|
||||||
|
ref={textareaRef}
|
||||||
|
value={prompt}
|
||||||
|
onChange={(e) => setPrompt(e.target.value)}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
placeholder="Describe the image you want to generate..."
|
||||||
|
disabled={!apiKeyValidated || generating}
|
||||||
|
rows={4}
|
||||||
|
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 resize-none"
|
||||||
|
/>
|
||||||
|
<div className="mt-4 flex items-center justify-between">
|
||||||
|
<p className="text-sm text-gray-500">Press Ctrl+Enter to submit</p>
|
||||||
|
<button
|
||||||
|
onClick={generateImages}
|
||||||
|
disabled={!apiKeyValidated || generating || !prompt.trim()}
|
||||||
|
className="px-8 py-3 rounded-lg bg-gradient-to-r from-amber-600 to-orange-600 text-white font-semibold hover:from-amber-500 hover:to-orange-500 transition-all disabled:opacity-50 disabled:cursor-not-allowed shadow-lg shadow-amber-900/30"
|
||||||
|
>
|
||||||
|
{generating ? 'Generating...' : 'Generate Images'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{generationError && (
|
||||||
|
<p className="mt-3 text-sm text-red-400">{generationError}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Results Section */}
|
||||||
|
{results.length > 0 && (
|
||||||
|
<div className="space-y-8">
|
||||||
|
<h2 className="text-2xl font-bold text-white">Generated Images</h2>
|
||||||
|
|
||||||
|
{results.map((result) => (
|
||||||
|
<ResultCard
|
||||||
|
key={result.id}
|
||||||
|
result={result}
|
||||||
|
apiKey={apiKey}
|
||||||
|
onZoom={setZoomedImage}
|
||||||
|
onCopy={copyToClipboard}
|
||||||
|
onDownload={downloadImage}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Zoom Modal */}
|
||||||
|
{zoomedImage && (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 z-50 bg-black/90 flex items-center justify-center p-4"
|
||||||
|
onClick={() => setZoomedImage(null)}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
onClick={() => setZoomedImage(null)}
|
||||||
|
className="absolute top-4 right-4 w-10 h-10 rounded-full bg-white/10 hover:bg-white/20 text-white flex items-center justify-center transition-colors"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
<img
|
||||||
|
src={zoomedImage}
|
||||||
|
alt="Zoomed"
|
||||||
|
className="max-w-full max-h-full object-contain"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Result Card Component
|
||||||
|
function ResultCard({
|
||||||
|
result,
|
||||||
|
apiKey,
|
||||||
|
onZoom,
|
||||||
|
onCopy,
|
||||||
|
onDownload,
|
||||||
|
}: {
|
||||||
|
result: GenerationResult;
|
||||||
|
apiKey: string;
|
||||||
|
onZoom: (url: string) => void;
|
||||||
|
onCopy: (text: string) => void;
|
||||||
|
onDownload: (url: string, filename: string) => void;
|
||||||
|
}) {
|
||||||
|
const [activeTab, setActiveTab] = useState<'curl' | 'fetch'>('curl');
|
||||||
|
|
||||||
|
const curlCode = `curl -X POST ${API_BASE_URL}/api/text-to-image \\
|
||||||
|
-H "Content-Type: application/json" \\
|
||||||
|
-H "X-API-Key: ${apiKey}" \\
|
||||||
|
-d '{
|
||||||
|
"prompt": "${result.originalPrompt.replace(/"/g, '\\"')}",
|
||||||
|
"filename": "generated_image"
|
||||||
|
}'`;
|
||||||
|
|
||||||
|
const fetchCode = `fetch('${API_BASE_URL}/api/text-to-image', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-API-Key': '${apiKey}'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
prompt: '${result.originalPrompt.replace(/'/g, "\\'")}',
|
||||||
|
filename: 'generated_image'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(data => console.log(data));`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-6 bg-slate-900/80 backdrop-blur-sm border border-slate-700 rounded-2xl animate-fade-in">
|
||||||
|
{/* Timestamp */}
|
||||||
|
<div className="mb-4 text-sm text-gray-500">
|
||||||
|
{result.timestamp.toLocaleString()}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Horizontal Scrollable Image Comparison */}
|
||||||
|
<div className="mb-6 overflow-x-auto scrollbar-thin scrollbar-thumb-slate-700 scrollbar-track-slate-800">
|
||||||
|
<div className="flex gap-4 pb-4">
|
||||||
|
{/* Left Image */}
|
||||||
|
<div className="flex-shrink-0">
|
||||||
|
<div className="mb-2 text-sm font-medium text-gray-400">
|
||||||
|
Original Prompt
|
||||||
|
</div>
|
||||||
|
{result.leftImage?.error ? (
|
||||||
|
<div className="h-96 w-96 flex items-center justify-center bg-red-900/20 border border-red-700 rounded-lg">
|
||||||
|
<div className="text-center p-4">
|
||||||
|
<div className="text-4xl mb-2">⚠️</div>
|
||||||
|
<div className="text-sm text-red-400">
|
||||||
|
{result.leftImage.error}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
result.leftImage && (
|
||||||
|
<div className="relative group cursor-pointer">
|
||||||
|
<img
|
||||||
|
src={result.leftImage.url}
|
||||||
|
alt="Original"
|
||||||
|
className="h-96 w-auto object-contain rounded-lg border border-slate-700"
|
||||||
|
onClick={() => onZoom(result.leftImage!.url)}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onDownload(result.leftImage!.url, 'original.png');
|
||||||
|
}}
|
||||||
|
className="absolute top-2 right-2 px-3 py-1.5 bg-black/70 hover:bg-black/90 text-white text-xs rounded-lg opacity-0 group-hover:opacity-100 transition-opacity"
|
||||||
|
>
|
||||||
|
Download
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
<div className="mt-2 text-sm text-gray-300 max-w-sm">
|
||||||
|
{result.originalPrompt}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right Image */}
|
||||||
|
<div className="flex-shrink-0">
|
||||||
|
<div className="mb-2 text-sm font-medium text-gray-400">
|
||||||
|
Enhanced Prompt
|
||||||
|
</div>
|
||||||
|
{result.rightImage?.error ? (
|
||||||
|
<div className="h-96 w-96 flex items-center justify-center bg-red-900/20 border border-red-700 rounded-lg">
|
||||||
|
<div className="text-center p-4">
|
||||||
|
<div className="text-4xl mb-2">⚠️</div>
|
||||||
|
<div className="text-sm text-red-400">
|
||||||
|
{result.rightImage.error}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
result.rightImage && (
|
||||||
|
<div className="relative group cursor-pointer">
|
||||||
|
<img
|
||||||
|
src={result.rightImage.url}
|
||||||
|
alt="Enhanced"
|
||||||
|
className="h-96 w-auto object-contain rounded-lg border border-slate-700"
|
||||||
|
onClick={() => onZoom(result.rightImage!.url)}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onDownload(result.rightImage!.url, 'enhanced.png');
|
||||||
|
}}
|
||||||
|
className="absolute top-2 right-2 px-3 py-1.5 bg-black/70 hover:bg-black/90 text-white text-xs rounded-lg opacity-0 group-hover:opacity-100 transition-opacity"
|
||||||
|
>
|
||||||
|
Download
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
<div className="mt-2 text-sm text-gray-300 max-w-sm">
|
||||||
|
{result.enhancedPrompt || result.originalPrompt}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* API Code Examples */}
|
||||||
|
<div className="bg-slate-950/50 rounded-xl border border-slate-700 overflow-hidden">
|
||||||
|
<div className="flex items-center gap-2 bg-slate-900/50 px-4 py-2 border-b border-slate-700">
|
||||||
|
<div className="flex gap-1.5">
|
||||||
|
<div className="w-3 h-3 rounded-full bg-red-500/50"></div>
|
||||||
|
<div className="w-3 h-3 rounded-full bg-yellow-500/50"></div>
|
||||||
|
<div className="w-3 h-3 rounded-full bg-green-500/50"></div>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 flex gap-2 ml-4">
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab('curl')}
|
||||||
|
className={`px-3 py-1 text-xs rounded transition-colors ${
|
||||||
|
activeTab === 'curl'
|
||||||
|
? 'bg-slate-700 text-white'
|
||||||
|
: 'text-gray-400 hover:text-white'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
cURL
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab('fetch')}
|
||||||
|
className={`px-3 py-1 text-xs rounded transition-colors ${
|
||||||
|
activeTab === 'fetch'
|
||||||
|
? 'bg-slate-700 text-white'
|
||||||
|
: 'text-gray-400 hover:text-white'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
JS Fetch
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => onCopy(activeTab === 'curl' ? curlCode : fetchCode)}
|
||||||
|
className="px-3 py-1 text-xs bg-amber-600/20 hover:bg-amber-600/30 text-amber-400 rounded transition-colors"
|
||||||
|
>
|
||||||
|
Copy
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<pre className="p-4 text-xs md:text-sm text-gray-300 overflow-x-auto">
|
||||||
|
<code>{activeTab === 'curl' ? curlCode : fetchCode}</code>
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -59,11 +59,11 @@ export default function RootLayout({
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
|
||||||
</head>
|
</head>
|
||||||
<body className={`${inter.variable} antialiased`}>
|
<body className={`${inter.variable} antialiased`}>
|
||||||
<div className="min-h-screen bg-gradient-to-br from-slate-950 via-purple-950 to-slate-950">
|
<div className="min-h-screen bg-gradient-to-br from-slate-950 via-slate-900 to-slate-950">
|
||||||
{/* Animated gradient background */}
|
{/* Animated gradient background */}
|
||||||
<div className="fixed inset-0 overflow-hidden pointer-events-none">
|
<div className="fixed inset-0 overflow-hidden pointer-events-none">
|
||||||
<div className="absolute top-1/4 -left-1/4 w-96 h-96 bg-purple-600/30 rounded-full blur-3xl animate-pulse"></div>
|
<div className="absolute top-1/4 -left-1/4 w-96 h-96 bg-purple-600/10 rounded-full blur-3xl animate-pulse"></div>
|
||||||
<div className="absolute bottom-1/4 -right-1/4 w-96 h-96 bg-cyan-600/30 rounded-full blur-3xl animate-pulse delay-700"></div>
|
<div className="absolute bottom-1/4 -right-1/4 w-96 h-96 bg-cyan-600/10 rounded-full blur-3xl animate-pulse delay-700"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"folders": [
|
||||||
|
{
|
||||||
|
"name": "banatie-service",
|
||||||
|
"path": "."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "api-service",
|
||||||
|
"path": "apps/api-service"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "landing",
|
||||||
|
"path": "apps/landing"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "database",
|
||||||
|
"path": "packages/database"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"settings": {}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue