diff --git a/ai/ai-samples/README.md b/ai/ai-samples/README.md
index 45616631a..4c8de7b80 100644
--- a/ai/ai-samples/README.md
+++ b/ai/ai-samples/README.md
@@ -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.
\ No newline at end of file
diff --git a/ai/ai-samples/package.json b/ai/ai-samples/package.json
index 9cc5eaebd..36d35540f 100644
--- a/ai/ai-samples/package.json
+++ b/ai/ai-samples/package.json
@@ -5,6 +5,12 @@
"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"
@@ -12,7 +18,9 @@
"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",
diff --git a/ai/ai-samples/src/App.tsx b/ai/ai-samples/src/App.tsx
index c9b04bf56..91e8df651 100644
--- a/ai/ai-samples/src/App.tsx
+++ b/ai/ai-samples/src/App.tsx
@@ -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 ;
+ case 'chat': return ;
+ case 'multimodal': return ;
+ case 'structured-output': return ;
+ case 'function-calling': return ;
+ case 'image-generation': return ;
+ }
+ }
+
+ // Otherwise, return the multi-feature app shell layout
return (
-
-
Firebase AI Samples
-
Modular Firebase AI capabilities.
+
+
+
+
+
- )
-}
+ );
+}
\ No newline at end of file
diff --git a/ai/ai-samples/src/features/chat/index.tsx b/ai/ai-samples/src/features/chat/index.tsx
index f41a77cab..3843f5a98 100644
--- a/ai/ai-samples/src/features/chat/index.tsx
+++ b/ai/ai-samples/src/features/chat/index.tsx
@@ -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([]);
+ const [input, setInput] = useState('');
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState(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(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 (
-
-
chat
+
+
+
+
Multi-Turn Chat
+
Maintains persistent history in a single session.
+
+
+
+
+ {/* Chat History Window */}
+
+ {messages.length === 0 && (
+
+ No messages yet. Say hello!
+
+ )}
+
+ {messages.map((msg, index) => (
+
+
+ {msg.role === 'user' ? 'You' : 'Gemini'}
+
+
{msg.text}
+
+ ))}
+ {loading &&
Gemini is typing...
}
+
+
+ {error && (
+
+ Error: {error}
+
+ )}
+
+ {/* Input Area */}
+
);
-}
+}
\ No newline at end of file
diff --git a/ai/ai-samples/src/features/chat/service.ts b/ai/ai-samples/src/features/chat/service.ts
index c942f90c9..411c7b7ff 100644
--- a/ai/ai-samples/src/features/chat/service.ts
+++ b/ai/ai-samples/src/features/chat/service.ts
@@ -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: [],
+ });
+}
+
+/**
+ * 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 {
+ try {
+ const result = await chat.sendMessage(message);
+ return result.response.text();
+ } catch (error) {
+ console.error('Error sending chat message:', error);
+ throw error;
+ }
+}
\ No newline at end of file
diff --git a/ai/ai-samples/src/features/function-calling/index.tsx b/ai/ai-samples/src/features/function-calling/index.tsx
index 10f0ac7ec..83f2c413a 100644
--- a/ai/ai-samples/src/features/function-calling/index.tsx
+++ b/ai/ai-samples/src/features/function-calling/index.tsx
@@ -1,9 +1,9 @@
import React from 'react';
-export default function Feature() {
+export default function FunctionCallingFeature() {
return (
function-calling
);
-}
+}
\ No newline at end of file
diff --git a/ai/ai-samples/src/features/function-calling/service.ts b/ai/ai-samples/src/features/function-calling/service.ts
index 1d926001b..6e9696125 100644
--- a/ai/ai-samples/src/features/function-calling/service.ts
+++ b/ai/ai-samples/src/features/function-calling/service.ts
@@ -1 +1 @@
-// Service for function-calling
+// Service for function-calling
\ No newline at end of file
diff --git a/ai/ai-samples/src/features/image-generation/index.tsx b/ai/ai-samples/src/features/image-generation/index.tsx
index 026aea372..da08d1226 100644
--- a/ai/ai-samples/src/features/image-generation/index.tsx
+++ b/ai/ai-samples/src/features/image-generation/index.tsx
@@ -1,9 +1,9 @@
import React from 'react';
-export default function Feature() {
+export default function ImageGenerationFeature() {
return (
image-generation
);
-}
+}
\ 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 4bddbe4a5..27c3a6599 100644
--- a/ai/ai-samples/src/features/image-generation/service.ts
+++ b/ai/ai-samples/src/features/image-generation/service.ts
@@ -1 +1 @@
-// Service for image-generation
+// Service for image-generation
\ No newline at end of file
diff --git a/ai/ai-samples/src/features/multimodal/index.tsx b/ai/ai-samples/src/features/multimodal/index.tsx
index a18674ff4..73a48218b 100644
--- a/ai/ai-samples/src/features/multimodal/index.tsx
+++ b/ai/ai-samples/src/features/multimodal/index.tsx
@@ -1,9 +1,9 @@
import React from 'react';
-export default function Feature() {
+export default function MultimodalFeature() {
return (
multimodal
);
-}
+}
\ No newline at end of file
diff --git a/ai/ai-samples/src/features/multimodal/service.ts b/ai/ai-samples/src/features/multimodal/service.ts
index 53742af07..5e0f76038 100644
--- a/ai/ai-samples/src/features/multimodal/service.ts
+++ b/ai/ai-samples/src/features/multimodal/service.ts
@@ -1 +1 @@
-// Service for multimodal
+// Service for multimodal
\ No newline at end of file
diff --git a/ai/ai-samples/src/features/text-generation/index.tsx b/ai/ai-samples/src/features/text-generation/index.tsx
index 34ab0187c..7cf7034a6 100644
--- a/ai/ai-samples/src/features/text-generation/index.tsx
+++ b/ai/ai-samples/src/features/text-generation/index.tsx
@@ -1,9 +1,49 @@
-import React from 'react';
+import { useState } from 'react';
+import { generateText } from './service';
+
+export default function TextGeneration() {
+ const [prompt, setPrompt] = useState('');
+ const [response, setResponse] = useState('');
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState(null);
+
+ const handleGenerate = async () => {
+ if (!prompt.trim()) return;
+
+ setLoading(true);
+ setError(null);
+ setResponse('');
+
+ try {
+ // Direct call to the decoupled logic service
+ const text = await generateText(prompt);
+ setResponse(text);
+ } catch (err: any) {
+ setError(err.message || 'An unexpected error occurred');
+ } finally {
+ setLoading(false);
+ }
+ };
+
-export default function Feature() {
return (
-
-
text-generation
+
+
Text Generation
+
+
);
-}
+}
\ No newline at end of file
diff --git a/ai/ai-samples/src/features/text-generation/service.ts b/ai/ai-samples/src/features/text-generation/service.ts
index 2f10c4fbc..50bd2f1eb 100644
--- a/ai/ai-samples/src/features/text-generation/service.ts
+++ b/ai/ai-samples/src/features/text-generation/service.ts
@@ -1 +1,58 @@
-// Service for text-generation
+import { getAiModel } from '../../services/firebaseAIService';
+
+/**
+ * Generates text from a text prompt.
+ * @param prompt The string instruction sent to the model.
+ * @returns The text string response generated by the model.
+ */
+export async function generateText(prompt: string): Promise {
+ try {
+ const model = getAiModel('gemini-3.5-flash');
+ const result = await model.generateContent(prompt);
+
+ return result.response.text();
+ } catch (error) {
+ console.error('Error generating text with Firebase AI:', error);
+ throw error;
+ }
+}
+
+/**
+ * Streams text from a text prompt, yielding chunks as they arrive.
+ * @param prompt The string instruction sent to the model.
+ * @param onChunk Callback fired for each non-empty text chunk.
+ */
+export async function streamText(prompt: string, onChunk: (chunk: string) => void): Promise {
+ try {
+ const model = getAiModel('gemini-3.5-flash');
+ const result = await model.generateContentStream(prompt);
+
+ for await (const chunk of result.stream) {
+ const chunkText = chunk.text();
+ if (chunkText) {
+ onChunk(chunkText);
+ }
+ }
+ } catch (error) {
+ console.error('Error streaming text with Firebase AI:', error);
+ throw error;
+ }
+}
+
+/**
+ * Generates text using a specific system instruction to guide the model's behavior.
+ * @param systemInstruction The persona or constraints for the model.
+ * @param prompt The string instruction sent to the model.
+ * @returns The text string response generated by the model.
+ */
+export async function generateWithSystemInstruction(systemInstruction: string, prompt: string): Promise {
+ try {
+ const model = getAiModel('gemini-3.5-flash', { systemInstruction });
+
+ const result = await model.generateContent(prompt);
+ return result.response.text();
+ } catch (error) {
+ console.error('Error generating text with system instruction:', error);
+ throw error;
+ }
+}
\ No newline at end of file
diff --git a/ai/ai-samples/src/index.tsx b/ai/ai-samples/src/index.tsx
index 39ef887d8..0e344ff26 100644
--- a/ai/ai-samples/src/index.tsx
+++ b/ai/ai-samples/src/index.tsx
@@ -1,9 +1,63 @@
-import React, { StrictMode } from 'react'
-import { createRoot } from 'react-dom/client'
-import App from './App'
-
-createRoot(document.getElementById('root')!).render(
-
-
- ,
-)
+import React from 'react';
+import ReactDOM from 'react-dom/client';
+import { createBrowserRouter, RouterProvider, Navigate } from 'react-router-dom';
+import App from './App';
+import TextGeneration from './features/text-generation';
+import Chat from './features/chat';
+import Multimodal from './features/multimodal';
+import StructuredOutput from './features/structured-output';
+import FunctionCalling from './features/function-calling';
+import ImageGeneration from './features/image-generation';
+
+
+const router = createBrowserRouter([
+ {
+ path: '/',
+ element: ,
+ children: [
+ // Redirect the root path to text-generation automatically
+ { index: true, element: },
+ { path: 'text-generation', element: },
+ { path: 'chat', element: },
+ { path: 'multimodal', element: },
+ { path: 'structured-output', element: },
+ { path: 'function-calling', element: },
+ { path: 'image-generation', element: },
+ ],
+ },
+]);
+
+const isolatedFeature = import.meta.env.VITE_ISOLATED_FEATURE;
+
+// Determine what to render based on the flag
+const renderContent = () => {
+ if (isolatedFeature) {
+ switch (isolatedFeature) {
+ case 'text-generation':
+ return ;
+ case 'chat':
+ return ;
+ case 'multimodal':
+ return ;
+ case 'structured-output':
+ return ;
+ case 'function-calling':
+ return ;
+ case 'image-generation':
+ return ;
+ default:
+ // Fallback if the flag is set to something unknown
+ return ;
+ }
+ }
+
+ // If no flag provided, run the normal full app router
+ return ;
+};
+
+// Mount to the DOM
+ReactDOM.createRoot(document.getElementById('root')!).render(
+
+ {renderContent()}
+
+);
\ No newline at end of file
diff --git a/ai/ai-samples/src/services/firebaseAIService.ts b/ai/ai-samples/src/services/firebaseAIService.ts
index 03e01e3f9..8d01471e7 100644
--- a/ai/ai-samples/src/services/firebaseAIService.ts
+++ b/ai/ai-samples/src/services/firebaseAIService.ts
@@ -1 +1,36 @@
-// Core Firebase AI service setup
+import { initializeApp} from 'firebase/app';
+import { getAI, getGenerativeModel } from 'firebase/ai';
+// Import App Check if you haven't already
+import { initializeAppCheck, ReCaptchaEnterpriseProvider } from 'firebase/app-check';
+
+const firebaseConfig = import.meta.env.VITE_FIREBASE_CONFIG
+ ? JSON.parse(import.meta.env.VITE_FIREBASE_CONFIG)
+ : {
+ apiKey: "YOUR_API_KEY",
+ authDomain: "YOUR_AUTH_DOMAIN",
+ projectId: "YOUR_PROJECT_ID",
+ storageBucket: "YOUR_STORAGE_BUCKET",
+ messagingSenderId: "YOUR_MESSAGING_SENDER_ID",
+ appId: "YOUR_APP_ID"
+ };
+
+const app = initializeApp(firebaseConfig);
+
+// initialize app check with debug token
+if (typeof window !== 'undefined') {
+ (window as any).FIREBASE_APPCHECK_DEBUG_TOKEN = true;
+
+ initializeAppCheck(app, {
+ // The string here doesn't matter in this specific case, as setting
+ // FIREBASE_APPCHECK_DEBUG_TOKEN above means it will be ignored.
+ // However, in production, this MUST be a valid reCAPTCHA site key.
+ provider: new ReCaptchaEnterpriseProvider('YOUR_RECAPTCHA_SITE_KEY'),
+ isTokenAutoRefreshEnabled: true
+ });
+}
+
+const ai = getAI(app);
+
+export const getAiModel = (modelName: string = 'gemini-3.5-flash', additionalConfig: Record = {}) => {
+ return getGenerativeModel(ai, { model: modelName, ...additionalConfig });
+};
\ No newline at end of file
diff --git a/ai/ai-samples/tsconfig.json b/ai/ai-samples/tsconfig.json
index 3934b8f6d..498328e64 100644
--- a/ai/ai-samples/tsconfig.json
+++ b/ai/ai-samples/tsconfig.json
@@ -14,7 +14,8 @@
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
- "noFallthroughCasesInSwitch": true
+ "noFallthroughCasesInSwitch": true,
+ "types": ["vite/client"]
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]