Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

Nextjs quickstart for to generating and editing images with Google Gemini 2.0 Flash. It allows users to generate images from text prompts or edit existing images through natural language instructions, maintaining conversation context for iterative refinements. Try out the hosted demo at [Hugging Face Spaces](https://huggingface.co/spaces/philschmid/image-generation-editing).

https://github.com/user-attachments/assets/8ffa5ee3-1b06-46a9-8b5e-761edb0e00c3
<div align="center">
<iframe width="560" height="315" src="https://www.youtube.com/embed/pRdHdezL6FI?si=yExJ2CXNnHah2194" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
</div>

Get your `GEMINI_API_KEY` key [here](https://ai.google.dev/gemini-api/docs/api-key) and start building.

Expand Down
19 changes: 13 additions & 6 deletions app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,31 @@
"use client";
'use client';
import { useState } from "react";
import { ImageUpload } from "@/components/ImageUpload";
import { ImagePromptInput } from "@/components/ImagePromptInput";
import { ImageResultDisplay } from "@/components/ImageResultDisplay";
import { ImageUpload } from "../components/ImageUpload";
import { ImagePromptInput } from "../components/ImagePromptInput";
import { ImageResultDisplay } from "../components/ImageResultDisplay";
import { ImageIcon, Wand2 } from "lucide-react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { HistoryItem } from "@/lib/types";
import { Card, CardContent, CardHeader, CardTitle } from "../components/ui/card";
import { HistoryItem } from "../lib/types";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../components/ui/tabs";


export default function Home() {
const [image, setImage] = useState<string | null>(null);
const [generatedImage, setGeneratedImage] = useState<string | null>(null);
const [description, setDescription] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [error, setError] = useState<string | null>(null);
const [history, setHistory] = useState<HistoryItem[]>([]);

// ...rest of your code

const handleImageSelect = (imageData: string) => {
setImage(imageData || null);
setGeneratedImage(null); // Reset generated image when a new image is selected
};


const handlePromptSubmit = async (prompt: string) => {
try {
setLoading(true);
Expand Down
177 changes: 56 additions & 121 deletions components/ImageUpload.tsx
Original file line number Diff line number Diff line change
@@ -1,143 +1,78 @@
"use client";

import { useCallback, useState, useEffect } from "react";
import { useDropzone } from "react-dropzone";
import { Button } from "./ui/button";
import { Upload as UploadIcon, Image as ImageIcon, X } from "lucide-react";
// Modify your existing ImageUpload component to include tabs for upload and webcam
'use client';
import { ChangeEvent, useState } from "react";
import { Upload, ImageIcon } from "lucide-react";
import { Button } from "../components/ui/button";
import { WebcamCapture } from "../components/WebcamCapture";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../components/ui/tabs";

interface ImageUploadProps {
onImageSelect: (imageData: string) => void;
currentImage: string | null;
onError?: (error: string) => void;
}

export function formatFileSize(bytes: number): string {
if (bytes === 0) return "0 Bytes";
const k = 1024;
const sizes = ["Bytes", "KB", "MB", "GB", "TB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return (
Number.parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i]
);
}

export function ImageUpload({ onImageSelect, currentImage, onError }: ImageUploadProps) {
const [selectedFile, setSelectedFile] = useState<File | null>(null);
const [isLoading, setIsLoading] = useState(false);
export function ImageUpload({ onImageSelect, currentImage }: ImageUploadProps) {
const [selectedTab, setSelectedTab] = useState<string>("upload");

// Update the selected file when the current image changes
useEffect(() => {
if (!currentImage) {
setSelectedFile(null);
}
}, [currentImage]);

const onDrop = useCallback(
(acceptedFiles: File[], fileRejections) => {
if (fileRejections?.length > 0) {
const error = fileRejections[0].errors[0];
onError?.(error.message);
return;
}

const file = acceptedFiles[0];
if (!file) return;

setSelectedFile(file);
setIsLoading(true);

// Convert the file to base64
const handleFileChange = (e: ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
const reader = new FileReader();
reader.onload = (event) => {
if (event.target && event.target.result) {
const result = event.target.result as string;
onImageSelect(result);
if (event.target?.result) {
onImageSelect(event.target.result.toString());
}
setIsLoading(false);
};
reader.onerror = (error) => {
onError?.("Error reading file. Please try again.");
setIsLoading(false);
};
reader.readAsDataURL(file);
},
[onImageSelect]
);

const { getRootProps, getInputProps, isDragActive } = useDropzone({
onDrop,
accept: {
"image/png": [".png"],
"image/jpeg": [".jpg", ".jpeg"]
},
maxSize: 10 * 1024 * 1024, // 10MB
multiple: false
});
}
};

const handleRemove = () => {
setSelectedFile(null);
onImageSelect("");
const handleWebcamCapture = (imageData: string) => {
onImageSelect(imageData);
};

return (
<div className="w-full">
{!currentImage ? (
<div
{...getRootProps()}
className={`min-h-[150px] p-4 rounded-lg
${isDragActive ? "bg-secondary/50" : "bg-secondary"}
${isLoading ? "opacity-50 cursor-wait" : ""}
transition-colors duration-200 ease-in-out hover:bg-secondary/50
border-2 border-dashed border-secondary
cursor-pointer flex items-center justify-center gap-4
`}
>
<input {...getInputProps()} />
<div className="flex flex-row items-center" role="presentation">
<UploadIcon className="w-8 h-8 text-primary mr-3 flex-shrink-0" aria-hidden="true" />
<div className="">
<p className="text-sm font-medium text-foreground">
Drop your image here or click to browse
</p>
<p className="text-xs text-muted-foreground">
Maximum file size: 10MB
</p>
</div>
</div>
</div>
) : (
<div className="flex flex-col items-center p-4 rounded-lg bg-secondary">
<div className="flex w-full items-center mb-4">
<ImageIcon className="w-8 h-8 text-primary mr-3 flex-shrink-0" aria-hidden="true" />
<div className="flex-grow min-w-0">
<p className="text-sm font-medium truncate text-foreground">
{selectedFile?.name || "Current Image"}
</p>
{selectedFile && (
<Tabs
defaultValue="upload"
value={selectedTab}
onValueChange={setSelectedTab}
className="w-full"
>
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="upload">Upload Image</TabsTrigger>
<TabsTrigger value="webcam">Use Webcam</TabsTrigger>
</TabsList>
<TabsContent value="upload" className="py-6">
<div className="flex flex-col items-center justify-center w-full h-64 border-2 border-dashed rounded-lg bg-muted/50 hover:bg-muted/80 transition-colors cursor-pointer">
<label
htmlFor="image-upload"
className="flex flex-col items-center justify-center w-full h-full cursor-pointer"
>
<div className="flex flex-col items-center justify-center pt-5 pb-6">
<Upload className="w-8 h-8 mb-4 text-muted-foreground" />
<p className="mb-2 text-sm text-muted-foreground">
<span className="font-semibold">Click to upload</span> or drag
and drop
</p>
<p className="text-xs text-muted-foreground">
{formatFileSize(selectedFile?.size ?? 0)}
JPG, PNG, GIF or WEBP (MAX. 5MB)
</p>
)}
</div>
<Button
variant="ghost"
size="icon"
onClick={handleRemove}
className="flex-shrink-0 ml-2"
>
<X className="w-4 h-4" />
<span className="sr-only">Remove image</span>
</Button>
</div>
<input
id="image-upload"
type="file"
accept="image/*"
className="hidden"
onChange={handleFileChange}
/>
</label>
</div>
<div className="w-full overflow-hidden rounded-md">
<img
src={currentImage}
alt="Selected"
className="w-full h-auto object-contain"
/>
</div>
</div>
)}
</TabsContent>
<TabsContent value="webcam" className="py-6">
<WebcamCapture onCapture={handleWebcamCapture} />
</TabsContent>
</Tabs>
</div>
);
}
}
80 changes: 80 additions & 0 deletions components/WebcamCapture.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
'use client';
import { useState, useRef, useCallback } from "react";
import Webcam from "react-webcam";
import { Button } from "./ui/button";
import { Camera, Repeat } from "lucide-react";

interface WebcamCaptureProps {
onCapture: (imageData: string) => void;
}

export function WebcamCapture({ onCapture }: WebcamCaptureProps) {
const [isCapturing, setIsCapturing] = useState(false);
const [capturedImage, setCapturedImage] = useState<string | null>(null);
const webcamRef = useRef<Webcam>(null);

const handleStartCapture = () => {
setIsCapturing(true);
setCapturedImage(null);
};

const capture = useCallback(() => {
if (webcamRef.current) {
const imageSrc = webcamRef.current.getScreenshot();
if (imageSrc) {
setCapturedImage(imageSrc);
onCapture(imageSrc);
setIsCapturing(false);
}
}
}, [onCapture]);

const retake = () => {
setCapturedImage(null);
setIsCapturing(true);
};

return (
<div className="flex flex-col items-center space-y-4">
{!isCapturing && !capturedImage ? (
<Button
onClick={handleStartCapture}
variant="outline"
className="flex items-center gap-2"
>
<Camera className="w-4 h-4" />
Use Webcam
</Button>
) : isCapturing ? (
<div className="space-y-4">
<div className="border rounded-lg overflow-hidden">
<Webcam
audio={false}
ref={webcamRef}
screenshotFormat="image/jpeg"
videoConstraints={{
facingMode: "user",
}}
className="w-full"
/>
</div>
<Button onClick={capture} className="w-full">Capture Photo</Button>
</div>
) : capturedImage ? (
<div className="space-y-4">
<div className="border rounded-lg overflow-hidden">
<img src={capturedImage} alt="Captured" className="w-full" />
</div>
<Button
onClick={retake}
variant="outline"
className="flex items-center gap-2"
>
<Repeat className="w-4 h-4" />
Retake Photo
</Button>
</div>
) : null}
</div>
);
}
55 changes: 55 additions & 0 deletions components/ui/tabs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
"use client"

import * as React from "react"
import * as TabsPrimitive from "@radix-ui/react-tabs"

import { cn } from "../../lib/utils"

const Tabs = TabsPrimitive.Root

const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={cn(
"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
className
)}
{...props}
/>
))
TabsList.displayName = TabsPrimitive.List.displayName

const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
className
)}
{...props}
/>
))
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName

const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={cn(
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
className
)}
{...props}
/>
))
TabsContent.displayName = TabsPrimitive.Content.displayName

export { Tabs, TabsList, TabsTrigger, TabsContent }
Loading