Skip to content
Merged
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
154 changes: 150 additions & 4 deletions ai/ai-samples/src/features/image-generation/index.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,155 @@
import React from 'react';
import { useState, useRef, useEffect } from 'react';
import { ChatSession, ImageConfigAspectRatio } from 'firebase/ai';
import { startImageChat, sendImageChatMessage } from './service';
import { ImageGenerationSegment } from './service';

interface ChatMessage {
role: 'user' | 'model';
text?: string;
segments?: ImageGenerationSegment[];
}

export default function ImageGenerationView() {
const [messages, setMessages] = useState<ChatMessage[]>([]);
const [prompt, setPrompt] = useState('Generate an image of the Eiffel Tower with fireworks in the background.');
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);

const fileInputRef = useRef<HTMLInputElement>(null);

const chatSessionRef = useRef<ChatSession | null>(null);
useEffect(() => {
try {
chatSessionRef.current = startImageChat(ImageConfigAspectRatio.SQUARE_1x1);
setError(null);
} catch (err: unknown) {
const message = err instanceof Error ? err.message : 'Failed to initialize chat session. Please check your Firebase configuration.';
setError(message);
}
}, []);

const handleGenerate = async () => {
if (loading) return;
const cleanedPrompt = prompt.trim();
if (!cleanedPrompt || !chatSessionRef.current) return;
Comment thread
sedanah-m marked this conversation as resolved.
const files = Array.from(fileInputRef.current?.files ?? []);
const referenceFile = files.length > 0 ? files[0] : undefined;
setLoading(true);
setError(null);
setPrompt('');

try {
setMessages((prev) => [...prev, { role: 'user', text: cleanedPrompt }]);
const result = await sendImageChatMessage(chatSessionRef.current, cleanedPrompt, referenceFile);
setMessages((prev) => [...prev, {
role: 'model',
segments: result.segments
}]);

if (fileInputRef.current) fileInputRef.current.value = '';
} catch (err: unknown) {
console.error(err);
const message = err instanceof Error ? err.message : 'An error occurred during image generation.';
setError(message);

} finally {
setLoading(false);
}
};

const handleResetChat = () => {
try {
chatSessionRef.current = startImageChat(ImageConfigAspectRatio.SQUARE_1x1);
setMessages([]);
setError(null);
setPrompt('');
if (fileInputRef.current) fileInputRef.current.value = '';
} catch (err: unknown) {
const message = err instanceof Error ? err.message : 'Failed to reset chat session.';
setError(message);
}
};

export default function ImageGenerationFeature() {
return (
<div>
<h2>image-generation</h2>
<div style={{ padding: '20px', maxWidth: '800px', margin: '0 auto', fontFamily: 'sans-serif' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<h2>Image Generation & Iterative Editing</h2>
<button onClick={handleResetChat} style={{ padding: '8px 12px', cursor: 'pointer' }}>
Reset Session
</button>
</div>
<p style={{ color: '#666', marginBottom: '20px' }}>
Ask Gemini to generate an image, or upload a reference file and ask the model to edit it iteratively.
</p>

{error && (
<div style={{ padding: '15px', backgroundColor: '#fee', color: '#c00', marginBottom: '20px', borderRadius: '4px' }}>
{error}
</div>
)}

<div style={{ border: '1px solid #ccc', borderRadius: '8px', padding: '20px', minHeight: '400px', marginBottom: '20px', backgroundColor: '#f9f9f9', display: 'flex', flexDirection: 'column', gap: '20px', overflowY: 'auto', maxHeight: '600px' }}>
{messages.map((msg, idx) => (
<div key={idx} style={{ textAlign: msg.role === 'user' ? 'right' : 'left' }}>
<div style={{ display: 'inline-block', padding: '15px', borderRadius: '8px', backgroundColor: msg.role === 'user' ? '#007bff' : '#e9ecef', color: msg.role === 'user' ? '#fff' : '#000', maxWidth: '85%' }}>
{msg.text && <p style={{ margin: '0 0 10px 0', whiteSpace: 'pre-wrap' }}>{msg.text}</p>}
{/* Native browser rendering of the ordered segments */}
{msg.segments && msg.segments.map((segment, i) => {

if (segment.type === 'text') {
return (
<p key={i} style={{ whiteSpace: 'pre-wrap', marginTop: '10px' }}>
{segment.text}
</p>
);
}

if (segment.type === 'image') {
return (
<img
key={i}
src={`data:${segment.mimeType};base64,${segment.base64}`}
alt="Generated by Gemini"
style={{ maxWidth: '100%', borderRadius: '4px', marginTop: '10px' }}
/>
);
}

return null;
})}

</div>
</div>
))}
{loading && <div style={{ color: '#666', fontStyle: 'italic' }}>Generating content...</div>}
</div>

<div style={{ display: 'flex', gap: '10px', flexDirection: 'column' }}>
<input
type="file"
ref={fileInputRef}
accept="image/*"
disabled={loading}
style={{ padding: '5px' }}
/>
<div style={{ display: 'flex', gap: '10px' }}>
<input
type="text"
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
placeholder="e.g., Make the sky purple and add a spaceship..."
style={{ flex: 1, padding: '10px', borderRadius: '4px', border: '1px solid #ccc' }}
disabled={loading}
/>
<button
onClick={handleGenerate}
disabled={loading || (!prompt.trim() && !fileInputRef.current?.files?.length)}
style={{ padding: '10px 20px', borderRadius: '4px', backgroundColor: '#007bff', color: '#fff', border: 'none', cursor: loading ? 'not-allowed' : 'pointer' }}
>
Send
</button>
</div>
</div>
</div>
);
}
154 changes: 153 additions & 1 deletion ai/ai-samples/src/features/image-generation/service.ts
Original file line number Diff line number Diff line change
@@ -1 +1,153 @@
// Service for image-generation
import { ChatSession, ImageConfigAspectRatio, ImageConfigImageSize, Part, ResponseModality } from 'firebase/ai';
import { getAiModel } from '../../services/firebaseAIService';

export type ImageGenerationSegment =
| { type: 'text'; text: string }
| { type: 'image'; mimeType: string; base64: string };
export interface ImageGenerationResult {
segments: ImageGenerationSegment[];
}

/**
* Helper: Safely extracts text and image Base64 data from a response parts array.
* Iterates over all parts and capturing everything.
*/
function extractTextAndImages(parts: Part[] = []): ImageGenerationResult {
const segments: ImageGenerationSegment[] = [];

for (const part of parts) {
if (part.text) {
segments.push({ type: 'text', text: part.text });
}
if (part.inlineData) {
segments.push({
type: 'image',
mimeType: part.inlineData.mimeType,
base64: part.inlineData.data
});
}
}

return { segments };
}

/**
* Helper: Converts a standard browser File object into a Firebase AI SDK Part.
* Uses the native browser FileReader API to extract the Base64 string.
*/
export async function fileToGenerativePart(file: File): Promise<Part> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onloadend = () => {
if (typeof reader.result !== 'string') {
return reject(new Error("Failed to parse file data as Base64."));
}

const base64Data = reader.result.split(',')[1];

if (!base64Data) {
return reject(new Error("Failed to extract Base64 data from file."));
}
resolve({
inlineData: {
data: base64Data,
mimeType: file.type
}
});
};

reader.onerror = reject;
reader.readAsDataURL(file);
});
}

