-
Notifications
You must be signed in to change notification settings - Fork 3.7k
feat/image-generation #1078
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+303
−5
Merged
feat/image-generation #1078
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
ae3fd3c
Implement Image-Generation
sedanah-m ab3ff3e
fix:fixing the bug where interleaved text and image contexts were fla…
sedanah-m e35777d
fix: verify that the extracted string exists before resolving the pro…
sedanah-m 670c112
fix: guard check to prevent race conditions or duplicate spam submiss…
sedanah-m eaf569d
style: standardizing error handling
sedanah-m 80a182b
fix: cleaning up error guard check
sedanah-m File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; | ||
| 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> | ||
| ); | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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]; | ||
|
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); | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.