diff --git a/ai/ai-samples/src/features/image-generation/index.tsx b/ai/ai-samples/src/features/image-generation/index.tsx index da08d1226..1c1058963 100644 --- a/ai/ai-samples/src/features/image-generation/index.tsx +++ b/ai/ai-samples/src/features/image-generation/index.tsx @@ -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([]); + 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(null); + + const fileInputRef = useRef(null); + + const chatSessionRef = useRef(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; + 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 ( -
-

image-generation

+
+
+

Image Generation & Iterative Editing

+ +
+

+ Ask Gemini to generate an image, or upload a reference file and ask the model to edit it iteratively. +

+ + {error && ( +
+ {error} +
+ )} + +
+ {messages.map((msg, idx) => ( +
+
+ {msg.text &&

{msg.text}

} + {/* Native browser rendering of the ordered segments */} + {msg.segments && msg.segments.map((segment, i) => { + + if (segment.type === 'text') { + return ( +

+ {segment.text} +

+ ); + } + + if (segment.type === 'image') { + return ( + Generated by Gemini + ); + } + + return null; + })} + +
+
+ ))} + {loading &&
Generating content...
} +
+ +
+ +
+ 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} + /> + +
+
); } \ No newline at end of file diff --git a/ai/ai-samples/src/features/image-generation/service.ts b/ai/ai-samples/src/features/image-generation/service.ts index 27c3a6599..e43d78b9b 100644 --- a/ai/ai-samples/src/features/image-generation/service.ts +++ b/ai/ai-samples/src/features/image-generation/service.ts @@ -1 +1,153 @@ -// Service for image-generation \ No newline at end of file +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 { + 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 { + 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 { + 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 { + 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 { + const messagePayload: (string | Part)[] = [prompt]; + if (file) { + messagePayload.push(await fileToGenerativePart(file)); + } + + const result = await chat.sendMessage(messagePayload); + const parts = result.response.candidates?.[0]?.content?.parts ?? []; + return extractTextAndImages(parts); +} \ No newline at end of file