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
75 changes: 69 additions & 6 deletions ai/ai-samples/README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,72 @@
# Firebase AI Samples

A modular migration of the Firebase AI logic, demonstrating different capabilities:
- Text Generation
- Chat
- Multimodal
- Structured Output
- Function Calling
- Image Generation

## Samples

You can open this sample as a Node/React project and run it in your local browser. When doing so, you need to add this sample app to a Firebase project on the Firebase console. You can add multiple sample apps to the same Firebase project; no need to create separate projects for each app.

This repository demonstrates the following capabilities:
* Text Generation
* Chat
* Multimodal
* Structured Output
* Function Calling
* Image Generation

## Setup & Configuration

To connect this sample app to your Firebase project, register a new Web App in your Firebase Console to generate your Firebase configuration object.

1. Navigate to this directory and install dependencies:
```bash
npm install

2. Add your Firebase config

Go to console.firebase.google.com and follow the Firebase AI Logic guided setup to enable the API and choose your gemini API provider.
Copy the example config file and fill in your project values. Open src/config/firebase-config.ts and replace the placeholder values with your firebase project config (found in Project Settings -> your apps)


3. Running the samples

For a full app experience to browse all features:
npm run dev

To run indivual features in isolated mode, run the single feature directly without the app shell using one of these scripts:

npm run dev:text #Text Generation
npm run dev:chat #Chat
npm run dev:multimodal #Multimodal
npm run dev:structured #Structured Output
npm run dev:function #Function Calling
npm run dev:image #Image Generation

After running any of the above commands, open your browser to https://localhost:*** (provided in the console)

## Copy service.ts for platform agnostic use

All AI logic is decoupled from the React UI. If you want to use these features in your own project, navigate to any src/features/*/service.ts file. These files are framework-agnostic and can be safely copy-pasted into any JavaScript or TypeScript web project.



## App Check

App check protects your API Key from unauthorized use. It is not required to run the samples locally but highly recommended before deployig to production.

Debug token:
firebaseAIService.ts includes App Check intilization for local development. To enable it:

1. Set VITE_APPCHECK_DEBUG_TOKEN=true in your .env.local file
2. On the first run, a deug token will be printed in the browser console.
3. Copy that token and register it in the Firebase Console under
App Check -> Apps -> your apps -> Debug Token

Production setup:
For production, use reCAPTCHA v3 as the App Check provider:

1. Go to the Firebase Console -> App Check -> Register your app
2. Choose reCaptcha v3 and follow the setup steps
3. Add your reCaptcha site key to firebase-config.ts

See the [App Check Docs](https://firebase.google.com/docs/app-check/web/recaptcha-provider) for full instruction.
10 changes: 9 additions & 1 deletion ai/ai-samples/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,22 @@
"type": "module",
"scripts": {
"dev": "vite",
"dev:text": "VITE_ISOLATED_FEATURE=text-generation vite",
"dev:chat": "VITE_ISOLATED_FEATURE=chat vite",
"dev:multimodal": "VITE_ISOLATED_FEATURE=multimodal vite",
"dev:structured": "VITE_ISOLATED_FEATURE=structured-output vite",
"dev:function": "VITE_ISOLATED_FEATURE=function-calling vite",
"dev:image": "VITE_ISOLATED_FEATURE=image-generation vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"firebase": "^12.2.1",
"react": "^19.2.1",
"react-dom": "^19.2.1"
"react-dom": "^19.2.1",
"react-router-dom": "^7.17.0",
"zod": "^4.4.3"
},
"devDependencies": {
"@types/react": "^19.0.10",
Expand Down
58 changes: 52 additions & 6 deletions ai/ai-samples/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,56 @@
import React from 'react'
import { Link, Outlet, useLocation } from 'react-router-dom';
import TextGenerationView from './features/text-generation';
import ChatView from './features/chat';
import MultimodalView from './features/multimodal';
import StructuredOutputView from './features/structured-output';
import FunctionCallingView from './features/function-calling';
import ImageGenerationView from './features/image-generation';

const NAV_ITEMS = [
{ path: '/text-generation', label: 'Text Generation' },
{ path: '/chat', label: 'Chat' },
{ path: '/multimodal', label: 'Multimodal' },
{ path: '/structured-output', label: 'Structured Output' },
{ path: '/function-calling', label: 'Function Calling' },
{ path: '/image-generation', label: 'Image Generation' },
];

export default function App() {
const { pathname } = useLocation();
const isolatedFeature = import.meta.env.VITE_ISOLATED_FEATURE;
// If running an isolated script, bypass the shell entirely
if (isolatedFeature) {
switch (isolatedFeature) {
case 'text-generation': return <TextGenerationView />;
case 'chat': return <ChatView />;
case 'multimodal': return <MultimodalView />;
case 'structured-output': return <StructuredOutputView />;
case 'function-calling': return <FunctionCallingView />;
case 'image-generation': return <ImageGenerationView />;
}
}

// Otherwise, return the multi-feature app shell layout
return (
<div>
<h1>Firebase AI Samples</h1>
<p>Modular Firebase AI capabilities.</p>
<div className="app-shell">
<nav className="sidebar">
<h1 className="sidebar-title">Firebase AI Samples</h1>
<ul className="nav-list">
{NAV_ITEMS.map(({ path, label }) => (
<li key={path}>
<Link
to={path}
className={`nav-link ${pathname === path ? 'nav-link-active' : ''}`}
>
{label}
</Link>
</li>
))}
</ul>
</nav>
<main className="content">
<Outlet />
</main>
</div>
)
}
);
}
156 changes: 151 additions & 5 deletions ai/ai-samples/src/features/chat/index.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,155 @@
import React from 'react';
import React, { useState, useRef, useEffect } from 'react';
import { ChatSession } from 'firebase/ai';
import { startNewChat, sendChatMessage } from './service';

type Message = {
role: 'user' | 'model';
text: string;
};

export default function ChatView() {
const [messages, setMessages] = useState<Message[]>([]);
const [input, setInput] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);

// Use a ref to hold the active chat session from the Firebase AI SDK.
// This prevents the session from being recreated on every React render.
const chatSessionRef = useRef<ChatSession | null>(null);

// Initialize the chat when the component mounts
useEffect(() => {
handleResetChat();
}, []);

const handleResetChat = () => {
try {
chatSessionRef.current = startNewChat();
setError(null);
} catch (err: any) {
setError(err.message || 'Failed to initialize chat session. Please check your Firebase configuration.');
}
setMessages([]);
setInput('');
};

const handleSendMessage = async (e: React.FormEvent) => {
e.preventDefault();
if (!input.trim() || !chatSessionRef.current) return;

const userMessage = input.trim();
setInput(''); // Clear input immediately
setError(null);
setLoading(true);

// Optimistically add user message to UI
setMessages((prev) => [...prev, { role: 'user', text: userMessage }]);

try {
// Call framework-agnostic service layer
const responseText = await sendChatMessage(chatSessionRef.current, userMessage);

// Add model response to UI
setMessages((prev) => [...prev, { role: 'model', text: responseText }]);
} catch (err: any) {
setError(err.message || 'Failed to send message. Check console for details.');
} finally {
setLoading(false);
}
};

export default function Feature() {
return (
<div>
<h2>chat</h2>
<div style={{ maxWidth: '800px', margin: '0 auto', padding: '24px', fontFamily: 'system-ui, sans-serif' }}>
<header style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '24px' }}>
<div>
<h2 style={{ fontSize: '1.8rem', margin: '0 0 8px 0', color: '#1a73e8' }}>Multi-Turn Chat</h2>
<p style={{ color: '#5f6368', margin: 0 }}>Maintains persistent history in a single session.</p>
</div>
<button
onClick={handleResetChat}
style={{ padding: '8px 16px', backgroundColor: '#f1f3f4', color: '#3c4043', border: '1px solid #dadce0', borderRadius: '4px', cursor: 'pointer' }}
>
Reset Chat
</button>
</header>

{/* Chat History Window */}
<div style={{
height: '400px',
overflowY: 'auto',
border: '1px solid #dadce0',
borderRadius: '8px',
padding: '16px',
marginBottom: '16px',
backgroundColor: '#f8f9fa',
display: 'flex',
flexDirection: 'column',
gap: '12px'
}}>
{messages.length === 0 && (
<div style={{ color: '#80868b', textAlign: 'center', marginTop: 'auto', marginBottom: 'auto' }}>
No messages yet. Say hello!
</div>
)}

{messages.map((msg, index) => (
<div key={index} style={{
alignSelf: msg.role === 'user' ? 'flex-end' : 'flex-start',
backgroundColor: msg.role === 'user' ? '#d2e3fc' : '#ffffff',
border: '1px solid',
borderColor: msg.role === 'user' ? '#aecbfa' : '#dadce0',
padding: '12px 16px',
borderRadius: '16px',
maxWidth: '80%'
}}>
<strong style={{ display: 'block', fontSize: '0.8rem', color: '#5f6368', marginBottom: '4px' }}>
{msg.role === 'user' ? 'You' : 'Gemini'}
</strong>
<div style={{ whiteSpace: 'pre-wrap', margin: 0 }}>{msg.text}</div>
</div>
))}
{loading && <div style={{ alignSelf: 'flex-start', color: '#80868b', fontStyle: 'italic' }}>Gemini is typing...</div>}
</div>

{error && (
<div style={{ marginBottom: '16px', padding: '12px', backgroundColor: '#fce8e6', color: '#c5221f', borderRadius: '8px' }}>
<strong>Error:</strong> {error}
</div>
)}

{/* Input Area */}
<form onSubmit={handleSendMessage} style={{ display: 'flex', gap: '8px' }}>
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Type your message..."
disabled={loading}
style={{
flex: 1,
padding: '12px',
borderRadius: '8px',
border: '1px solid #dadce0',
fontSize: '1rem',
}}
/>
<button
type="submit"
disabled={loading || !input.trim()}
style={{
padding: '0 24px',
backgroundColor: loading || !input.trim() ? '#ccc' : '#1a73e8',
color: 'white',
border: 'none',
borderRadius: '8px',
fontSize: '1rem',
fontWeight: 500,
cursor: loading || !input.trim() ? 'not-allowed' : 'pointer',
}}
>
Send
</button>
</form>
</div>
);
}
}
30 changes: 30 additions & 0 deletions ai/ai-samples/src/features/chat/service.ts
Original file line number Diff line number Diff line change
@@ -1 +1,31 @@
// Service for chat
import { ChatSession } from 'firebase/ai';
import { getAiModel } from '../../services/firebaseAIService';

/**
* Initializes a new multi-turn chat session.
* The SDK's ChatSession automatically maintains the conversation history.
* * @returns A new ChatSession instance.
*/
export function startNewChat(): ChatSession {
const model = getAiModel('gemini-3.5-flash');
return model.startChat({
history: [],
});
}
Comment thread
sedanah-m marked this conversation as resolved.

/**
* Sends a message to an existing chat session and returns the response text.
* * @param chat The active ChatSession instance.
* @param message The user's message string.
* @returns The text string response generated by the model.
*/
export async function sendChatMessage(chat: ChatSession, message: string): Promise<string> {
try {
const result = await chat.sendMessage(message);
return result.response.text();
} catch (error) {
console.error('Error sending chat message:', error);
throw error;
}
}
4 changes: 2 additions & 2 deletions ai/ai-samples/src/features/function-calling/index.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import React from 'react';

export default function Feature() {
export default function FunctionCallingFeature() {
return (
<div>
<h2>function-calling</h2>
</div>
);
}
}
2 changes: 1 addition & 1 deletion ai/ai-samples/src/features/function-calling/service.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
// Service for function-calling
// Service for function-calling
4 changes: 2 additions & 2 deletions ai/ai-samples/src/features/image-generation/index.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import React from 'react';

export default function Feature() {
export default function ImageGenerationFeature() {
return (
<div>
<h2>image-generation</h2>
</div>
);
}
}
2 changes: 1 addition & 1 deletion ai/ai-samples/src/features/image-generation/service.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
// Service for image-generation
// Service for image-generation
Loading
Loading