├── .cursorrules ├── .env.example ├── .eslintrc.json ├── .gitignore ├── README.md ├── components.json ├── next.config.ts ├── package-lock.json ├── package.json ├── pnpm-lock.yaml ├── postcss.config.mjs ├── src ├── app │ ├── favicon.ico │ ├── flux │ │ └── [model-id] │ │ │ └── page.tsx │ ├── globals.css │ ├── layout.tsx │ └── page.tsx ├── components │ ├── api-key-input.tsx │ ├── image-generator.tsx │ ├── image-generator │ │ ├── generation-settings.tsx │ │ ├── generations-gallery.tsx │ │ └── image-display.tsx │ ├── navbar.tsx │ └── ui │ │ ├── alert.tsx │ │ ├── badge.tsx │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── checkbox.tsx │ │ ├── dialog.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── lightbox.tsx │ │ ├── progress.tsx │ │ ├── radio-group.tsx │ │ ├── scroll-area.tsx │ │ ├── select.tsx │ │ ├── separator.tsx │ │ ├── slider.tsx │ │ ├── switch.tsx │ │ ├── tabs.tsx │ │ ├── textarea.tsx │ │ ├── toast.tsx │ │ └── toaster.tsx ├── hooks │ └── use-toast.ts └── lib │ ├── actions │ └── generate-image.ts │ ├── models │ ├── flux │ │ ├── image-to-image.ts │ │ ├── text-to-text.ts │ │ └── training.ts │ └── registry.ts │ ├── types.ts │ └── utils.ts ├── tailwind.config.ts └── tsconfig.json /.cursorrules: -------------------------------------------------------------------------------- 1 | # Project Overview 2 | name: FAL.AI Web Interface 3 | description: A modern web interface for FAL.AI API integration allowing users to input their API keys and interact with FAL's AI services. 4 | 5 | # Technical Stack 6 | framework: Next.js 14 (App Router) 7 | styling: Tailwind CSS + Shadcn UI 8 | language: TypeScript 9 | node_version: ">=20.0.0" 10 | 11 | # Key Features 12 | - User API key management 13 | - FAL.AI service integration 14 | - Real-time AI processing 15 | - Secure credential handling 16 | 17 | # Architecture Notes 18 | - Server components by default 19 | - Client components only when necessary (user interactions, state management) 20 | - API key storage in secure client-side storage 21 | - Server-side proxy implementation for FAL.AI requests 22 | 23 | 24 | # Dependencies 25 | - @fal-ai/client: Latest # Official FAL.AI client 26 | - @fal-ai/server-proxy # FAL.AI server proxy 27 | - shadcn/ui: Latest # UI component library 28 | - tailwindcss: Latest # Utility-first CSS 29 | 30 | # Security Considerations 31 | - API keys stored in encrypted client storage 32 | - Server-side proxy for secure API communication 33 | - No API keys in client-side code 34 | - Rate limiting implementation 35 | 36 | # Performance Optimization 37 | - React Server Components for initial render 38 | - Streaming responses for AI operations 39 | - Lazy loading for non-critical components 40 | - Image optimization for AI-generated content 41 | 42 | # State Management 43 | - React hooks for local state 44 | - Server actions for data mutations 45 | - Optimistic updates for better UX 46 | 47 | # Error Handling 48 | - Graceful fallbacks for API failures 49 | - User-friendly error messages 50 | - Request retry mechanisms -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_API_KEY = your_api_key -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next/core-web-vitals", "next/typescript"], 3 | "rules": { 4 | "@typescript-eslint/explicit-function-return-type": "off", 5 | "react/react-in-jsx-scope": "off", 6 | "react/jsx-props-no-spreading": "off", 7 | "import/prefer-default-export": "off", 8 | "@next/next/no-img-element": "off", 9 | "jsx-a11y/alt-text": "warn", 10 | "@typescript-eslint/no-explicit-any": "warn", 11 | "@typescript-eslint/no-floating-promises": "off", 12 | "@typescript-eslint/no-unused-vars": "off", 13 | "react-hooks/exhaustive-deps": "off" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | 32 | # env files (can opt-in for committing if needed) 33 | .env* 34 | !.env.example 35 | 36 | # vercel 37 | .vercel 38 | 39 | # typescript 40 | *.tsbuildinfo 41 | next-env.d.ts 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FAL.AI Web Interface 2 | 3 | ![generation-2413047d-16b7-4f13-a4a1-a71d6a2e7be8](https://github.com/user-attachments/assets/73d3a42f-1341-4569-980c-21915474ef4a) 4 | 5 | 6 | A modern web interface for interacting with FAL.AI services, built with Next.js 14 and TypeScript. This application provides a seamless way to manage FAL.AI API keys and interact with various AI services. 7 | 8 | initial idea: reddit: https://www.reddit.com/r/StableDiffusion/comments/1hvklr4/i_made_a_simple_web_ui_to_use_flux_through_the/ 9 | ## Features 10 | 11 | - 🔑 Secure API key management 12 | - 🤖 Direct FAL.AI service integration 13 | - ⚡ Real-time AI processing 14 | - 🔒 Secure credential handling 15 | - 🎨 Modern, responsive UI 16 | 17 | ## Tech Stack 18 | 19 | - **Framework:** Next.js 14 (App Router) 20 | - **Language:** TypeScript 21 | - **Styling:** Tailwind CSS + Shadcn UI 22 | - **AI Integration:** FAL.AI Client SDK 23 | - **Node Version:** >=20.0.0 24 | 25 | ## Getting Started 26 | 27 | 1. Clone the repository: 28 | ```bash 29 | git clone https://github.com/yourusername/fal-ai-web-interface.git 30 | cd fal-ai-web-interface 31 | ``` 32 | 33 | 2. Install dependencies: 34 | ```bash 35 | npm install 36 | # or 37 | pnpm install 38 | # or 39 | yarn install 40 | ``` 41 | 42 | 3. Create a `.env.local` file in the root directory and add your FAL.AI credentials(https://fal.ai/dashboard/keys): 43 | ```env 44 | NEXT_PUBLIC_API_KEY=your_fal_api_key 45 | ``` 46 | 47 | 4. Run the development server: 48 | ```bash 49 | npm run dev 50 | # or 51 | pnpm dev 52 | # or 53 | yarn dev 54 | ``` 55 | 56 | 5. Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 57 | 58 | ## Project Structure 59 | 60 | ``` 61 | src/ 62 | ├── app/ # Next.js 14 App Router pages 63 | ├── components/ # Reusable UI components 64 | ├── lib/ # Utility functions and configurations 65 | ├── types/ # TypeScript type definitions 66 | └── styles/ # Global styles and Tailwind configurations 67 | ``` 68 | 69 | ## Key Dependencies 70 | 71 | - `@fal-ai/client` - Official FAL.AI client 72 | - `@fal-ai/server-proxy` - FAL.AI server proxy 73 | - `shadcn/ui` - UI component library 74 | - `tailwindcss` - Utility-first CSS framework 75 | 76 | ## Security 77 | 78 | - API keys are stored securely in encrypted client storage 79 | - Server-side proxy implementation for secure API communication 80 | - No sensitive credentials exposed in client-side code 81 | - Built-in rate limiting 82 | 83 | ## Performance 84 | 85 | - Leverages React Server Components for optimal performance 86 | - Streaming responses for AI operations 87 | - Lazy loading for non-critical components 88 | - Optimized image handling for AI-generated content 89 | 90 | ## Contributing 91 | 92 | Contributions are welcome! Please feel free to submit a Pull Request. 93 | 94 | ## License 95 | 96 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 97 | 98 | ## Deployment 99 | 100 | The application is optimized for deployment on Vercel. For other platforms, please ensure they support Next.js 14 and Edge Runtime. 101 | 102 | To deploy on Vercel: 103 | 104 | 1. Push your code to GitHub 105 | 2. Import your repository on Vercel 106 | 3. Add your environment variables 107 | 4. set env in vercel: NEXT_PUBLIC_API_KEY=your_fal_api_key or leave it empty 108 | 5. Deploy! 109 | 110 | ## Support 111 | 112 | For support, please open an issue in the GitHub repository or contact the maintainers. 113 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "src/app/globals.css", 9 | "baseColor": "zinc", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } -------------------------------------------------------------------------------- /next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from "next"; 2 | 3 | const nextConfig: NextConfig = { 4 | images: { 5 | remotePatterns: [ 6 | { 7 | protocol: 'https', 8 | hostname: 'fal.media', 9 | pathname: '/files/**', 10 | }, 11 | ], 12 | }, 13 | } 14 | 15 | export default nextConfig; 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simple-fluex-api-ui", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev --turbopack", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@fal-ai/client": "^1.2.3", 13 | "@fal-ai/server-proxy": "^1.1.1", 14 | "@radix-ui/react-checkbox": "^1.1.4", 15 | "@radix-ui/react-dialog": "^1.1.6", 16 | "@radix-ui/react-label": "^2.1.2", 17 | "@radix-ui/react-progress": "^1.1.2", 18 | "@radix-ui/react-radio-group": "^1.2.3", 19 | "@radix-ui/react-scroll-area": "^1.2.3", 20 | "@radix-ui/react-select": "^2.1.6", 21 | "@radix-ui/react-separator": "^1.1.2", 22 | "@radix-ui/react-slider": "^1.2.3", 23 | "@radix-ui/react-slot": "^1.1.2", 24 | "@radix-ui/react-switch": "^1.1.3", 25 | "@radix-ui/react-tabs": "^1.1.3", 26 | "@radix-ui/react-toast": "^1.2.6", 27 | "class-variance-authority": "^0.7.1", 28 | "clsx": "^2.1.1", 29 | "date-fns": "^4.1.0", 30 | "lucide-react": "^0.461.0", 31 | "next": "15.0.3", 32 | "react": "19.0.0-rc-66855b96-20241106", 33 | "react-dom": "19.0.0-rc-66855b96-20241106", 34 | "tailwind-merge": "^2.6.0", 35 | "tailwindcss-animate": "^1.0.7", 36 | "uuid": "^11.1.0", 37 | "zod": "^3.24.2" 38 | }, 39 | "devDependencies": { 40 | "@types/node": "^20.17.23", 41 | "@types/react": "^18.3.18", 42 | "@types/react-dom": "^18.3.5", 43 | "@types/uuid": "^10.0.0", 44 | "eslint": "^8.57.1", 45 | "eslint-config-next": "15.0.3", 46 | "postcss": "^8.5.3", 47 | "tailwindcss": "^3.4.17", 48 | "typescript": "^5.8.2" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/14790897/simple-flux-web-api-ui/67f1631a296e8651324db1732254806ee869e469/src/app/favicon.ico -------------------------------------------------------------------------------- /src/app/flux/[model-id]/page.tsx: -------------------------------------------------------------------------------- 1 | import { ImageGenerator } from "@/components/image-generator"; 2 | import { allModels } from "@/lib/models/registry"; 3 | import { notFound } from "next/navigation"; 4 | 5 | type Props = { 6 | params: Promise<{ "model-id": string }>; 7 | }; 8 | 9 | export default async function FluxModelPage({ params }: Props) { 10 | const { "model-id": modelId } = await params; 11 | 12 | // Find the model from our registry 13 | const model = allModels.find((m) => m.id.replace(/\//g, "-") === modelId); 14 | 15 | if (!model) { 16 | notFound(); 17 | } 18 | 19 | return ( 20 |
21 |
22 |

