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

AndryAndry Dina
Next.js AI Image Generator with Black Forest Labs FLUX API

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:

  1. Visit dashboard.bfl.ai and create an account
  2. Confirm your email address
  3. 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:

bash
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:

bash
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 uptime
  • BFL_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:

typescript
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:

typescript
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}
typescript
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}
typescript
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:

typescript
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:

tsx
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:

tsx
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}
tsx
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}
tsx
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

tsx
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:

tsx
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:

jsx
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};

Similar articles

Never miss an update

Subscribe to receive news and special offers.

By subscribing you agree to our Privacy Policy.