Build AI Image Generator in Next.js with Flux.1 Kontext

Build AI Image Generator in Next.js with Flux.1 Kontext in Black Forest Labs
Create stunning AI images in seconds. In this comprehensive tutorial, you'll learn how to build a professional AI image generation application using Next.js and Black Forest Labs' powerful FLUX API. We'll cover everything from setup to deployment, including advanced features like image editing and iterative refinement.
What You'll Build
By the end of this tutorial, you'll have a fully functional AI image generator that can:
- Generate images from text prompts using FLUX.1 Kontext models
- Edit existing images with simple text instructions
- Handle multiple aspect ratios and output formats
- Implement proper error handling and loading states
- Store and manage generated images securely
Prerequisites
Before we start, make sure you have:
- Node.js 18+ installed on your machine
- Basic knowledge of React and Next.js
- A Black Forest Labs account (we'll set this up together)
Step 1: Setting Up Your Black Forest Labs Account
Create Your Account
First, let's get your BFL account ready:
- Visit dashboard.bfl.ai and create an account
- Confirm your email address
- Log in to access your dashboard
Generate Your API Key
Once logged in, you will arrive on your BFL Dashboard - there you can create an API KEY by clicking the Add Key button in your dashboard.
Important Security Note: Keep your API key secure and never expose it in client-side code. Treat it like a password!
Step 2: Initialize Your Next.js Project
Let's create a new Next.js project with all the necessary dependencies:
1# Create new Next.js project
2npx create-next-app@latest flux-ai-generator
3cd flux-ai-generator
4
5# Install required dependencies
6npm install axios lucide-react clsx tailwind-merge
7npm install -D @types/node
8
9# Install additional UI dependencies
10npm install @radix-ui/react-dialog @radix-ui/react-select
Step 3: Environment Configuration
Create your environment file with the BFL API configuration:
1# .env.local
2NEXT_PUBLIC_BFL_API_URL=https://api.bfl.ai
3BFL_API_KEY=your_api_key_here
4NEXT_PUBLIC_MAX_CONCURRENT_REQUESTS=24
Environment Variables Explained:
NEXT_PUBLIC_BFL_API_URL
: Primary Global Endpoint - Recommended for most use cases with automatic failover between clusters for enhanced uptimeBFL_API_KEY
: Your secure API key (server-side only)NEXT_PUBLIC_MAX_CONCURRENT_REQUESTS
: Rate limit of 24 active tasks maximum
Step 4: Core API Service
Make a solid service to handle BFL API interactions:
1// lib/bfl-service.ts
2interface GenerationRequest {
3 prompt: string;
4 aspect_ratio?: string;
5 seed?: number;
6 output_format?: 'jpeg' | 'png';
7 safety_tolerance?: number;
8}
9
10interface EditingRequest extends GenerationRequest {
11 input_image: string; // Base64 encoded image
12}
13
14interface BFLResponse {
15 id: string;
16 polling_url?: string;
17}
18
19interface GenerationResult {
20 id: string;
21 status: 'Ready' | 'Pending' | 'Error';
22 result?: {
23 sample: string;
24 };
25 error?: string;
26}
27
28class BFLService {
29 private readonly apiKey: string;
30 private readonly baseUrl: string;
31
32 constructor() {
33 this.apiKey = process.env.BFL_API_KEY!;
34 this.baseUrl = process.env.NEXT_PUBLIC_BFL_API_URL!;
35 }
36
37 /**
38 * Generate image from text prompt using FLUX.1 Kontext
39 */
40 async generateImage(request: GenerationRequest): Promise<BFLResponse> {
41 const response = await fetch(`${this.baseUrl}/v1/flux-kontext-pro`, {
42 method: 'POST',
43 headers: {
44 'Authorization': `Bearer ${this.apiKey}`,
45 'Content-Type': 'application/json',
46 },
47 body: JSON.stringify({
48 prompt: request.prompt,
49 aspect_ratio: request.aspect_ratio || '1:1',
50 seed: request.seed,
51 output_format: request.output_format || 'jpeg',
52 safety_tolerance: request.safety_tolerance || 2,
53 prompt_upsampling: true, // Recommended for T2I
54 }),
55 });
56
57 if (!response.ok) {
58 const error = await response.json();
59 throw new Error(error.detail || `HTTP ${response.status}`);
60 }
61
62 return response.json();
63 }
64
65 /**
66 * Edit existing image with text instructions
67 */
68 async editImage(request: EditingRequest): Promise<BFLResponse> {
69 const response = await fetch(`${this.baseUrl}/v1/flux-kontext-pro`, {
70 method: 'POST',
71 headers: {
72 'Authorization': `Bearer ${this.apiKey}`,
73 'Content-Type': 'application/json',
74 },
75 body: JSON.stringify({
76 prompt: request.prompt,
77 input_image: request.input_image,
78 seed: request.seed,
79 output_format: request.output_format || 'jpeg',
80 safety_tolerance: request.safety_tolerance || 2,
81 }),
82 });
83
84 if (!response.ok) {
85 const error = await response.json();
86 throw new Error(error.detail || `HTTP ${response.status}`);
87 }
88
89 return response.json();
90 }
91
92 /**
93 * Poll for generation result using polling URL
94 */
95 async pollResult(requestId: string, pollingUrl?: string): Promise<GenerationResult> {
96 // Use polling URL if provided (required for global endpoint)
97 const url = pollingUrl || `${this.baseUrl}/v1/get_result`;
98
99 const response = await fetch(url, {
100 method: 'POST',
101 headers: {
102 'Authorization': `Bearer ${this.apiKey}`,
103 'Content-Type': 'application/json',
104 },
105 body: JSON.stringify({ id: requestId }),
106 });
107
108 if (!response.ok) {
109 throw new Error(`Failed to poll result: ${response.status}`);
110 }
111
112 return response.json();
113 }
114
115 /**
116 * Download and store image from BFL delivery URL
117 */
118 async downloadAndStoreImage(imageUrl: string): Promise<string> {
119 // Download the image
120 const response = await fetch(imageUrl);
121 if (!response.ok) {
122 throw new Error('Failed to download image');
123 }
124
125 const buffer = await response.arrayBuffer();
126 const base64 = Buffer.from(buffer).toString('base64');
127
128 // In a real app, you'd upload to your CDN/storage service
129 // For demo purposes, we'll return the base64 data URL
130 return `data:image/jpeg;base64,${base64}`;
131 }
132}
133
134export const bflService = new BFLService();
Step 5: API Routes
Create Next.js API routes to handle image generation securely:
1// app/api/generate/route.ts
2import { NextRequest, NextResponse } from 'next/server';
3import { bflService } from '@/lib/bfl-service';
4
5export async function POST(request: NextRequest) {
6 try {
7 const { prompt, aspect_ratio, seed, output_format } = await request.json();
8
9 // Validate input
10 if (!prompt || typeof prompt !== 'string') {
11 return NextResponse.json(
12 { error: 'Valid prompt is required' },
13 { status: 400 }
14 );
15 }
16
17 // Submit generation request
18 const result = await bflService.generateImage({
19 prompt,
20 aspect_ratio,
21 seed,
22 output_format,
23 });
24
25 return NextResponse.json(result);
26 } catch (error) {
27 console.error('Generation error:', error);
28 return NextResponse.json(
29 { error: error.message || 'Generation failed' },
30 { status: 500 }
31 );
32 }
33}
1// app/api/edit/route.ts
2import { NextRequest, NextResponse } from 'next/server';
3import { bflService } from '@/lib/bfl-service';
4
5export async function POST(request: NextRequest) {
6 try {
7 const { prompt, input_image, seed, output_format } = await request.json();
8
9 if (!prompt || !input_image) {
10 return NextResponse.json(
11 { error: 'Prompt and input image are required' },
12 { status: 400 }
13 );
14 }
15
16 const result = await bflService.editImage({
17 prompt,
18 input_image,
19 seed,
20 output_format,
21 });
22
23 return NextResponse.json(result);
24 } catch (error) {
25 console.error('Edit error:', error);
26 return NextResponse.json(
27 { error: error.message || 'Edit failed' },
28 { status: 500 }
29 );
30 }
31}
1// app/api/poll/route.ts
2import { NextRequest, NextResponse } from 'next/server';
3import { bflService } from '@/lib/bfl-service';
4
5export async function POST(request: NextRequest) {
6 try {
7 const { id, polling_url } = await request.json();
8
9 if (!id) {
10 return NextResponse.json(
11 { error: 'Request ID is required' },
12 { status: 400 }
13 );
14 }
15
16 const result = await bflService.pollResult(id, polling_url);
17
18 // If image is ready, download and store it
19 if (result.status === 'Ready' && result.result?.sample) {
20 try {
21 const storedImage = await bflService.downloadAndStoreImage(
22 result.result.sample
23 );
24 result.result.sample = storedImage;
25 } catch (downloadError) {
26 console.error('Download error:', downloadError);
27 // Continue with original URL if download fails
28 }
29 }
30
31 return NextResponse.json(result);
32 } catch (error) {
33 console.error('Polling error:', error);
34 return NextResponse.json(
35 { error: error.message || 'Polling failed' },
36 { status: 500 }
37 );
38 }
39}
Step 6: Custom Hooks for State Management
Create reusable hooks to manage generation and editing states:
1// hooks/useImageGeneration.ts
2import { useState, useCallback } from 'react';
3
4interface GenerationOptions {
5 prompt: string;
6 aspectRatio?: string;
7 seed?: number;
8 outputFormat?: 'jpeg' | 'png';
9}
10
11interface GenerationState {
12 isGenerating: boolean;
13 progress: string;
14 result: string | null;
15 error: string | null;
16}
17
18export function useImageGeneration() {
19 const [state, setState] = useState<GenerationState>({
20 isGenerating: false,
21 progress: '',
22 result: null,
23 error: null,
24 });
25
26 const generateImage = useCallback(async (options: GenerationOptions) => {
27 setState({
28 isGenerating: true,
29 progress: 'Submitting request...',
30 result: null,
31 error: null,
32 });
33
34 try {
35 // Submit generation request
36 const response = await fetch('/api/generate', {
37 method: 'POST',
38 headers: { 'Content-Type': 'application/json' },
39 body: JSON.stringify(options),
40 });
41
42 if (!response.ok) {
43 const error = await response.json();
44 throw new Error(error.error || 'Generation failed');
45 }
46
47 const { id, polling_url } = await response.json();
48
49 // Poll for results
50 setState(prev => ({ ...prev, progress: 'Generating image...' }));
51
52 const result = await pollForResult(id, polling_url);
53
54 setState({
55 isGenerating: false,
56 progress: '',
57 result: result.result.sample,
58 error: null,
59 });
60 } catch (error) {
61 setState({
62 isGenerating: false,
63 progress: '',
64 result: null,
65 error: error.message,
66 });
67 }
68 }, []);
69
70 const pollForResult = async (id: string, pollingUrl?: string) => {
71 const maxAttempts = 60; // 5 minutes maximum
72 let attempts = 0;
73
74 while (attempts < maxAttempts) {
75 const response = await fetch('/api/poll', {
76 method: 'POST',
77 headers: { 'Content-Type': 'application/json' },
78 body: JSON.stringify({ id, polling_url: pollingUrl }),
79 });
80
81 if (!response.ok) {
82 throw new Error('Polling failed');
83 }
84
85 const result = await response.json();
86
87 if (result.status === 'Ready') {
88 return result;
89 }
90
91 if (result.status === 'Error') {
92 throw new Error(result.error || 'Generation failed');
93 }
94
95 // Wait 5 seconds before next poll
96 await new Promise(resolve => setTimeout(resolve, 5000));
97 attempts++;
98 }
99
100 throw new Error('Generation timeout');
101 };
102
103 const reset = useCallback(() => {
104 setState({
105 isGenerating: false,
106 progress: '',
107 result: null,
108 error: null,
109 });
110 }, []);
111
112 return {
113 ...state,
114 generateImage,
115 reset,
116 };
117}
Step 7: Main Image Generator Component
Create the main component with a modern, user-easy interface:
1// components/ImageGenerator.tsx
2'use client';
3
4import { useState } from 'react';
5import { useImageGeneration } from '@/hooks/useImageGeneration';
6import { AspectRatioSelector } from './AspectRatioSelector';
7import { GeneratedImage } from './GeneratedImage';
8import { LoadingSpinner } from './LoadingSpinner';
9import { Wand2, Download, RefreshCw } from 'lucide-react';
10
11const EXAMPLE_PROMPTS = [
12 "A serene mountain landscape at sunset with vibrant colors",
13 "A futuristic cityscape with flying cars and neon lights",
14 "A cute robot reading a book in a cozy library",
15 "Abstract art with flowing colors and geometric shapes",
16];
17
18export function ImageGenerator() {
19 const [prompt, setPrompt] = useState('');
20 const [aspectRatio, setAspectRatio] = useState('1:1');
21 const [seed, setSeed] = useState<number | undefined>();
22 const { isGenerating, progress, result, error, generateImage, reset } = useImageGeneration();
23
24 const handleGenerate = async () => {
25 if (!prompt.trim()) return;
26
27 await generateImage({
28 prompt: prompt.trim(),
29 aspectRatio,
30 seed,
31 outputFormat: 'jpeg',
32 });
33 };
34
35 const handleExampleClick = (examplePrompt: string) => {
36 setPrompt(examplePrompt);
37 };
38
39 const generateRandomSeed = () => {
40 setSeed(Math.floor(Math.random() * 1000000));
41 };
42
43 return (
44 <div className="max-w-4xl mx-auto p-6 space-y-8">
45 <div className="text-center space-y-4">
46 <h1 className="text-4xl font-bold bg-gradient-to-r from-purple-600 to-pink-600 bg-clip-text text-transparent">
47 AI Image Generator
48 </h1>
49 <p className="text-gray-600 text-lg">
50 Create stunning images with FLUX.1 Kontext - powered by Black Forest Labs
51 </p>
52 </div>
53
54 {/* Generation Form */}
55 <div className="bg-white rounded-xl shadow-lg p-6 space-y-6">
56 {/* Prompt Input */}
57 <div className="space-y-2">
58 <label className="block text-sm font-medium text-gray-700">
59 Describe your image
60 </label>
61 <textarea
62 value={prompt}
63 onChange={(e) => setPrompt(e.target.value)}
64 placeholder="A beautiful sunset over a mountain range..."
65 className="w-full p-4 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent resize-none"
66 rows={4}
67 disabled={isGenerating}
68 />
69 </div>
70
71 {/* Example Prompts */}
72 <div className="space-y-2">
73 <label className="block text-sm font-medium text-gray-700">
74 Try these examples:
75 </label>
76 <div className="flex flex-wrap gap-2">
77 {EXAMPLE_PROMPTS.map((example, index) => (
78 <button
79 key={index}
80 onClick={() => handleExampleClick(example)}
81 className="px-3 py-1 text-sm bg-gray-100 hover:bg-gray-200 rounded-full transition-colors"
82 disabled={isGenerating}
83 >
84 {example.slice(0, 30)}...
85 </button>
86 ))}
87 </div>
88 </div>
89
90 {/* Options */}
91 <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
92 <AspectRatioSelector
93 value={aspectRatio}
94 onChange={setAspectRatio}
95 disabled={isGenerating}
96 />
97
98 <div className="space-y-2">
99 <label className="block text-sm font-medium text-gray-700">
100 Seed (optional)
101 </label>
102 <div className="flex gap-2">
103 <input
104 type="number"
105 value={seed || ''}
106 onChange={(e) => setSeed(e.target.value ? parseInt(e.target.value) : undefined)}
107 placeholder="Random"
108 className="flex-1 p-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
109 disabled={isGenerating}
110 />
111 <button
112 onClick={generateRandomSeed}
113 className="px-3 py-2 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors"
114 disabled={isGenerating}
115 >
116 <RefreshCw className="w-4 h-4" />
117 </button>
118 </div>
119 </div>
120 </div>
121
122 {/* Generate Button */}
123 <button
124 onClick={handleGenerate}
125 disabled={!prompt.trim() || isGenerating}
126 className="w-full py-4 px-6 bg-gradient-to-r from-purple-600 to-pink-600 text-white rounded-lg font-medium disabled:opacity-50 disabled:cursor-not-allowed hover:from-purple-700 hover:to-pink-700 transition-all duration-200 flex items-center justify-center gap-2"
127 >
128 {isGenerating ? (
129 <>
130 <LoadingSpinner />
131 {progress || 'Generating...'}
132 </>
133 ) : (
134 <>
135 <Wand2 className="w-5 h-5" />
136 Generate Image
137 </>
138 )}
139 </button>
140 </div>
141
142 {/* Results */}
143 {error && (
144 <div className="bg-red-50 border border-red-200 rounded-lg p-4">
145 <div className="flex items-center gap-2">
146 <div className="w-2 h-2 bg-red-500 rounded-full"></div>
147 <p className="text-red-700 font-medium">Generation Error</p>
148 </div>
149 <p className="text-red-600 mt-1">{error}</p>
150 <button
151 onClick={reset}
152 className="mt-2 text-red-600 hover:text-red-700 underline"
153 >
154 Try again
155 </button>
156 </div>
157 )}
158
159 {result && (
160 <GeneratedImage
161 src={result}
162 alt={prompt}
163 prompt={prompt}
164 aspectRatio={aspectRatio}
165 seed={seed}
166 />
167 )}
168 </div>
169 );
170}
Step 8: Supporting Components
Create the supporting UI components:
1// components/AspectRatioSelector.tsx
2interface AspectRatioSelectorProps {
3 value: string;
4 onChange: (value: string) => void;
5 disabled?: boolean;
6}
7
8const ASPECT_RATIOS = [
9 { value: '1:1', label: 'Square (1:1)', description: '1024×1024' },
10 { value: '16:9', label: 'Landscape (16:9)', description: '1365×768' },
11 { value: '9:16', label: 'Portrait (9:16)', description: '768×1365' },
12 { value: '4:3', label: 'Standard (4:3)', description: '1182×886' },
13 { value: '3:4', label: 'Portrait (3:4)', description: '886×1182' },
14];
15
16export function AspectRatioSelector({ value, onChange, disabled }: AspectRatioSelectorProps) {
17 return (
18 <div className="space-y-2">
19 <label className="block text-sm font-medium text-gray-700">
20 Aspect Ratio
21 </label>
22 <select
23 value={value}
24 onChange={(e) => onChange(e.target.value)}
25 disabled={disabled}
26 className="w-full p-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
27 >
28 {ASPECT_RATIOS.map((ratio) => (
29 <option key={ratio.value} value={ratio.value}>
30 {ratio.label} - {ratio.description}
31 </option>
32 ))}
33 </select>
34 </div>
35 );
36}
1// components/GeneratedImage.tsx
2import { useState } from 'react';
3import { Download, Copy, Edit } from 'lucide-react';
4
5interface GeneratedImageProps {
6 src: string;
7 alt: string;
8 prompt: string;
9 aspectRatio: string;
10 seed?: number;
11}
12
13export function GeneratedImage({ src, alt, prompt, aspectRatio, seed }: GeneratedImageProps) {
14 const [isDownloading, setIsDownloading] = useState(false);
15
16 const handleDownload = async () => {
17 setIsDownloading(true);
18 try {
19 const response = await fetch(src);
20 const blob = await response.blob();
21 const url = URL.createObjectURL(blob);
22
23 const link = document.createElement('a');
24 link.href = url;
25 link.download = `ai-generated-${Date.now()}.jpg`;
26 document.body.appendChild(link);
27 link.click();
28 document.body.removeChild(link);
29
30 URL.revokeObjectURL(url);
31 } catch (error) {
32 console.error('Download failed:', error);
33 } finally {
34 setIsDownloading(false);
35 }
36 };
37
38 const copyPrompt = () => {
39 navigator.clipboard.writeText(prompt);
40 };
41
42 return (
43 <div className="bg-white rounded-xl shadow-lg overflow-hidden">
44 <div className="relative">
45 <img
46 src={src}
47 alt={alt}
48 className="w-full h-auto"
49 style={{ aspectRatio: aspectRatio.replace(':', '/') }}
50 />
51
52 {/* Action Buttons */}
53 <div className="absolute top-4 right-4 flex gap-2">
54 <button
55 onClick={handleDownload}
56 disabled={isDownloading}
57 className="p-2 bg-black/50 hover:bg-black/70 text-white rounded-lg transition-colors"
58 title="Download Image"
59 >
60 <Download className="w-4 h-4" />
61 </button>
62 </div>
63 </div>
64
65 {/* Image Details */}
66 <div className="p-6 space-y-4">
67 <div>
68 <h3 className="font-medium text-gray-900 mb-2">Generated Image</h3>
69 <p className="text-gray-600 text-sm">{prompt}</p>
70 </div>
71
72 <div className="flex flex-wrap gap-4 text-sm text-gray-500">
73 <span>Aspect Ratio: {aspectRatio}</span>
74 {seed && <span>Seed: {seed}</span>}
75 </div>
76
77 <div className="flex gap-2">
78 <button
79 onClick={copyPrompt}
80 className="flex items-center gap-2 px-3 py-2 bg-gray-100 hover:bg-gray-200 rounded-lg text-sm transition-colors"
81 >
82 <Copy className="w-4 h-4" />
83 Copy Prompt
84 </button>
85 </div>
86 </div>
87 </div>
88 );
89}
1// components/LoadingSpinner.tsx
2export function LoadingSpinner() {
3 return (
4 <div className="animate-spin rounded-full h-5 w-5 border-b-2 border-white"></div>
5 );
6}
Step 9: Main Page Implementation
1// app/page.tsx
2import { ImageGenerator } from '@/components/ImageGenerator';
3
4export default function Home() {
5 return (
6 <main className="min-h-screen bg-gradient-to-br from-purple-50 to-pink-50">
7 <div className="container mx-auto py-8">
8 <ImageGenerator />
9 </div>
10 </main>
11 );
12}
Step 10: Advanced Features
Image Editing Functionality
Add image editing capabilities to your application:
1// components/ImageEditor.tsx
2'use client';
3
4import { useState, useCallback } from 'react';
5import { Upload, Edit } from 'lucide-react';
6
7export function ImageEditor() {
8 const [originalImage, setOriginalImage] = useState<string | null>(null);
9 const [editPrompt, setEditPrompt] = useState('');
10 const [isEditing, setIsEditing] = useState(false);
11 const [editedImage, setEditedImage] = useState<string | null>(null);
12
13 const handleImageUpload = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
14 const file = event.target.files?.[0];
15 if (!file) return;
16
17 const reader = new FileReader();
18 reader.onload = (e) => {
19 const result = e.target?.result as string;
20 setOriginalImage(result);
21 setEditedImage(null);
22 };
23 reader.readAsDataURL(file);
24 }, []);
25
26 const handleEdit = async () => {
27 if (!originalImage || !editPrompt.trim()) return;
28
29 setIsEditing(true);
30 try {
31 // Convert data URL to base64
32 const base64 = originalImage.split(',')[1];
33
34 const response = await fetch('/api/edit', {
35 method: 'POST',
36 headers: { 'Content-Type': 'application/json' },
37 body: JSON.stringify({
38 prompt: editPrompt.trim(),
39 input_image: base64,
40 }),
41 });
42
43 if (!response.ok) {
44 throw new Error('Edit failed');
45 }
46
47 const { id, polling_url } = await response.json();
48
49 // Poll for results (similar to generation)
50 const result = await pollForResult(id, polling_url);
51 setEditedImage(result.result.sample);
52 } catch (error) {
53 console.error('Edit error:', error);
54 } finally {
55 setIsEditing(false);
56 }
57 };
58
59 return (
60 <div className="max-w-4xl mx-auto p-6 space-y-8">
61 <div className="text-center">
62 <h2 className="text-3xl font-bold mb-4">Image Editor</h2>
63 <p className="text-gray-600">
64 Upload an image and describe how you want to edit it
65 </p>
66 </div>
67
68 {/* Upload Section */}
69 <div className="bg-white rounded-xl shadow-lg p-6">
70 <div className="space-y-4">
71 <label className="block">
72 <span className="sr-only">Choose image to edit</span>
73 <input
74 type="file"
75 accept="image/*"
76 onChange={handleImageUpload}
77 className="block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-purple-50 file:text-purple-700 hover:file:bg-purple-100"
78 />
79 </label>
80
81 {originalImage && (
82 <div className="grid grid-cols-1 md:grid-cols-2 gap-6 mt-4">
83 <div>
84 <h3 className="font-medium mb-2">Original Image</h3>
85 <div className="relative aspect-square bg-gray-100 rounded-lg overflow-hidden">
86 <img
87 src={originalImage}
88 alt="Original uploaded image"
89 className="object-contain w-full h-full"
90 />
91 </div>
92 </div>
93
94 <div>
95 <h3 className="font-medium mb-2">Edited Result</h3>
96 <div className="relative aspect-square bg-gray-100 rounded-lg overflow-hidden">
97 {isEditing ? (
98 <div className="absolute inset-0 flex items-center justify-center">
99 <div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-purple-500"></div>
100 </div>
101 ) : editedImage ? (
102 <img
103 src={editedImage}
104 alt="AI edited image"
105 className="object-contain w-full h-full"
106 />
107 ) : (
108 <div className="absolute inset-0 flex items-center justify-center text-gray-400">
109 <p>Edit result will appear here</p>
110 </div>
111 )}
112 </div>
113 </div>
114 </div>
115 )}
116
117 <div className="mt-4 space-y-4">
118 <div>
119 <label className="block text-sm font-medium text-gray-700 mb-1">
120 Edit Instructions
121 </label>
122 <textarea
123 value={editPrompt}
124 onChange={(e) => setEditPrompt(e.target.value)}
125 placeholder="Describe how you want to edit the image..."
126 className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-purple-500 focus:border-purple-500"
127 rows={3}
128 disabled={!originalImage || isEditing}
129 />
130 </div>
131
132 <button
133 onClick={handleEdit}
134 disabled={isEditing || !editPrompt.trim() || !originalImage}
135 className="w-full py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-purple-600 hover:bg-purple-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500 disabled:bg-gray-300 disabled:cursor-not-allowed"
136 >
137 {isEditing ? 'Processing...' : 'Edit Image'}
138 </button>
139 </div>
140 </div>
141 </div>
142 </div>
143 );
144}
Let's implement the handleImageUpload
and handleEdit
functions to make our editor fully functional:
1// Inside the ImageEditor component
2const handleImageUpload = (e) => {
3 const file = e.target.files?.[0];
4 if (!file) return;
5
6 // Reset state when new image is uploaded
7 setEditPrompt('');
8 setEditedImage(null);
9
10 const reader = new FileReader();
11 reader.onload = (event) => {
12 setOriginalImage(event.target?.result as string);
13 };
14 reader.readAsDataURL(file);
15};
16
17const handleEdit = async () => {
18 if (!originalImage || !editPrompt.trim()) return;
19
20 setIsEditing(true);
21
22 try {
23 // Call our API endpoint
24 const response = await fetch('/api/edit-image', {
25 method: 'POST',
26 headers: {
27 'Content-Type': 'application/json',
28 },
29 body: JSON.stringify({
30 image: originalImage,
31 prompt: editPrompt,
32 }),
33 });
34
35 if (!response.ok) {
36 throw new Error('Failed to edit image');
37 }
38
39 const data = await response.json();
40
41 // Poll for results
42 const pollingInterval = setInterval(async () => {
43 const pollResponse = await fetch('/api/poll', {
44 method: 'POST',
45 headers: {
46 'Content-Type': 'application/json',
47 },
48 body: JSON.stringify({
49 id: data.id,
50 pollingUrl: data.polling_url,
51 }),
52 });
53
54 const pollData = await pollResponse.json();
55
56 if (pollData.status === 'Ready' && pollData.result?.sample) {
57 setEditedImage(`data:image/jpeg;base64,${pollData.result.sample}`);
58 clearInterval(pollingInterval);
59 setIsEditing(false);
60 } else if (pollData.status === 'Error') {
61 throw new Error(pollData.error || 'Failed to process image');
62 }
63 }, 1000);
64
65 // Clean up interval after 5 minutes (timeout)
66 setTimeout(() => {
67 clearInterval(pollingInterval);
68 if (isEditing) {
69 setIsEditing(false);
70 alert('Image editing timed out. Please try again.');
71 }
72 }, 300000);
73 } catch (error) {
74 console.error('Error editing image:', error);
75 alert('Failed to edit image. Please try again.');
76 setIsEditing(false);
77 }
78};
Today we're releasing FLUX.1 Kontext - a suite of generative flow matching models that allow you to generate and edit images. Unlike traditional text-to-image models, Kontext understands both text AND images as input, enabling true in-context generation and editing.