/**
* Concept 1 & 5: Generate Images (Text-Only Input) + Image Configuration
* Demonstrates unary text-to-image generation, injecting custom aspect ratio and size parameters.
*/
export async function generateImage(
prompt: string,
aspectRatio: ImageConfigAspectRatio = ImageConfigAspectRatio.SQUARE_1x1,
imageSize: ImageConfigImageSize = ImageConfigImageSize.SIZE_1K
): Promise<ImageGenerationResult> {
const model = getAiModel('gemini-3.1-flash-lite-image', {
generationConfig: {
responseModalities: [ResponseModality.IMAGE],
imageConfig: { aspectRatio, imageSize }
}
});

const result = await model.generateContent(prompt);
const parts = result.response.candidates?.[0]?.content?.parts ?? [];
return extractTextAndImages(parts);
}

/**
* Concept 2: Generate Interleaved Images and Text
* Demonstrates instructing the model to return both text blocks and images in a single unary response.
*/
export async function generateInterleavedContent(prompt: string): Promise<ImageGenerationResult> {
const model = getAiModel('gemini-3.1-flash-lite-image', {
generationConfig: {
responseModalities: [ResponseModality.TEXT, ResponseModality.IMAGE]
}
});

const result = await model.generateContent(prompt);
const parts = result.response.candidates?.[0]?.content?.parts ?? [];
return extractTextAndImages(parts);
}

/**
* Concept 3: Edit Images (Text-and-Image Input)
* Demonstrates unary multimodal prompting where you pass a reference image and a text instruction.
*/
export async function editSingleImage(prompt: string, file: File): Promise<ImageGenerationResult> {
const imagePart = await fileToGenerativePart(file);
const model = getAiModel('gemini-3.1-flash-lite-image', {
generationConfig: {
responseModalities: [ResponseModality.IMAGE]
}
});

const result = await model.generateContent([prompt, imagePart]);
const parts = result.response.candidates?.[0]?.content?.parts ?? [];
return extractTextAndImages(parts);
}

/**
* Concept 4: Iterate and Edit Images Using Multi-Turn Chat
* Initializes a stateful chat session specifically for iterative visual editing.
*/
export function startImageChat(aspectRatio: ImageConfigAspectRatio = ImageConfigAspectRatio.SQUARE_1x1): ChatSession {
const model = getAiModel('gemini-3.1-flash-lite-image', {
generationConfig: {
responseModalities: [ResponseModality.TEXT, ResponseModality.IMAGE],
imageConfig: { aspectRatio }
}
});

return model.startChat({ history: [] });
}

/**
* Sends a message to the active image chat session.
* For the initial turn, you can pass a reference file. Follow-up turns can omit the file
* and rely purely on the ChatSession history.
* Note: The SDK's ChatSession automatically appends prompts and responses to the history array behind the scenes.
* For long iterative image sessions, generated base64 image strings will accumulate in memory.
*/
export async function sendImageChatMessage(
chat: ChatSession,
prompt: string,
file?: File
): Promise<ImageGenerationResult> {
const messagePayload: (string | Part)[] = [prompt];
Comment thread
rafikhan marked this conversation as resolved.
if (file) {
messagePayload.push(await fileToGenerativePart(file));
}

const result = await chat.sendMessage(messagePayload);
const parts = result.response.candidates?.[0]?.content?.parts ?? [];
return extractTextAndImages(parts);
}
Loading