Skip to content
Draft
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
7 changes: 0 additions & 7 deletions ai/ai-samples/src/config/firebase-config.ts

This file was deleted.

102 changes: 98 additions & 4 deletions ai/ai-samples/src/features/multimodal/index.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,103 @@
import React from 'react';
import { useState, useRef } from 'react';
import { generateMultimodalContent } from './service';

export default function MultimodalView() {
const [prompt, setPrompt] = useState('Describe these files in detail.');
const [response, setResponse] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);

const fileInputRef = useRef<HTMLInputElement>(null);

const handleGenerate = async () => {
const fileArray = Array.from(fileInputRef.current?.files ?? []);

if (fileArray.length === 0) {
setError('Please select at least one file (Image or PDF).');
return;
}

const cleanedPrompt = prompt.trim();

if (!cleanedPrompt) {
setError('Please enter a prompt.');
return;
}

setLoading(true);
setError(null);
setResponse('');

try {
const resultText = await generateMultimodalContent(cleanedPrompt, fileArray);
setResponse(resultText);
} catch (error: unknown) {
const message = error instanceof Error ? error.message : 'An error occurred during multimodal generation.';
setError(message);
} finally {
setLoading(false);
}
};

export default function MultimodalFeature() {
return (
<div>
<h2>multimodal</h2>
<div style={{ padding: '20px', maxWidth: '600px', margin: '0 auto' }}>
<h2>Multimodal Generation</h2>
<p style={{ color: '#666', marginBottom: '20px' }}>
Upload images or PDFs alongside a text prompt to generate content.
</p>

<div style={{ marginBottom: '15px' }}>
<label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold' }}>
Upload Files:
</label>
<input
type="file"
multiple
ref={fileInputRef}
accept="image/*,application/pdf"
style={{ width: '100%', padding: '8px' }}
/>
</div>

<div style={{ marginBottom: '15px' }}>
<label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold' }}>
Prompt:
</label>
<textarea
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
rows={4}
style={{ width: '100%', padding: '8px', fontFamily: 'inherit' }}
/>
</div>

<button
onClick={handleGenerate}
disabled={loading}
style={{
padding: '10px 20px',
cursor: loading ? 'not-allowed' : 'pointer',
backgroundColor: loading ? '#ccc' : '#007BFF',
color: '#fff',
border: 'none',
borderRadius: '4px'
}}
>
{loading ? 'Analyzing Files...' : 'Generate Content'}
</button>

{error && (
<div style={{ color: '#D8000C', backgroundColor: '#FFD2D2', padding: '10px', marginTop: '15px', borderRadius: '4px' }}>
<strong>Error:</strong> {error}
</div>
)}

{response && (
<div style={{ marginTop: '20px', borderTop: '1px solid #eee', paddingTop: '15px' }}>
<h3>Response:</h3>
<p style={{ whiteSpace: 'pre-wrap', lineHeight: '1.5' }}>{response}</p>
</div>
)}
</div>
);
}
57 changes: 56 additions & 1 deletion ai/ai-samples/src/features/multimodal/service.ts
Original file line number Diff line number Diff line change
@@ -1 +1,56 @@
// Service for multimodal
import { getAiModel } from '../../services/firebaseAIService';
import { Part } from 'firebase/ai';

/**
* Converts a standard browser File object (Image, PDF, etc.) into a Firebase AI SDK Part.
* Uses the native browser FileReader API to extract the Base64 string.
* @param file The DOM File object from an <input type="file">
* @returns A Promise that resolves to an InlineData Part object
*/
export async function fileToGenerativePart(file: File): Promise<Part> {
return new Promise((resolve, reject) => {
const reader = new FileReader();

reader.onloadend = () => {
const result = reader.result;
if (typeof result === 'string') {
const splitResult = result.split(',');
if (splitResult.length > 1) {
resolve({
inlineData: {
data: splitResult[1],
mimeType: file.type
}
});
return;
}
}
reject(new Error('Failed to parse file data as Base64.'));
};

reader.onerror = () => reject(reader.error || new Error('Error reading file.'));
reader.readAsDataURL(file);
});
}

// Note: The Gemini API restricts the total size of inline data payloads.
// To process larger files without hitting HTTP 413 errors (PayLoad Too Large),
// See the official Firebase AI documentation for current file size limits and workarounds:
// https://firebase.google.com/docs/ai-logic/solutions/cloud-storage

/**
* Sends a multimodal request (text + files) to the Gemini model.
* @param prompt The string instruction sent to the model.
* @param files An array of standard DOM File objects.
* @returns The text string response generated by the model.
*/
export async function generateMultimodalContent(prompt: string, files: File[]): Promise<string> {
try {
const fileParts = await Promise.all(files.map(fileToGenerativePart));
const model = getAiModel('gemini-3.5-flash');
const result = await model.generateContent([prompt, ...fileParts]);
return result.response.text();
} catch (error: unknown) {
throw error instanceof Error ? error : new Error('An unknown error occurred during generation.');
}
}
Loading
Loading