{model.name}

23 |

24 | Generate images using {model.name} 25 |

26 | 27 |
28 |
29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | body { 6 | font-family: Arial, Helvetica, sans-serif; 7 | } 8 | 9 | @layer base { 10 | :root { 11 | --background: 0 0% 100%; 12 | --foreground: 240 10% 3.9%; 13 | --card: 0 0% 100%; 14 | --card-foreground: 240 10% 3.9%; 15 | --popover: 0 0% 100%; 16 | --popover-foreground: 240 10% 3.9%; 17 | --primary: 240 5.9% 10%; 18 | --primary-foreground: 0 0% 98%; 19 | --secondary: 240 4.8% 95.9%; 20 | --secondary-foreground: 240 5.9% 10%; 21 | --muted: 240 4.8% 95.9%; 22 | --muted-foreground: 240 3.8% 46.1%; 23 | --accent: 240 4.8% 95.9%; 24 | --accent-foreground: 240 5.9% 10%; 25 | --destructive: 0 84.2% 60.2%; 26 | --destructive-foreground: 0 0% 98%; 27 | --border: 240 5.9% 90%; 28 | --input: 240 5.9% 90%; 29 | --ring: 240 10% 3.9%; 30 | --chart-1: 12 76% 61%; 31 | --chart-2: 173 58% 39%; 32 | --chart-3: 197 37% 24%; 33 | --chart-4: 43 74% 66%; 34 | --chart-5: 27 87% 67%; 35 | --radius: 0.5rem; 36 | } 37 | .dark { 38 | --background: 240 10% 3.9%; 39 | --foreground: 0 0% 98%; 40 | --card: 240 10% 3.9%; 41 | --card-foreground: 0 0% 98%; 42 | --popover: 240 10% 3.9%; 43 | --popover-foreground: 0 0% 98%; 44 | --primary: 0 0% 98%; 45 | --primary-foreground: 240 5.9% 10%; 46 | --secondary: 240 3.7% 15.9%; 47 | --secondary-foreground: 0 0% 98%; 48 | --muted: 240 3.7% 15.9%; 49 | --muted-foreground: 240 5% 64.9%; 50 | --accent: 240 3.7% 15.9%; 51 | --accent-foreground: 0 0% 98%; 52 | --destructive: 0 62.8% 30.6%; 53 | --destructive-foreground: 0 0% 98%; 54 | --border: 240 3.7% 15.9%; 55 | --input: 240 3.7% 15.9%; 56 | --ring: 240 4.9% 83.9%; 57 | --chart-1: 220 70% 50%; 58 | --chart-2: 160 60% 45%; 59 | --chart-3: 30 80% 55%; 60 | --chart-4: 280 65% 60%; 61 | --chart-5: 340 75% 55%; 62 | } 63 | } 64 | 65 | @layer base { 66 | * { 67 | @apply border-border; 68 | } 69 | body { 70 | @apply bg-background text-foreground; 71 | } 72 | } 73 | 74 | /* Interactive elements cursor styles */ 75 | button, a, [role="button"], select, summary, 76 | [type="button"], [type="reset"], [type="submit"], 77 | [type="checkbox"], [type="radio"] { 78 | cursor: pointer; 79 | } 80 | -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import "./globals.css"; 2 | import { Metadata } from "next"; 3 | import { Navbar } from "@/components/navbar"; 4 | import { Toaster } from "@/components/ui/toaster"; 5 | 6 | export const metadata: Metadata = { 7 | title: "FAL.AI Web Interface", 8 | description: "A modern web interface for FAL.AI image generation models", 9 | }; 10 | 11 | export default function RootLayout({ 12 | children, 13 | }: { 14 | children: React.ReactNode; 15 | }) { 16 | return ( 17 | 18 | 19 | 20 |
{children}
21 | 22 | 23 | 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import { Card, CardHeader, CardTitle } from "@/components/ui/card"; 3 | import { allModels } from "@/lib/models/registry"; 4 | 5 | export default function Home() { 6 | return ( 7 |
8 |
9 |

FAL.AI Web Interface

10 |

11 | Generate amazing images using FAL.AI's powerful AI models 12 |

13 |
14 | {allModels.map((model) => ( 15 | 20 | 21 | 22 | {model.name} 23 | 24 | 25 | 26 | ))} 27 |
28 |
29 |
30 | ); 31 | } -------------------------------------------------------------------------------- /src/components/api-key-input.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useState, useEffect } from "react"; 4 | import { Input } from "@/components/ui/input"; 5 | import { Button } from "@/components/ui/button"; 6 | import { Eye, EyeOff, Check, Key } from "lucide-react"; 7 | import { useToast } from "@/hooks/use-toast"; 8 | import { fal } from "@fal-ai/client"; 9 | 10 | const API_KEY_STORAGE_KEY = 'fal-ai-api-key'; 11 | 12 | function configureFalClient(apiKey: string) { 13 | fal.config({ 14 | credentials: apiKey, 15 | }); 16 | } 17 | 18 | export function ApiKeyInput() { 19 | const [isVisible, setIsVisible] = useState(false); 20 | const [apiKey, setApiKey] = useState(''); 21 | const [hasStoredKey, setHasStoredKey] = useState(false); 22 | const [isInputVisible, setIsInputVisible] = useState(false); 23 | const { toast } = useToast(); 24 | 25 | useEffect(() => { 26 | const storedKey = localStorage.getItem(API_KEY_STORAGE_KEY); 27 | if (storedKey) { 28 | setApiKey(storedKey); 29 | setHasStoredKey(true); 30 | configureFalClient(storedKey); 31 | } 32 | }, []); 33 | 34 | const handleSave = () => { 35 | if (!apiKey.trim()) { 36 | toast({ 37 | title: "Error", 38 | description: "Please enter a valid API key", 39 | variant: "destructive", 40 | }); 41 | return; 42 | } 43 | 44 | const trimmedKey = apiKey.trim(); 45 | localStorage.setItem(API_KEY_STORAGE_KEY, trimmedKey); 46 | configureFalClient(trimmedKey); 47 | setHasStoredKey(true); 48 | setIsInputVisible(false); 49 | toast({ 50 | title: "API Key Saved", 51 | description: "Your API key has been saved successfully.", 52 | }); 53 | }; 54 | 55 | if (!isInputVisible) { 56 | return ( 57 |
58 | 74 |
75 | ); 76 | } 77 | 78 | return ( 79 |
80 |
81 | setApiKey(e.target.value)} 85 | placeholder="Enter your FAL.AI API key" 86 | className="pr-8 w-[300px]" 87 | /> 88 |
89 | 97 | 104 | 111 |
112 | ); 113 | } -------------------------------------------------------------------------------- /src/components/image-generator.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Model } from "@/lib/types"; 4 | import { useState, useEffect } from "react"; 5 | import { GenerationSettings } from "./image-generator/generation-settings"; 6 | import { ImageDisplay } from "./image-generator/image-display"; 7 | import { GenerationsGallery } from "./image-generator/generations-gallery"; 8 | import { generateImage } from "@/lib/actions/generate-image"; 9 | import { useToast } from "@/hooks/use-toast"; 10 | import { Image, Generation } from "@/lib/types"; 11 | import { v4 as uuidv4 } from 'uuid'; 12 | 13 | const API_KEY_STORAGE_KEY = 'fal-ai-api-key'; 14 | const GENERATIONS_STORAGE_KEY = 'fal-ai-generations'; 15 | 16 | interface ImageGeneratorProps { 17 | model: Model; 18 | } 19 | 20 | export function ImageGenerator({ model }: ImageGeneratorProps) { 21 | const [prompt, setPrompt] = useState(""); 22 | const [isGenerating, setIsGenerating] = useState(false); 23 | const [result, setResult] = useState(null); 24 | const [generations, setGenerations] = useState([]); 25 | const { toast } = useToast(); 26 | 27 | const [parameters, setParameters] = useState>(() => { 28 | // Initialize parameters with default values from the model schema 29 | return Object.fromEntries( 30 | model.inputSchema 31 | .filter(param => param.default !== undefined) 32 | .map(param => [param.key, param.default]) 33 | ); 34 | }); 35 | 36 | // Load generations from localStorage on mount 37 | useEffect(() => { 38 | const savedGenerations = localStorage.getItem(GENERATIONS_STORAGE_KEY); 39 | if (savedGenerations) { 40 | try { 41 | setGenerations(JSON.parse(savedGenerations)); 42 | } catch (error) { 43 | console.error('Failed to parse saved generations:', error); 44 | } 45 | } 46 | }, []); 47 | 48 | async function handleGenerate() { 49 | console.log("🎨 Starting client-side image generation process"); 50 | 51 | const apiKey = 52 | localStorage.getItem(API_KEY_STORAGE_KEY) ?? 53 | process.env.NEXT_PUBLIC_API_KEY; 54 | if (!apiKey) { 55 | console.log("❌ No API key found in localStorage"); 56 | toast({ 57 | title: "API Key Required", 58 | description: "Please set your FAL.AI API key first", 59 | variant: "destructive", 60 | }); 61 | return; 62 | } 63 | 64 | console.log("🔄 Setting generation state..."); 65 | setIsGenerating(true); 66 | 67 | try { 68 | const allParameters = { 69 | ...parameters, 70 | prompt, 71 | }; 72 | 73 | console.log("📤 Sending generation request with parameters:", { 74 | modelId: model.id, 75 | parameters: { 76 | ...allParameters, 77 | prompt: allParameters.prompt?.substring(0, 50) + "...", 78 | }, 79 | }); 80 | 81 | const response = await generateImage(model, allParameters, apiKey); 82 | 83 | if (response.success) { 84 | console.log("✅ Generation successful:", { 85 | seed: response.seed, 86 | requestId: response.requestId, 87 | }); 88 | setResult(response.image); 89 | 90 | // Create a new generation record 91 | const newGeneration: Generation = { 92 | id: uuidv4(), 93 | modelId: model.id, 94 | modelName: model.name, 95 | prompt, 96 | parameters: allParameters, 97 | output: { 98 | images: [response.image], 99 | timings: response.timings || {}, 100 | seed: response.seed, 101 | has_nsfw_concepts: response.has_nsfw_concepts || [], 102 | }, 103 | timestamp: Date.now(), 104 | }; 105 | 106 | // Update generations in state and localStorage 107 | const updatedGenerations = [newGeneration, ...generations]; 108 | setGenerations(updatedGenerations); 109 | localStorage.setItem( 110 | GENERATIONS_STORAGE_KEY, 111 | JSON.stringify(updatedGenerations) 112 | ); 113 | 114 | toast({ 115 | title: "Image generated successfully", 116 | description: `Seed: ${response.seed}`, 117 | }); 118 | } else { 119 | console.error("❌ Generation failed:", response.error); 120 | toast({ 121 | title: "Generation failed", 122 | description: response.error, 123 | variant: "destructive", 124 | }); 125 | } 126 | } catch (error) { 127 | console.error("💥 Unexpected error during generation:", error); 128 | toast({ 129 | title: "Generation failed", 130 | description: 131 | error instanceof Error 132 | ? error.message 133 | : "An unexpected error occurred", 134 | variant: "destructive", 135 | }); 136 | } finally { 137 | console.log("🏁 Finishing generation process"); 138 | setIsGenerating(false); 139 | } 140 | } 141 | 142 | return ( 143 |
144 |
145 | 154 | 155 |
156 | 157 |
158 | ); 159 | } -------------------------------------------------------------------------------- /src/components/image-generator/generation-settings.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Button } from "@/components/ui/button"; 4 | import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; 5 | import { Textarea } from "@/components/ui/textarea"; 6 | import { Label } from "@/components/ui/label"; 7 | import { Model, ModelParameter } from "@/lib/types"; 8 | import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; 9 | import { Switch } from "@/components/ui/switch"; 10 | import { Input } from "@/components/ui/input"; 11 | 12 | interface GenerationSettingsProps { 13 | prompt: string; 14 | setPrompt: (prompt: string) => void; 15 | onGenerate: () => void; 16 | isGenerating: boolean; 17 | model: Model; 18 | parameters: Record; 19 | setParameters: (params: Record) => void; 20 | } 21 | 22 | export function GenerationSettings({ 23 | prompt, 24 | setPrompt, 25 | onGenerate, 26 | isGenerating, 27 | model, 28 | parameters, 29 | setParameters 30 | }: GenerationSettingsProps) { 31 | function renderParameter(param: ModelParameter) { 32 | if (param.key === 'prompt' || param.key === 'sync_mode' || param.key === 'enable_safety_checker') return null; 33 | 34 | const value = parameters[param.key] ?? param.default; 35 | const onChange = (newValue: any) => { 36 | setParameters({ ...parameters, [param.key]: newValue }); 37 | }; 38 | 39 | switch (param.type) { 40 | case 'enum': 41 | return ( 42 |
43 | 44 | 56 |
57 | ); 58 | 59 | case 'boolean': 60 | return ( 61 |
62 | 63 | 68 |
69 | ); 70 | 71 | case 'number': 72 | // Special handling for guidance_scale and num_inference_steps 73 | if (param.key === 'guidance_scale' || param.key === 'num_inference_steps') { 74 | const config = { 75 | guidance_scale: { 76 | min: 1, 77 | max: 10, 78 | step: 0.1, 79 | default: 3.5, 80 | decimals: 1 81 | }, 82 | num_inference_steps: { 83 | min: 1, 84 | max: 50, 85 | step: 1, 86 | default: 35, 87 | decimals: 0 88 | } 89 | }[param.key]; 90 | 91 | return ( 92 |
93 |
94 | 97 | 98 | {Number(value || config.default).toFixed(config.decimals)} 99 | 100 |
101 | onChange(Number(e.target.value))} 110 | /> 111 |
112 | ); 113 | } 114 | 115 | // Default number input for other numeric parameters 116 | return ( 117 |
118 | 119 | onChange(Number(e.target.value))} 125 | /> 126 |
127 | ); 128 | 129 | case 'array': 130 | if (param.key === 'loras') { 131 | const loras = value as Array<{ path: string; scale: number }> || []; 132 | const MAX_LORAS = 3; 133 | 134 | return ( 135 |
136 |
137 | 138 | 146 |
147 |
148 | {loras.map((lora, index) => ( 149 |
150 |
151 | 152 | 164 |
165 |
166 |
167 | { 172 | const newLoras = [...loras]; 173 | newLoras[index] = { ...lora, path: e.target.value }; 174 | onChange(newLoras); 175 | }} 176 | /> 177 |
178 |
179 | { 187 | const newLoras = [...loras]; 188 | newLoras[index] = { ...lora, scale: Number(e.target.value) }; 189 | onChange(newLoras); 190 | }} 191 | /> 192 | 193 | {lora.scale.toFixed(1)} 194 | 195 |
196 |
197 |
198 | ))} 199 |
200 |
201 | ); 202 | } 203 | return null; 204 | 205 | default: 206 | return null; 207 | } 208 | } 209 | 210 | // Group parameters by type for more efficient layout 211 | const groupParameters = () => { 212 | const enumParams: JSX.Element[] = []; 213 | const booleanParams: JSX.Element[] = []; 214 | const numberParams: JSX.Element[] = []; 215 | const otherParams: JSX.Element[] = []; 216 | 217 | model.inputSchema.forEach(param => { 218 | const rendered = renderParameter(param); 219 | if (!rendered) return; 220 | 221 | switch (param.type) { 222 | case 'enum': 223 | enumParams.push(rendered); 224 | break; 225 | case 'boolean': 226 | booleanParams.push(rendered); 227 | break; 228 | case 'number': 229 | numberParams.push(rendered); 230 | break; 231 | default: 232 | otherParams.push(rendered); 233 | } 234 | }); 235 | 236 | return { enumParams, booleanParams, numberParams, otherParams }; 237 | }; 238 | 239 | const { enumParams, booleanParams, numberParams, otherParams } = groupParameters(); 240 | 241 | return ( 242 | 243 | 244 | Settings 245 | Configure your image generation for {model.name} 246 | 247 | 248 |
249 | 250 |